Я написал простую многопоточную программу следующим образом:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Он ведет себя нормально в режиме отладки в Visual studio или -O0
в gc c и выводит результат через 1
несколько секунд. Но он застрял и ничего не печатает в режиме выпуска или -O1 -O2 -O3
.
c++
multithreading
thread-safety
data-race
Sz Ppeter
источник
источник
Ответы:
UB Это касается двух потоков, обращающихся к неатомарным неохраняемым переменным
finished
. Вы можете сделатьfinished
тип,std::atomic<bool>
чтобы исправить это.Мое исправление:
Вывод:
Живая Демо на Колиру
Кто-то может подумать: «Это
bool
- возможно, один бит. Как это может быть неатомным? (Я сделал, когда я начал с многопоточности самостоятельно.)Но обратите внимание, что отсутствие слез не единственное, что
std::atomic
дает вам. Это также делает одновременный доступ для чтения и записи из нескольких потоков четко определенным, не позволяя компилятору предполагать, что повторное чтение переменной всегда будет видеть одно и то же значение.Создание
bool
неохраняемого, неатомного может вызвать дополнительные проблемы:atomic<bool>
сmemory_order_relaxed
магазином / нагрузка будет работать, но там , гдеvolatile
не будет. Использование энергозависимым для этого будет UB, даже если он работает на реальных реализациях C ++.)Чтобы этого не случилось, компилятору должно быть явно сказано не делать этого.
Я немного удивлен развивающейся дискуссией о потенциальном отношении
volatile
к этой проблеме. Таким образом, я хотел бы потратить свои два цента:источник
func()
и подумал: «Я мог бы оптимизировать это». Оптимизатор не заботится о потоках, он обнаружит бесконечный цикл и с радостью превратит его в «while (True)», если мы посмотрим на Godbolt. .org / z / Tl44iN мы видим это. Если закончено,True
это возвращается. Если нет, то он переходит в безусловный переход к самому себе (бесконечный цикл) на лейбле.L5
volatile
в C ++ 11 нет причин злоупотреблять, потому что вы можете получить идентичный asm с помощьюatomic<T>
иstd::memory_order_relaxed
. Это работает, хотя на реальном оборудовании: кэши являются связными, поэтому инструкция загрузки не может продолжать считывать устаревшие значения, как только хранилище на другом ядре решает кешировать там. (MESI)volatile
все еще UB все же. Вы действительно никогда не должны предполагать что-то, что определенно и ясно, что UB безопасен только потому, что вы не можете придумать, как это может пойти не так, и это сработало, когда вы попробовали это. Это сжигало людей снова и снова.finished
выполняетсяstd::mutex
(безvolatile
илиatomic
). Фактически, вы можете заменить все атомы на «простую» схему value + mutex; это все равно будет работать и будет медленнее.atomic<T>
разрешено использовать внутренний мьютекс; толькоatomic_flag
гарантировано без блокировки.Ответ Шеффа описывает, как исправить ваш код. Я думал, что добавлю немного информации о том, что на самом деле происходит в этом случае.
Я скомпилировал ваш код в Godbolt, используя уровень оптимизации 1 (
-O1
). Ваша функция компилируется так:Итак, что здесь происходит? Во-первых, у нас есть сравнение:
cmp BYTE PTR finished[rip], 0
- это проверяет,finished
является ли оно ложным или нет.Если это не ложь (иначе истина), мы должны выйти из цикла при первом запуске. Это достигается путем
jne .L4
которого J umps при п OT е каче к этикетке ,.L4
где значениеi
(0
) хранится в регистре для последующего использования и функция возвращает.Если это является ложным , однако, мы переходим к
Это безусловный переход, для обозначения
.L5
которого именно так и происходит сама команда перехода.Другими словами, поток помещается в бесконечный цикл занятости.
Так почему же это произошло?
Что касается оптимизатора, потоки находятся за пределами его компетенции. Предполагается, что другие потоки не читают и не записывают переменные одновременно (потому что это будет гонка данных UB). Вы должны сказать ему, что он не может оптимизировать доступ. Здесь приходит ответ Шеффа. Я не буду его повторять.
Поскольку оптимизатору не сообщают, что
finished
переменная может потенциально измениться во время выполнения функции, он видит, чтоfinished
она не изменена самой функцией, и предполагает, что она постоянна.Оптимизированный код обеспечивает два пути кода, которые будут получены при вводе функции с постоянным значением bool; либо он запускает цикл бесконечно, либо цикл никогда не запускается.
у
-O0
компилятора (как и ожидалось) не оптимизируется тело цикла и сравнение отсюда:поэтому функция, когда она неоптимизирована, работает, отсутствие атомарности здесь обычно не является проблемой, потому что код и тип данных просты. Вероятно, худшее, с чем мы могли бы здесь столкнуться, - это значение
i
, которое на единицу меньше, чем должно быть.Более сложная система со структурами данных может привести к повреждению данных или неправильному выполнению.
источник
atomic
переменные в коде, который не записывает эти переменные. Например, онif (cond) foo=1;
не может быть преобразован в asm, это похоже наfoo = cond ? 1 : foo;
то, что загрузка + хранилище (не атомарный RMW) может перейти на запись из другого потока. Компиляторы уже избегали подобных вещей, потому что они хотели быть полезными для написания многопоточных программ, но C ++ 11 официальноa[1]
a[2]
Ради полноты кривой обучения; Вы должны избегать использования глобальных переменных. Вы хорошо поработали, сделав его статичным, поэтому он будет локальным для модуля перевода.
Вот пример:
Жить на wandbox
источник
finished
какstatic
внутри функционального блока. Он все равно будет инициализирован только один раз, и если он инициализирован константой, это не требует блокировки.finished
также может использовать более дешевуюstd::memory_order_relaxed
нагрузку и магазины; Там нет необходимости заказа в отношении. другие переменные в любом потоке. Я не уверен, что предложение @ Дэвислораstatic
имеет смысл, хотя; если бы у вас было несколько потоков с подсчетом вращений, вам не нужно останавливать их все с одним и тем же флагом. Вы действительно хотите написать инициализациюfinished
так, чтобы компилировать это просто для инициализации, а не для атомарного хранилища. (Как вы делаете сfinished = false;
синтаксисом инициализатора C ++ 17 по умолчанию. Godbolt.org/z/EjoKgq ).