Многопоточное выполнение и гонки данных (начиная с C++11)
Поток выполнения — это поток управления внутри программы, который начинается с вызова определённой функции верхнего уровня (посредством std::thread, std::async, std::jthread(начиная с C++20) или иным способом) и рекурсивно включает каждый последующий вызов функции, выполняемый этим потоком.
- Когда один поток создаёт другой, начальный вызов функции верхнего уровня нового потока выполняется новым потоком, а не потоком, который его создал.
Любой поток потенциально может обращаться к любому объекту и функции в программе:
- Объекты с автоматической и потоковой длительностью хранения по-прежнему могут быть доступны из другого потока через указатель или ссылку.
- В реализации с поддержкой хостовой среды программа на C++ может иметь несколько потоков, работающих одновременно. Выполнение каждого потока происходит в соответствии с определениями, приведёнными в остальной части этой страницы. Выполнение всей программы состоит из выполнения всех её потоков.
- В автономной реализации возможность наличия в программе более одного потока выполнения определяется реализацией.
Для обработчика сигнала, который выполняется не в результате вызова std::raise, не указано, какой поток выполнения содержит вызов обработчика сигнала.
Гонки данных
Разным потокам выполнения всегда разрешено одновременно обращаться (читать и изменять) к разным областям памяти без взаимного влияния и требований синхронизации.
Две операции вычисления выражений конфликтуют, если одна из них изменяет область памяти или начинает/завершает время жизни объекта в области памяти, а другая читает или изменяет ту же область памяти либо начинает/завершает время жизни объекта, занимающего хранилище, которое перекрывается с этой областью памяти.
В программе возникает гонка данных, если в ней имеются две конфликтующие операции вычисления, за исключением случаев, когда
- обе операции вычисления выполняются в одном потоке или в одном и том же обработчике сигнала, или
- обе конфликтующие операции вычисления являются атомарными операциями (см.
std::atomic), или - одна из конфликтующих операций вычисления предшествует по порядку выполнения другой (см.
std::memory_order).
Если возникает гонка данных, поведение программы не определено.
(В частности, освобождение std::mutex синхронизировано с захватом того же мьютекса другим потоком и, следовательно, предшествует по порядку выполнения ему, что позволяет использовать блокировки мьютексов для защиты от гонок данных.)
int cnt = 0;
auto f = [&] { cnt++; };
std::thread t1{f}, t2{f}, t3{f}; // undefined behavior
std::atomic<int> cnt{0};
auto f = [&] { cnt++; };
std::thread t1{f}, t2{f}, t3{f}; // OK
Гонки данных в контейнерах
Все контейнеры стандартной библиотеки, кроме std::vector<bool>, гарантируют, что одновременное изменение содержимого объектов, находящихся в разных элементах одного контейнера, никогда не приведёт к гонкам данных.
std::vector<int> vec = {1, 2, 3, 4};
auto f = [&](int index) { vec[index] = 5; };
std::thread t1{f, 0}, t2{f, 1}; // OK
std::thread t3{f, 2}, t4{f, 2}; // undefined behavior
std::vector<bool> vec = {false, false};
auto f = [&](int index) { vec[index] = true; };
std::thread t1{f, 0}, t2{f, 1}; // undefined behavior
Порядок памяти
При чтении значения из области памяти поток может увидеть начальное значение, значение, записанное в том же потоке, или значение, записанное в другом потоке. Подробности о порядке, в котором записи, сделанные потоками, становятся видимыми для других потоков, см. в std::memory_order.
Прогресс выполнения
Свобода от препятствий
Если только один поток, не заблокированный в функции стандартной библиотеки, выполняет атомарную функцию без блокировок, выполнение гарантированно завершится (все атомарные операции стандартной библиотеки без блокировок обладают свойством свободы от препятствий).
Свобода от блокировок
Если одна или несколько атомарных функций без блокировок выполняются одновременно, гарантируется завершение по крайней мере одной из них (все атомарные операции стандартной библиотеки без блокировок обладают свойством свободы от блокировок — реализация должна обеспечить, чтобы другие потоки не могли бесконечно удерживать их в состоянии активного ожидания, например постоянно перехватывая строку кэша).
Гарантия прогресса
В корректной программе на C++ каждый поток в конечном итоге совершает одно из следующих действий:
- Завершает выполнение.
- Вызывает
std::this_thread::yield. - Вызывает функцию ввода-вывода библиотеки.
- Выполняет обращение через glvalue типа volatile.
- Выполняет атомарную операцию или операцию синхронизации.
- Продолжает выполнение тривиального бесконечного цикла (см. ниже).
Считается, что поток совершает прогресс, если он выполняет один из перечисленных выше шагов выполнения, блокируется в функции стандартной библиотеки или вызывает атомарную функцию без блокировок, которая не завершается из-за незаблокированного параллельного потока.
Это позволяет компиляторам удалять, объединять и переупорядочивать циклы, не имеющие наблюдаемого поведения, не доказывая, что они в конечном итоге завершатся: компилятор может предполагать, что ни один поток выполнения не может работать бесконечно, не совершая ни одного из таких наблюдаемых действий. Для тривиальных бесконечных циклов сделано исключение: их нельзя удалять или переупорядочивать.
Тривиальные бесконечные циклы
Тривиально пустой оператор цикла — это оператор цикла, соответствующий одной из следующих форм:
while ( condition ) ; | (1) | |
while ( condition ) { } | (2) | |
do ; while ( condition ) ; | (3) | |
do { } while ( condition ) ; | (4) | |
for ( init-statement condition (необязательно) ; ) ; | (5) | |
for ( init-statement condition (необязательно) ; ) { } | (6) |
Управляющим выражением тривиально пустого оператора цикла является:
true.Тривиальный бесконечный цикл — это тривиально пустой оператор цикла, преобразованное управляющее выражение которого является константным выражением, если оно явно вычисляется как константное, и вычисляется в true.
Тело тривиального бесконечного цикла заменяется вызовом функции std::this_thread::yield. Выполняется ли эта замена в автономных реализациях, определяется реализацией.
for (;;); // trivial infinite loop, well defined as of P2809
for (;;) { int x; } // undefined behavior
Параллельный прогресс выполненияЕсли поток обеспечивает гарантию параллельного прогресса, он будет совершать прогресс (в приведённом выше смысле) за конечное время, пока не завершится, независимо от того, совершают ли прогресс другие потоки (если они есть). Стандарт рекомендует, но не требует, чтобы главный поток и потоки, запущенные посредством Параллельный прогресс выполненияЕсли поток обеспечивает гарантию параллельного прогресса, реализация не обязана обеспечивать, чтобы поток в конечном итоге совершил прогресс, если он ещё не выполнил ни одного шага выполнения (ввода-вывода, обращения к volatile, атомарной операции или операции синхронизации). Однако после выполнения таким потоком одного шага он обеспечивает гарантии параллельного прогресса (это правило описывает поток в пуле потоков, который выполняет задачи в произвольном порядке). Слабый параллельный прогресс выполненияЕсли поток обеспечивает гарантию слабого параллельного прогресса, он не гарантирует, что в конечном итоге совершит прогресс, независимо от того, совершают ли прогресс другие потоки. Тем не менее гарантировать прогресс таких потоков можно, блокируя их с делегированием гарантии прогресса: если поток Параллельные алгоритмы стандартной библиотеки C++ блокируются с делегированием гарантии прогресса в ожидании завершения неопределённого набора потоков, управляемых библиотекой. | (начиная с C++17) |
Отчёты об ошибках
Следующие отчёты об ошибках, изменяющие поведение, были применены задним числом к ранее опубликованным стандартам C++.
| DR | Применено к | Поведение согласно опубликованной версии | Корректное поведение |
|---|---|---|---|
| CWG 1953 | C++11 | две операции вычисления, начинающие/завершающие время жизни объектов с перекрывающимся хранилищем, не конфликтовали | они конфликтуют |
| LWG 2200 | C++11 | не было ясно, применяется ли требование об отсутствии гонок данных в контейнерах только к контейнерам последовательностей | применяется ко всем контейнерам |
| P2809R3 | C++11 | поведение при выполнении «тривиальных»[1] бесконечных циклов было неопределённым | «тривиальные бесконечные циклы» получили корректное определение, и поведение стало определённым |
- Здесь «тривиальный» означает, что выполнение бесконечного цикла никогда не совершает прогресс.
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/multithread