Ваша программа будет оптимизирована в бесконечный цикл † .
void foo() { A{}.work(); }
компилируется в (g ++ с O2)
foo():
sub rsp, 8
.L2:
call processSomeJob()
jmp .L2
Стандарт определяет, что гипотетическая абстрактная машина будет делать с программой.Стандартно-совместимые компиляторы должны компилировать вашу программу так, чтобы она работала так же, как и эта машина, во всех наблюдаемых режимах.Это известно как правило as-if , у компилятора есть свобода, пока то, что делает ваша программа, одинаково, независимо от how .
Как правило, чтение и запись в переменную не представляется настолько наблюдаемой, поэтому компилятор может исключить столько операций чтения и записи, сколько ему захочется.Компилятор может видеть, что working
не назначен и оптимизирует чтение.Эффект (часто неправильно понимаемый) volatile
заключается именно в том, чтобы сделать их видимыми, что заставляет компиляторы оставлять чтение и запись в одиночку ‡ .
Но, подождите, вы говорите, другой поток можетприсвоить working
.Вот где появляется свобода неопределенного поведения. Компилятор может делать что угодно , когда есть неопределенное поведение, включая форматирование жесткого диска и при этом соответствие стандарту.Поскольку синхронизация отсутствует и working
не является атомарной, любой другой поток, записывающий в working
, является гонкой данных, которая безусловно является неопределенным поведением.Таким образом, единственный случай, когда бесконечный цикл является неправильным, это когда неопределенное поведение, и компилятор решил, что ваша программа может продолжать цикл.
TL; DR Не используйте обычные bool
и volatile
для многопоточности.Используйте std::atomic<bool>
.
† Не во всех ситуациях.void bar(A& a) { a.work(); }
не для некоторых версий.
‡ На самом деле, есть некоторые дебаты вокруг этого.