Последнее обновление:2026-10-12
Введение в многопоточность C++¶
Весь код из этой статьи находится в репозитории «C++那些事».
1. Понимание потоков и процессов¶
Поток — базовая единица планирования, процесс — базовая единица распределения ресурсов. Программу можно рассматривать как процесс, а процесс, в свою очередь, содержит несколько потоков.
Например: браузер — это процесс, а каждая открытая вкладка — поток.
Краткие различия между ними:
- Адресное пространство и другие ресурсы: процессы независимы друг от друга, а потоки одного процесса разделяют их. Потоки внутри одного процесса невидимы для других процессов.
- Связь: между процессами — IPC, между потоками — можно напрямую читать и писать сегмент данных процесса (например, глобальные переменные) — с привлечением средств синхронизации и взаимного исключения для гарантии согласованности данных.
- Планирование и переключение: переключение контекста потоков происходит гораздо быстрее, чем переключение контекста процессов.
- В многопоточной ОС процесс не является исполняемой сущностью.
О связи через IPC и обмене между потоками будет отдельная статья.
2. Пять способов создания потоков¶
- указатель на функцию
- лямбда-функция
- Functor (функциональный объект)
- нестатическая функция-член
- статическая функция-член
2.1 Указатель на функцию¶
// 1.函数指针
void fun(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
// 调用
std::thread t1(fun, 10);
t1.join();
2.2 Лямбда-функция¶
// 注意:如果我们创建多线程 并不会保证哪一个先开始
int main() {
// 2.Lambda函数
auto fun = [](int x) {
while (x-- > 0) {
cout << x << endl;
}
};
// std::1.thread t1(fun, 10);
// 也可以写成下面:
std::thread t1_1([](int x) {
while (x-- > 0) {
cout << x << endl;
}
}, 11);
// std::1.thread t2(fun, 10);
// t1.join();
t1_1.join();
// t2.join();
return 0;
}
2.3 Функциональный объект¶
// 3.functor (Funciton Object)
class Base {
public:
void operator()(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
};
// 调用
thread t(Base(), 10);
t.join();
2.4 Нестатическая функция-член¶
// 4.Non-static member function
class Base {
public:
void fun(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
};
// 调用
thread t(&Base::fun,&b, 10);
t.join();
2.5 Статическая функция-член¶
// 4.Non-static member function
class Base {
public:
static void fun(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
};
// 调用
thread t(&Base::fun, 10);
t.join();
3. join и detach¶
3.1 join¶
- как только поток запущен, чтобы дождаться его завершения, нужно вызвать на объекте join()
- двойной join приведёт к завершению программы
- перед join следует проверить, можно ли его выполнить, с помощью joinable()
void run(int count) {
while (count-- > 0) {
cout << count << endl;
}
std::this_thread::sleep_for(chrono::seconds(3));
}
int main() {
thread t1(run, 10);
cout << "main()" << endl;
t1.join();
if (t1.joinable()) {
t1.join();
}
cout << "main() after" << endl;
return 0;
}
3.2 detach¶
- это используется для отделения вновь созданного потока от родительского
- перед отделением потока обязательно проверьте, является ли он joinable, иначе возможно двойное отделение, а двойной detach() приведёт к завершению программы
- если у нас есть отделённый поток и функция main возвращается, выполнение отделённого потока будет приостановлено
void run(int count) {
while (count-- > 0) {
cout << count << endl;
}
std::this_thread::sleep_for(chrono::seconds(3));
}
int main() {
thread t1(run, 10);
cout << "main()" << endl;
t1.detach();
if(t1.joinable())
t1.detach();
cout << "main() after" << endl;
return 0;
4. Критические секции и мьютексы¶
4.1 Что такое критическая секция (Critical Sections)?¶
Критическая секция — это участок кода, который для корректной работы программы должен выполняться одновременно только одним потоком. Если два потока (или процесса) одновременно выполняют код внутри критической секции, программа может перестать работать корректно.
4.2 Является ли простое увеличение переменной критической секцией?¶
Пожалуй, да.
Процесс увеличения переменной (i++) состоит из трёх шагов:
- копирование содержимого памяти в регистр CPU. load
- увеличение значения в CPU. increment
- сохранение нового значения в памяти. store
Если к этой ячейке памяти (например, к переменной i ниже) возможен доступ только одного потока, гонки не возникает и связанной с i критической секции нет. Но переменная sum — глобальная, к ней могут обращаться два потока. Два потока могут попытаться увеличить переменную одновременно.
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
int sum = 0; //shared
mutex m;
void *countgold() {
int i; //local to each thread
for (i = 0; i < 10000000; i++) {
sum += 1;
}
return NULL;
}
int main() {
thread t1(countgold);
thread t2(countgold);
//Wait for both threads to finish
t1.join();
t2.join();
cout << "sum = " << sum << endl;
return 0;
}
4.3 Как гарантировать, что только один поток одновременно обращается к глобальной переменной?¶
Если один поток сейчас находится в критической секции, мы хотим, чтобы другой поток ждал, пока первый завершит. Для этого можно использовать мьютекс (сокращение от «взаимное исключение»).
Наглядная метафора мьютекса:

Простой способ не пустить других внутрь — повесить на дверь замок. Пришедший первым запирает дверь, пришедшие позже видят замок и выстраиваются у двери, дожидаясь, когда замок откроют. Это и называется "мьютексом" (Mutual exclusion, сокращённо Mutex) — он не даёт нескольким потокам одновременно читать и писать одну область памяти.
Приведённый выше код выводит корректно:sum = 20000000.
Справочные материалы¶
https://www.youtube.com/watch?v=eZ8yKZo-PGw&list=PLk6CEY9XxSIAeK-EAh3hB4fgNvYkYmghp&index=4
Похожие темы¶
- Путеводитель по C++ Concurrency in Action | C++那些事
- Поиск взаимоблокировок в C++ | C++那些事
- Векторизация C++ SIMD | C++那些事
- Параллельный STL в C++17 | C++那些事
- C++ False Sharing и строка кэша | C++那些事
- Отладка многопоточности GDB в режиме All-Stop | C++那些事
- Обнаружение ошибок конкурентности с помощью Helgrind | C++那些事
- Принцип событийного цикла libev | C++那些事
- Внутренняя реализация C++ std::thread | C++那些事
- C++ std::thread и jthread | C++那些事