Другие ответы указывают на неатомарный прирост и различные проблемы. В основном я хочу указать на некоторые интересные практические подробности о том, что именно мы видим при запуске этого кода в реальной системе. (x86-64 Arch Linux, gcc9.1 -O3, i7-6700k 4c8t Skylake).
Может быть полезно понять, почему определенные ошибки или варианты дизайна приводят к определенному поведению, для устранения неполадок / отладки.
Используйте int tmp = ++a;
для записи результата fetch_add в локальную переменную вместо его перезагрузки из общей переменной . (И, как говорит 1202ProgramAlarm, вы, возможно, захотите рассматривать весь прирост и печатать как атомарную транзакцию, если вы настаиваете на том, чтобы ваши подсчеты были напечатаны в порядке и были выполнены правильно). Или вы можете захотеть, чтобы каждый поток записывал значения, которые он видел в частной структуре данных, для печати позже, вместо того, чтобы также сериализовать потоки с printf
во время приращений. (На практике все попытки увеличить одну и ту же атомарную переменную будут сериализовать их в ожидании доступа к строке кэша; ++a
будет идти по порядку, чтобы вы могли определить из порядка изменения, какой поток шел в каком порядке.)
Забавный факт: a.store(1 + a.load(std:memory_order_relaxed), std::memory_order_release)
- это то, что вы можете сделать для переменной, которая была записана только одним потоком, но читалась несколькими потоками. Вам не нужен атомарный RMW, потому что никакой другой поток никогда не изменяет его. Вам просто нужен потокобезопасный способ публикации обновлений. (Или лучше, в цикле держать локальный счетчик и просто .store()
его без загрузки из общей переменной.)
Если вы использовали значение по умолчанию a = ...
для последовательно согласованного хранилища, вы могли бы такжесделали атомарный RMW на x86. Один хороший способ компиляции с атомарным xchg
или mov
+ mfence
настолько же дорог (или больше).
Что интересно, несмотря на огромные проблемы с вашим кодом, не было подсчета , потеряно или наступление (нет повторных подсчетов), просто переупорядочена печать. Так что на практике опасность не возникала из-за других происходящих эффектов.
Я попробовал это на своей собственной машине и потерял некоторые счета. Но после удаления спящего режима я просто переупорядочил. (я скопировал около 1000 строк вывода в файл, и sort -u
, чтобы унифицировать вывод, не изменил количество строк. хотя печатает вокруг; предположительно, один поток на некоторое время остановился.) В моем тестировании не проверялась возможность подсчета потерянных , пропущено не сохранение значения, сохраненного в a
, и его перезагрузка,Я не уверен, что есть правдоподобный способ, чтобы это произошло здесь без нескольких потоков, считывающих одинаковое количество, которое будет обнаружено.
Store + reload, даже хранилище seq-cst, которое должно очищатьсяБуфер хранилища, прежде чем он сможет перезагрузиться, очень быстр по сравнению с printf
, выполняющим системный вызов write()
. (Строка формата включает новую строку, и я не перенаправил вывод в файл, так что stdout буферизован строкойи не может просто добавить строку в буфер.)
(write()
системные вызовы одного и того же дескриптора файла сериализуются в POSIX: write(2)
является атомарным. Также, printf(3)
сам по себе является поточно-ориентированным в GNU / Linux, как того требует C ++ 17 и, вероятно, POSIX задолго до этого.)
Блокировка Stdio в printf
оказываетсядостаточно сериализации почти во всех случаях: поток, который только что разблокировал stdout и оставил printf, может сделать атомарный инкремент и затем попытаться снова взять блокировку stdout.
Все остальные потоки были заблокированы, пытаясь получить блокировку на stDOUT. Один (другой?) Поток может проснуться и захватить блокировку на stdout, но для его приращения, чтобы состязаться с другим потоком, он должен будет войти и выйти из printf и загрузить a
в первый раз, прежде чем этот другой поток выполнит свою a = ...
seq-cst store.
Это не значит, что это действительно безопасно
Только то, что тестирование этой конкретной версии программы (по крайней мере, на x86) не может легко выявить отсутствие безопасности. Прерывания или изменения в расписании, включая соревнование с другими вещами, работающими на той же машине,конечно, может заблокировать поток в самый неподходящий момент.
Мой рабочий стол имеет 8 логических ядер, поэтому для каждого потока было достаточно, чтобы получить его, не прибегая к расписанию. (Хотя обычно это обычно происходит при вводе-выводе или при ожидании блокировки).
При наличии sleep
весьма вероятно, что несколько потоков пробудятсяпочти в одно и то же время и на практике соревнуются друг с другом на реальном оборудовании x86. Я думаю, что это так долго, что степень детализации таймера становится фактором. Или что-то в этом роде.
Перенаправление вывода в файл
С открытым stdout
для файла, не являющегося TTY, он полностью буферизуется, а не буферизуется строкой , и не всегда выполняет системный вызов, удерживая блокировку стандартного вывода.
(Я получил 17-мегабайтный файл в / tmp от удара по control-C через доли секунды после выполнения ./a.out > output
.)
Это делает процесс достаточно быстрым, чтобы потоки фактически состязались друг с другом на практике, показывая ожидаемые ошибки повторяющихся значений. (Поток читает a
, но теряет владение строкой кэша до того, как сохраняет (tmp)+1
, в результате чего два или более потоков выполняют одинаковое приращение. И / или несколько потоков читают одно и то же значение при перезагрузке a
после очистки ихбуфер хранения.)
1228589
уникальных строк (sort -u | wc
), но общий вывод составляет
1291035
всего строк. Таким образом, ~ 5% выходных строк были дубликатами.
Я не проверял, дублировалось ли обычно одно значение несколько раз или обычно это был только один дубликат. Или как далеко назад значение когда-либо прыгнуло. Если поток оказался остановленным обработчиком прерываний после загрузки, но перед сохранением val+1
, это может быть довольно далеко. Или, если он действительно спал или блокировался по какой-либо причине, он мог бы перематываться бесконечно далеко.