Порядок памяти C для последовательного хранения в атомной переменной из нескольких потоков

#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 переменной.