Многопоточная программа застряла в оптимизированном режиме, но нормально работает в -O0
Я написал следующие простые многопоточные программы:
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
.
3 ответа
Два потока, обращающийся неатомарный, неохраняемый переменной UB Это касаетсяfinished
. Вы могли бы сделатьfinished
типа std::atomic<bool>
чтобы исправить это.
Мое исправление:
#include <iostream>
#include <future>
#include <atomic>
static std::atomic<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;
}
Выход:
result =1023045342
main thread id=140147660588864
Кто-то может подумать: "Это bool
- наверное, немного. Как это может быть неатомарно? (Я делал это, когда сам начинал с многопоточности.)
Но учтите, что отсутствие разрывов - не единственное, что std::atomic
дает тебе. Он также делает четко определенным одновременный доступ для чтения и записи из нескольких потоков, не позволяя компилятору предполагать, что при повторном чтении переменной всегда будет отображаться одно и то же значение.
Создание bool
неохраняемые, неатомарные могут вызвать дополнительные проблемы:
- Компилятор может решить оптимизировать переменную в регистре или даже множественный доступ CSE в один и поднять нагрузку из цикла.
- Переменная может быть кэширована для ядра ЦП. (В реальной жизни процессоры имеют согласованные кеши. Это не настоящая проблема, но стандарт C++ достаточно свободен, чтобы охватить гипотетические реализации C++ в некогерентной общей памяти, где
atomic<bool>
сmemory_order_relaxed
store / load будет работать, но гдеvolatile
не стал бы. Использование volatile для этого было бы UB, хотя на практике это работает на реальных реализациях C++.)
Чтобы этого не произошло, компилятору нужно явно запретить делать это.
Меня немного удивляет развивающаяся дискуссия о потенциальной связи volatile
к этому вопросу. Таким образом, я бы хотел потратить свои два цента:
Ответ Шеффа описывает, как исправить ваш код. Я подумал, что добавлю немного информации о том, что на самом деле происходит в этом случае.
Я скомпилировал ваш код на Godbolt, используя уровень оптимизации 1 (-O1
). Ваша функция компилируется так:
func():
cmp BYTE PTR finished[rip], 0
jne .L4
.L5:
jmp .L5
.L4:
mov eax, 0
ret
Итак, что здесь происходит? Во-первых, у нас есть сравнение:cmp BYTE PTR finished[rip], 0
- это проверяет, есть ли finished
ложно или нет.
Если это не ложь (также известная как истина), мы должны выйти из цикла при первом запуске. Это выполненоjne .L4
который J umps при п ВЗ е каче на этикетке.L4
где значение i
(0
) сохраняется в регистре для последующего использования, и функция возвращается.
Если это является ложным, однако, мы переходим к
.L5:
jmp .L5
Это безусловный прыжок, чтобы обозначить .L5
которая как раз и является самой командой перехода.
Другими словами, поток попадает в бесконечный цикл занятости.
Так почему это произошло?
Что касается оптимизатора, потоки выходят за рамки его компетенции. Предполагается, что другие потоки не читают и не записывают переменные одновременно (потому что это будет UB гонки данных). Вы должны сказать ему, что он не может оптимизировать доступ. Вот тут и приходит на помощь Шефф. Я не буду его повторять.
Поскольку оптимизатору не сообщается, что finished
переменная потенциально может измениться во время выполнения функции, она видит, что finished
не изменяется самой функцией и предполагает, что она постоянная.
Оптимизированный код предоставляет два пути кода, которые появятся в результате ввода функции с постоянным значением типа bool; либо он запускает цикл бесконечно, либо цикл никогда не запускается.
в -O0
компилятор (как и ожидалось) не оптимизирует тело цикла и сравнение:
func():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], 0
.L148:
movzx eax, BYTE PTR finished[rip]
test al, al
jne .L147
add QWORD PTR [rbp-8], 1
jmp .L148
.L147:
mov rax, QWORD PTR [rbp-8]
pop rbp
ret
поэтому функция, когда неоптимизированная, действительно работает, отсутствие атомарности здесь обычно не проблема, потому что код и тип данных просты. Вероятно, худшее, с чем мы можем здесь столкнуться, - это значениеi
это на единицу не так, как должно быть.
Более сложная система со структурами данных с гораздо большей вероятностью приведет к повреждению данных или неправильному выполнению.
Для полноты кривой обучения; вам следует избегать использования глобальных переменных. Вы хорошо поработали, сделав его статическим, поэтому он будет локальным для единицы перевода.
Вот пример:
class ST {
public:
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
void setFinished(bool val)
{
finished = val;
}
private:
std::atomic<bool> finished = false;
};
int main()
{
ST st;
auto result=std::async(std::launch::async, &ST::func, std::ref(st));
std::this_thread::sleep_for(std::chrono::seconds(1));
st.setFinished(true);
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Жить на wandbox