Необходимо ли использовать std :: atomic, чтобы сигнализировать, что поток завершил выполнение?

Я хотел бы проверить, если std::thread закончил выполнение. В поисках stackoverflow я нашел следующее вопрос который решает эту проблему. В принятом ответе предлагается, чтобы рабочий поток установил переменную непосредственно перед выходом, а основной поток проверял эту переменную. Вот минимальный рабочий пример такого решения:

#include <unistd.h>
#include <thread>

void work( bool* signal_finished ) {
sleep( 5 );
*signal_finished = true;
}

int main()
{
bool thread_finished = false;
std::thread worker(work, &thread_finished);

while ( !thread_finished ) {
// do some own work until the thread has finished ...
}

worker.join();
}

Кто-то, кто прокомментировал принятый ответ, утверждает, что нельзя использовать простой bool переменная в качестве сигнала, код был взломан без барьера памяти и с помощью std::atomic<bool> было бы правильно. Мое первоначальное предположение, что это неправильно и просто bool достаточно, но я хочу убедиться, что я не пропустил что-то. Нужен ли приведенный выше код std::atomic<bool> чтобы быть правильным?

Давайте предположим, что основной поток и работник работают на разных процессорах в разных сокетах. Я думаю, что произойдет, что основной поток читает thread_finished из кеша своего процессора. Когда работник обновляет его, протокол когерентности кэша заботится о том, чтобы записать изменение работников в глобальную память и сделать недействительной кэш-память ЦП основного потока, чтобы он мог считывать обновленное значение из глобальной памяти. Разве весь смысл когерентности кэша в том, чтобы код, подобный приведенному выше, не работал?

16

Решение

Кто-то, кто прокомментировал принятый ответ, утверждает, что нельзя использовать простую переменную bool в качестве сигнала, код был разбит без барьера памяти, и использование std :: atomic было бы правильным.

Комментатор прав: простой bool недостаточно, потому что неатомарные записи из потока, который устанавливает thread_finished в true можно переупорядочить.

Рассмотрим поток, который устанавливает статическую переменную x на очень важный номер, а затем сигнализирует о его выходе, например:

x = 42;
thread_finished = true;

Когда ваш главный поток видит thread_finished установлен в true, предполагается, что рабочий поток завершен. Тем не менее, когда ваш основной поток исследует x, он может найти неправильный номер, потому что две записи выше были переупорядочены.

Конечно, это только упрощенный пример, иллюстрирующий общую проблему. С помощью std::atomic для тебя thread_finished переменная добавляет барьер памяти, убедившись, что все записи, прежде чем это сделано. Это устраняет потенциальную проблему неправильных записей.

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


Важное примечание: сделать ваш thread_finished летучий не собираюсь решить проблему; фактически, volatile не следует использовать в сочетании с многопоточностью — оно предназначено для работы с отображенным в память оборудованием.

21

Другие решения

Используя сырье bool недостаточно

Выполнение программы содержит гонка данных если он содержит два конфликтующих действия в разных потоках, по крайней мере одно из которых не является атомарным, и ни одно из них не происходит раньше другого. Любая такая гонка данных приводит к неопределенному поведению. § 1.10 с21

Две оценки выражений конфликтуют, если одна из них изменяет ячейку памяти (1.7), а другая обращается или изменяет ту же ячейку памяти. § 1.10 р4

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

Существует несколько способов избежать гонки данных, в том числе использование std::atomic<bool> с соответствующим упорядочением памяти, использованием барьера памяти или заменой bool условной переменной.

7

Это не хорошо. Оптимизатор может оптимизировать

  while ( !thread_finished ) {
// do some own work until the thread has finished ...
}

чтобы:

  if(!thread_finished)
while (1) {
// do some own work until the thread has finished ...
}

при условии, что это может доказать, что «какая-то собственная работа» не меняется thread_finished,

2

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

2
По вопросам рекламы [email protected]