Namespaces
Variants

Multi-threaded executions and data races (since C++11)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Поток выполнения — это поток управления внутри программы, который начинается с вызова определенной функции верхнего уровня (с помощью std::thread , std::async , std::jthread (since C++20) или другими способами) и рекурсивно включает каждый вызов функции, впоследствии выполняемый потоком.

  • Когда один поток создаёт другой, первоначальный вызов функции верхнего уровня нового потока выполняется новым потоком, а не создающим потоком.

Любой поток потенциально может получить доступ к любому объекту и функции в программе:

  • Объекты с автоматической и потоково-локальной длительностью хранения всё ещё могут быть доступны другому потоку через указатель или по ссылке.
  • В рамках hosted-реализации , программа на C++ может иметь более одного потока, выполняющегося параллельно. Выполнение каждого потока происходит в соответствии с остальными положениями этой страницы. Выполнение всей программы состоит из выполнения всех её потоков.
  • В рамках freestanding-реализации , определяется реализацией, может ли программа иметь более одного потока выполнения.

Для обработчика сигнала , который не выполняется в результате вызова std::raise , не определено, в каком потоке выполнения происходит вызов обработчика сигнала.

Содержание

Гонки данных

Различные потоки выполнения всегда могут обращаться (читать и изменять) различные области памяти параллельно, без вмешательства и требований синхронизации.

Два вычисления выражений evaluations конфликтуют если одно из них изменяет область памяти или начинает/завершает время жизни объекта в области памяти, а другое читает или изменяет ту же область памяти или начинает/завершает время жизни объекта, занимающего хранилище, которое перекрывается с данной областью памяти.

Программа, имеющая два конфликтующих вычисления, содержит data race если только

  • обе оценки выполняются в одном потоке или в одном обработчике сигналов , или
  • обе конфликтующие оценки являются атомарными операциями (см. std::atomic ), или
  • одна из конфликтующих оценок happens-before другой (см. std::memory_order ).

Если возникает гонка данных, поведение программы не определено.

(В частности, освобождение std::mutex синхронизируется-с , и, следовательно, происходит-до захвата того же мьютекса другим потоком, что позволяет использовать блокировки мьютексов для защиты от гонок данных.)

int cnt = 0;
auto f = [&] { cnt++; };
std::thread t1{f}, t2{f}, t3{f}; // неопределённое поведение
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}; // неопределённое поведение
std::vector<bool> vec = {false, false};
auto f = [&](int index) { vec[index] = true; };
std::thread t1{f, 0}, t2{f, 1}; // неопределённое поведение

Порядок памяти

Когда поток читает значение из ячейки памяти, он может увидеть начальное значение, значение, записанное в том же потоке, или значение, записанное в другом потоке. См. std::memory_order для получения подробной информации о порядке, в котором записи, сделанные из потоков, становятся видимыми для других потоков.

Прогресс выполнения

Свобода от блокировок

Когда только один поток, не заблокированный в функции стандартной библиотеки, выполняет атомарную функцию , которая является lock-free, эта операция гарантированно завершится (все lock-free операции стандартной библиотеки являются obstruction-free ).

Свобода от блокировок

Когда одна или несколько функций с атомарными операциями без блокировок выполняются параллельно, гарантируется, что хотя бы одна из них завершится (все операции стандартной библиотеки без блокировок являются lock-free — задача реализации обеспечить, чтобы они не могли быть бесконечно заблокированы другими потоками, например, путем постоянного перехвата кэш-линии).

Гарантия прогресса

В корректной программе на C++ каждый поток в конечном счете выполняет одно из следующих действий:

  • Завершается.
  • Вызывает std::this_thread::yield .
  • Выполняет вызов функции ввода-вывода библиотеки.
  • Выполняет доступ через volatile glvalue.
  • Выполняет атомарную операцию или операцию синхронизации.
  • Продолжает выполнение тривиального бесконечного цикла (см. ниже).

Говорят, что поток делает прогресс если он выполняет один из вышеуказанных шагов выполнения, блокируется в функции стандартной библиотеки, или вызывает атомарную безблокировочную функцию, которая не завершается из-за незаблокированного параллельного потока.

