Нужно ли использовать 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 из кеша своего процессора. Когда рабочий процесс обновляет его, протокол когерентности кеша заботится о записи изменений рабочих процессов в глобальную память и аннулировании кеша ЦП основного потока, поэтому он должен считывать обновленное значение из глобальной памяти. Разве весь смысл когерентности кеша не в том, чтобы код, подобный приведенному выше, просто работал?


person Robert Rüger    schedule 16.01.2013    source источник
comment
Почему бы вам не использовать переменную условия, семафор или событие автоматического сброса, чтобы сигнализировать потоку? Вот для чего нужны эти вещи.   -  person Tony The Lion    schedule 16.01.2013
comment
Проблема может возникнуть, если компилятор выполнил некоторую оптимизацию, основанную на том факте, что вы снова и снова проверяете значение переменной и в конечном итоге изменяете поведение приложения. Я никогда не видел, чтобы это происходило, но я слышал, что это может быть причиной использования атомарных чисел, а не простых логических значений.   -  person mfontanini    schedule 16.01.2013
comment
Боже мой, когда мы писали требования к многопоточности для C++11, мы полностью забыли о магии алгоритмов когерентности кэша!   -  person Pete Becker    schedule 16.01.2013
comment
@TonyTheLion: условные переменные, семафоры и события предназначены для случаев, когда вы хотите подождать (приостановить поток), пока что-то не произойдет. Он просто хочет проверить, не произошло ли что-то, поэтому atomic bool подходит больше.   -  person Andrew Tomazos    schedule 16.01.2013
comment
для связанного вопроса: stackoverflow.com/q/12507705/819272   -  person TemplateRex    schedule 17.01.2013
comment
а также комментарии к этому ответу: stackoverflow.com/a/12087141/819272   -  person TemplateRex    schedule 17.01.2013
comment
stackoverflow.com/questions/9224542/   -  person Zan Lynx    schedule 21.01.2016


Ответы (4)


Кто-то, кто прокомментировал принятый ответ, утверждает, что нельзя использовать простую логическую переменную в качестве сигнала, код был взломан без барьера памяти, и использование 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 не решит проблему; на самом деле volatile не следует использовать вместе с threading — он предназначен для работы с оборудованием с отображением памяти.

person Sergey Kalinichenko    schedule 16.01.2013
comment
+1 За упоминание энергозависимого оборудования и оборудования с отображением памяти. Мало кто это понимает :) - person Jesus Ramos; 16.01.2013
comment
volatile предотвратит оптимизацию логического значения, но не предоставит мембар или не гарантирует каких-либо временных рамок для проверки согласованности кеша с основной памятью. atomic bool - правильный ход. - person Andrew Tomazos; 16.01.2013
comment
убедившись, что все записи до того, как это будет выполнено, звучит так, как будто это случай x = 42; membar; thread_finished = true; или x = 42; thread_finished = true; membar; - первое не обеспечит своевременную видимость обновления thread_finished, последнее может рискнуть переупорядоченным раскрытием до в дело вступает membar. Мембар на самом деле может быть префиксом/модификатором для x = 42; membar(thread_finished = true);, обеспечивающим относительный порядок ‹= видимости и своевременную видимость обоих обновлений, или его необходимо использовать дважды для x = 42; membar; thread_finished = true; membar. Ваше здоровье. - person Tony Delroy; 16.07.2014

Использование необработанного bool недостаточно.

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

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

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

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

person bames53    schedule 16.01.2013

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

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

to:

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

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

person zch    schedule 16.01.2013

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

person Pete Becker    schedule 16.01.2013