#c #multithreading #c 11 #atomic #memory-barriers
#c #многопоточность #c 11 #атомарный #барьеры памяти
Вопрос:
Запуская следующий код сотни раз, я ожидал, что напечатанное значение будет всегда 3
, но, похоже, оно равно 3 только примерно в 75% случаев. Вероятно, это означает, что у меня неправильное понимание назначения различных порядков памяти в C или точки атомарных операций. Мой вопрос: есть ли способ гарантировать, что результат следующей программы предсказуем?
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
int main () {
std::atomic<int> cnt{0};
auto f = [amp;](int n) {cnt.store(n, std::memory_order_seq_cst);};
std::vector<std::thread> v;
for (int n = 1; n < 4; n)
v.emplace_back(f, n);
for (autoamp; t : v)
t.join();
std::cout << cnt.load() << std::endl;
return 0;
}
Например, вот статистика вывода после 100 запусков:
$ clang -std=c 20 -Wall foo.cpp -pthread amp;amp; for i in {1..100}; do ./a.out; done | sort | uniq -c
2 1
21 2
77 3
Комментарии:
1. Почему вы ожидаете, что это всегда будет 3? Знание этого может помочь нам исправить ваше недоразумение.
2. Вы, кажется, считаете, что три потока должны выполняться один за другим в том порядке, в котором они были созданы. Если бы это было так, было бы мало смысла начинать с потоков; одновременный запуск — это своего рода причина, по которой они существуют.
3. Вся цель потоков — обеспечить независимое продвижение вперед с любой скоростью, которую система может оптимально обеспечить. Зачем использовать потоки, если это не то, что вы хотите или ожидаете? Если вы хотите выполнить три действия в точном порядке, почему бы одному потоку не выполнить все три действия?
4. Все отличные вопросы; Я думаю, я в основном ожидал, что использование atomics сделает результат предсказуемым. Возможно, это мое непонимание того, что означает «четко определенное поведение». Если бы результат ответа был разделен поровну тремя способами, я бы тоже это понял, но 75% 3, 20% 2 и пара 1s не казались четко определенными.
Ответ №1:
То, что вы наблюдаете, ортогонально порядкам памяти.
Планировщик не может гарантировать порядок выполнения потоков с одинаковым приоритетом. Даже если процессоры простаивают, а потоки назначены разным процессорам, промахи кэша и конфликт блокировок могут привести к остановке потоков относительно потоков на других процессорах. Или, если процессоры заняты выполнением других потоков с тем же или более высоким приоритетом, тогда вашим новым потокам придется ждать, пока запущенные потоки не исчерпают свои временные интервалы или блок в ядре, что бы ни произошло раньше, планировщику трудно предсказать. Только если в вашей системе один процессор, новые потоки будут выполняться в ожидаемом порядке относительно друг друга, потому что они будут формировать одну очередь на одном процессоре.
std::memory_order_relaxed
здесь достаточно, поскольку вам не требуется никакого упорядочения между хранилищем cnt
и хранилищами / загрузками в другие неатомные переменные. std::atomic
всегда является атомарным, std::memory_order
указывает, можно ли изменять порядок загрузки и сохранения в другие неатомные переменные относительно загрузки или сохранения std::atomic
переменной.