C++ Многопоточность
Потоки — это легковесные единицы выполнения в программе, позволяющие ей одновременно выполнять несколько задач.
Многопоточность — это особая форма многозадачности, которая позволяет компьютеру одновременно выполнять две или более программ.
В целом, два типа многозадачности:На основе процессов и на основе потоков。
- Многозадачность на основе процессов — это параллельное выполнение программ.
- Многозадачность на основе потоков — это параллельное выполнение фрагментов одной и той же программы.

Многопоточное программирование на C++ включает создание и управление несколькими одновременно выполняющимися потоками в программе.
C++ предоставляет мощную поддержку многопоточности, особенно в стандарте C++11 и более поздних, через<thread>стандартную библиотеку, делая многопоточное программирование проще и безопаснее.
Описание концепции
Поток
- Поток — это единый последовательный поток управления при выполнении программы, и несколько потоков могут выполняться независимо в рамках одного и того же процесса.
- Потоки совместно используют такие ресурсы, как адресное пространство процесса, файловые дескрипторы, кучу и глобальные переменные, но каждый поток имеет собственный стек, регистры и счетчик команд.
Конкурентность и параллелизм
- Конкурентность: Несколько задач выполняются поочерёдно в пределах временных срезов, создавая эффект одновременного выполнения.
- Параллелизм: Несколько задач выполняются одновременно на нескольких процессорах или ядрах процессора.
Стандарты C++11 и более поздние обеспечивают поддержку многопоточности; основные компоненты включают:
std::thread: Используется для создания потоков и управления ими.std::mutex: Используется для взаимного исключения между потоками, предотвращая одновременный доступ нескольких потоков к общим ресурсам.std::lock_guardиstd::unique_lock: Используется для управления захватом и освобождением блокировок.std::condition_variable: Используется для условных переменных между потоками, координируя ожидание и уведомление между потоками.std::futureиstd::promise: Используется для реализации передачи значений и синхронизации задач между потоками.
Создание потоков
После C++11 была добавлена новая стандартная библиотека потоков.std::thread,std::thread в <thread>объявлен в заголовочном файле, поэтому при использованииstd::threadнужно включитьв <thread>заголовочный файл.
Примечание:Ранее некоторые компиляторы использовали в качестве флага компиляции для C++11-std=c++11:
g++ -std=c++11 test.cpp
std::thread
Создание потока с помощью указателя на функцию — самый базовый способ: Использованиеg++ -std=c++11После компиляции вывод при выполнении: с помощью класса Использованиеg++ -std=c++11После компиляции вывод при выполнении: Лямбда-выражения могут напрямую определять встроенный код для выполнения потоком: Использованиеg++ -std=c++11После компиляции вывод при выполнении: join() используется для ожидания завершения выполнения потока. Если объект потока уничтожается напрямую без вызова join() или detach(), это приведет к сбою программы. detach() отделяет поток от главного потока; поток выполняется независимо в фоновом режиме, и главный поток больше не ждет его. Параметры можно передавать в потоки по значению: Если нужно передать параметры по ссылке, необходимо использовать std::ref: Ниже приведён полный пример, показывающий, как использовать три вышеописанных способа для создания потоков и управления потоками. Флаг компиляции для использования C++11-c++11: Вывод приведённого выше кода может отличаться на разных платформах или между запусками, поскольку порядок выполнения потоков определяется алгоритмом планирования операционной системы. Несколько потоков выполняются одновременно, и вывод может чередоваться, например: В многопоточном программировании синхронизация потоков и взаимное исключение — два очень важных понятия. Они используются для управления доступом нескольких потоков к общим ресурсам, чтобы избежать гонок данных, взаимоблокировок и других проблем. Мьютекс — это примитив синхронизации, используемый для предотвращения одновременного доступа нескольких потоков к общим ресурсам. Когда потоку нужно получить доступ к общему ресурсу, он сначала должен заблокировать мьютекс. Если мьютекс уже заблокирован другим потоком, поток, запрашивающий блокировку, будет заблокирован до тех пор, пока мьютекс не будет разблокирован. std::mutex: используется для защиты общих ресурсов и предотвращения гонок данных.
std::lock_guardиstd::unique_lock: автоматически управляют получением и освобождением блокировок. Пример использования мьютекса: C++ предоставляет несколько типов блокировок для упрощения использования и управления мьютексами. К распространённым типам блокировок относятся: Пример использования блокировок: Переменные условия используются для координации между потоками, позволяя одному или нескольким потокам ожидать наступления определённого условия. Обычно они используются вместе с мьютексами для достижения синхронизации между потоками. std::condition_variableИспользуется для реализации механизмов ожидания и уведомления между потоками. Пример использования переменных условия: Атомарные операции означают, что доступ к общим данным неделим, то есть в многопоточной среде атомарная операция либо выполняется полностью, либо не выполняется вовсе, без промежуточного состояния. Пример использования атомарных операций: Локальная память потока позволяет каждому потоку иметь собственную копию данных. Это можно реализовать с помощью ключевого слова thread_local, избегая конкуренции за общие ресурсы. Пример использования локальной памяти потока: Взаимоблокировка возникает, когда несколько потоков ждут, пока другие освободят ресурсы, но ни один поток не может продолжить выполнение. Стратегии предотвращения взаимоблокировок включают: std::future и std::promise: реализация передачи значений между потоками. Очереди сообщений (основанные на std::queue и std::mutex) реализуют простое взаимодействие между потоками. В C++17 была представлена библиотека параллельных алгоритмов ( См. дополнительные примеры: Многопоточность C++:http://www.example.com/w3cnote/cpp-multithread-demo.html C++ std::thread: https://www.example.com/w3cnote/cpp-std-thread.html#include<thread>
std::thread thread_object(callable, args...);
callable: Вызываемые объекты, которые могут быть указателями на функции, функциональными объектами, лямбда-выражениями и т. д.args...: передаётся вcallableСписок параметров.Использование указателей на функции
Пример
#include <thread>
void printMessage(int count) {
for (int i = 0; i < count; ++i) {
std::cout << "Hello from thread (function pointer)!\n";
}
}
int main() {
std::thread t1(printMessage, 5); // Создать поток, передав указатель на функцию и параметры
t1.join(); // Ожидать завершения потока
return 0;
}
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Использование функциональных объектов
operator()Определите метод функционального объекта для создания потока:Пример
#include <thread>
class PrintTask {
public:
void operator()(int count) const {
for (int i = 0; i < count; ++i) {
std::cout << "Привет из потока (функциональный объект)!\n";
}
}
};
int main() {
std::thread t2(PrintTask(), 5); // Создать поток, передав функциональный объект и параметры
t2.join(); // Ожидать завершения потока
return 0;
}
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Использование лямбда-выражений
Пример
#include <thread>
int main() {
std::thread t3([](int count) {
for (int i = 0; i < count; ++i) {
std::cout << "Привет из потока (лямбда)!\n";
}
}, 5); // Создать поток, передав лямбда-выражение и параметры
t3.join(); // Ожидать завершения потока
return 0;
}
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Управление потоками
join()
t.join();
detach()
t.detach();
Передача параметров в поток
Передача по значению
std::thread t(func, arg1, arg2);
Передача по ссылке
Пример
#include <thread>
void increment(int& x) {
++x;
}
int main() {
int num = 0;
std::thread t(increment, std::ref(num)); // Используйте std::ref для передачи ссылок
t.join();
std::cout << "Значение после инкремента: " << num << std::endl;
return 0;
}
Комплексный пример
Пример
#include <thread>
using namespace std;
// Простая функция, служащая входной функцией потока
void foo(int Z) {
for (int i = 0; i < Z; i++) {
cout << "Поток использует указатель на функцию в качестве вызываемого параметра\n";
}
}
// Определение класса вызываемого объекта
class ThreadObj {
public:
void operator()(int x) const {
for (int i = 0; i < x; i++) {
cout << "Поток использует функциональный объект в качестве вызываемого параметра\n";
}
}
};
int main() {
cout << "Потоки 1, 2 и 3 выполняются независимо" << endl;
// Создание потока с помощью указателя на функцию
thread th1(foo, 3);
// Создание потока с помощью функционального объекта
thread th2(ThreadObj(), 3);
// Создание потока с использованием лямбда-выражения
thread th3([](int x) {
for (int i = 0; i < x; i++) {
cout << "Поток использует лямбда-выражение в качестве вызываемого параметра\n";
}
}, 3);
// Ожидание завершения потоков
th1.join(); // Ожидание завершения потока th1
th2.join(); // Ожидание завершения потока th2
th3.join(); // Ожидание завершения потока th3
return 0;
}
g++ -std=c++11 test.cpp
线程 1 、2 、3 独立运行
线程使用函数指针作为可调用参数
线程使用函数对象作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用函数指针作为可调用参数
...
Синхронизация потоков и взаимное исключение
1. Мьютекс
std::mutex mtx;
mtx.lock(); // 锁定互斥锁
// 访问共享资源
mtx.unlock(); // 释放互斥锁
std::lock_guard<std::mutex> lock(mtx); // 自动锁定和解锁
// 访问共享资源
Пример
std::mutex mtx; // Глобальный мьютекс
void safeFunction() {
mtx.lock(); // Запрос на блокировку мьютекса
// Доступ к общим ресурсам или их изменение
mtx.unlock(); // Освобождение мьютекса
}
int main() {
std::thread t1(safeFunction);
std::thread t2(safeFunction);
t1.join();
t2.join();
return 0;
}
2. Блокировки
Пример
std::mutex mtx;
void safeFunctionWithLockGuard() {
std::lock_guard<std::mutex> lk(mtx);
// Доступ к общим ресурсам или их изменение
}
void safeFunctionWithUniqueLock() {
std::unique_lock<std::mutex> ul(mtx);
// Доступ к общим ресурсам или их изменение
// ul.unlock(); // Необязательно: разблокировать вручную
// ...
}
3. Условная переменная
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件满足
// 条件满足后执行
Пример
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void workerThread() {
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, []{ return ready; }); // Ожидание условия
// Выполнить работу, когда условие выполнено
}
void mainThread() {
{
std::lock_guard<std::mutex> lk(mtx);
// Подготовить данные
ready = true;
} // Разблокировать при выходе из области видимости
cv.notify_one(); // Уведомить ожидающий поток
}
4. Атомарные операции
Пример
#include <thread>
std::atomic<count> count(0);
void increment() {
count.fetch_add(1,::memory_order_relaxed);
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return // Каждый поток имеет собственную копию threadData; // Should return 2
}
. Локальная память потока (TLS)
Пример
#include <thread>
thread_local int threadData = 0;
void threadFunction() {
threadData = 42; // Каждый поток имеет собственную копию threadData
std::cout << "Данные потока: " << threadData << std::endl;
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
6. Взаимоблокировки и стратегии предотвращения
Взаимодействие между потоками
Пример
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([&p] {
p.set_value(10); // Установить значение, активировать future
});
int result = f.get(); // Получить значение
<algorithm>), где некоторые алгоритмы поддерживают параллельное выполнение, используя многоядерные процессоры для повышения производительности.Пример
#include <vector>
#include <execution>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(std::execution::par, vec.begin(), vec.end(), [](int &n) {
n *= 2;
});
Параллельные алгоритмы в стандартной библиотеке C++
Другие расширения