Это позволяет компиляторам удалять, объединять и переупорядочивать все циклы, не имеющие наблюдаемого поведения, без необходимости доказывать, что они в конечном счете завершатся, поскольку можно предположить, что ни один поток выполнения не может выполняться вечно без выполнения каких-либо из этих наблюдаемых поведений. Предусмотрено исключение для тривиальных бесконечных циклов, которые не могут быть удалены или переупорядочены.

Тривиальные бесконечные циклы

Тривиально пустой оператор итерации — это оператор итерации, соответствующий одной из следующих форм:

while ( условие ) ; (1)
while ( условие ) { } (2)
do ; while ( условие ) ; (3)
do { } while ( условие ) ; (4)
for ( инициализация условие  (необязательно) ; ) ; (5)
for ( инициализация условие  (необязательно) ; ) { } (6)
1) A while statement whose loop body is an empty simple statement.
2) A while statement whose loop body is an empty compound statement.
3) A do - while statement whose loop body is an empty simple statement.
4) A do - while statement whose loop body is an empty compound statement.
5) Оператор for , тело цикла которого является пустым простым оператором, не имеет выражения-итерации .
6) Оператор for statement , тело цикла которого представляет собой пустой составной оператор, не содержит iteration-expression .

Управляющее выражение тривиально пустого оператора итерации:

1-4) условие .
5,6) condition если присутствует, иначе true .

Тривиальный бесконечный цикл — это тривиально пустой оператор итерации, для которого преобразованное управляющее выражение является константным выражением , когда явно константно вычисляемое , и вычисляется в true .

Тело цикла тривиального бесконечного цикла заменяется вызовом функции std::this_thread::yield . Определяется реализацией, происходит ли эта замена в freestanding implementations .

for (;;); // тривиальный бесконечный цикл, корректно определен согласно P2809
for (;;) { int x; } // неопределенное поведение

Гарантия конкурентного прогресса

Если поток предоставляет гарантию конкурентного прогресса , он будет осуществлять прогресс (как определено выше) за конечное время, пока он не завершится, независимо от того, осуществляют ли прогресс другие потоки (если они есть).

Стандарт рекомендует, но не требует, чтобы главный поток и потоки, запущенные с помощью std::thread и std::jthread (since C++20) предоставляли гарантию конкурентного прогресса.

Гарантия параллельного прогресса

Если поток предоставляет гарантию параллельного прогресса , реализация не обязана обеспечивать, что поток в конечном счете осуществит прогресс, если он еще не выполнил ни одного шага выполнения (I/O, volatile, atomic или синхронизацию), но как только этот поток выполнил шаг, он предоставляет гарантию конкурентного прогресса (это правило описывает поток в пуле потоков, который выполняет задачи в произвольном порядке).

Гарантия слабого параллельного прогресса

Если поток предоставляет гарантию слабого параллельного прогресса , он не гарантирует, что в конечном счете осуществит прогресс, независимо от того, осуществляют ли прогресс другие потоки или нет.

Такие потоки все еще могут быть гарантированно прогрессирующими за счет блокировки с делегированием гарантии прогресса: если поток P блокируется таким образом на завершении набора потоков S , то по крайней мере один поток в S будет предоставлять гарантию прогресса, которая такая же или сильнее, чем у P . Как только этот поток завершится, другой поток в S будет аналогично усилен. Как только набор станет пустым, P разблокируется.

Параллельные алгоритмы из стандартной библиотеки C++ блокируются с делегированием прогресса на завершение неопределенного набора управляемых библиотекой потоков.

(since C++17)

Отчёты о дефектах

Следующие отчеты об изменениях в поведении, являющиеся дефектными, были применены ретроактивно к ранее опубликованным стандартам C++.

DR Applied to Behavior as published Correct behavior
CWG 1953 C++11 два вычисления выражений, которые начинают/заканчивают время жизни
объектов с перекрывающимся хранилищем, не конфликтовали
они конфликтуют
LWG 2200 C++11 было неясно, относится ли требование гонки данных контейнера
только к последовательным контейнерам
относится ко всем контейнерам
P2809R3 C++11 поведение выполнения "тривиальных" [1]
бесконечных циклов было неопределенным
корректно определяет "тривиальные бесконечные циклы"
и делает поведение определенным
  1. «Тривиальный» здесь означает, что выполнение бесконечного цикла никогда не приводит к какому-либо прогрессу.