Я использую Cygwin GCC и запускаю этот код:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
unsigned u = 0;
void foo()
{
u++;
}
int main()
{
vector<thread> threads;
for(int i = 0; i < 1000; i++) {
threads.push_back (thread (foo));
}
for (auto& t : threads) t.join();
cout << u << endl;
return 0;
}
Собран с линии: g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o
.
Он печатает 1000, что правильно. Однако я ожидал меньшего числа из-за того, что потоки перезаписывают ранее увеличенное значение. Почему этот код не страдает от взаимного доступа?
Моя тестовая машина имеет 4 ядра, и я не накладываю ограничений на программу, о которой я знаю.
Проблема сохраняется при замене содержимого общего доступа foo
чем-то более сложным, например
if (u % 3 == 0) {
u += 4;
} else {
u -= 1;
}
c++
race-condition
мафу
источник
источник
u
в память. ЦП на самом деле будет делать удивительные вещи, например, замечать, что строка памяти дляu
не находится в кеше ЦП, и перезапускает операцию увеличения. Вот почему переход от x86 к другим архитектурам может открыть глаза!while true; do res=$(./a.out); if [[ $res != 1000 ]]; then echo $res; break; fi; done;
вывода 999 или 998 в моей системе.Ответы:
foo()
настолько короток, что каждый поток, вероятно, заканчивается еще до того, как будет создан следующий. Если вы добавите сон на случайное времяfoo()
доu++
, вы можете начать видеть то, что ожидаете.источник
Важно понимать, что состояние гонки не гарантирует, что код будет работать некорректно, просто он может делать что угодно, так как это неопределенное поведение. В том числе работает как положено.
В частности, на машинах X86 и AMD64 условия гонки в некоторых случаях редко вызывают проблемы, поскольку многие инструкции являются атомарными, а гарантии согласованности очень высоки. Эти гарантии несколько снижаются в многопроцессорных системах, где префикс блокировки необходим для того, чтобы многие инструкции были атомарными.
Если на вашем компьютере инкремент является атомарной операцией, он, скорее всего, будет работать правильно, даже если в соответствии со стандартом языка это Undefined Behavior.
В частности, я ожидаю, что в этом случае код может быть скомпилирован для атомарной выборки и добавления инструкцию (ADD или XADD в сборке X86), которая действительно является атомарной в однопроцессорных системах, однако в многопроцессорных системах не гарантируется, что она будет атомарной, а блокировка потребуется сделать это так. Если вы работаете в многопроцессорной системе, появится окно, в котором потоки могут вмешиваться и давать неверные результаты.
В частности, я скомпилировал ваш код в сборку с помощью https://godbolt.org/ и
foo()
компилировал в:Это означает, что он выполняет исключительно инструкцию добавления, которая для одного процессора будет атомарной (хотя, как упоминалось выше, не так для многопроцессорной системы).
источник
inc [u]
не атомарно.LOCK
Префикс требуется , чтобы сделать инструкции действительно атомарной. OP просто везет. Напомним, что даже если вы говорите ЦП «добавить 1 к слову по этому адресу», ЦП все равно должен извлекать, увеличивать, сохранять это значение, а другой ЦП может делать то же самое одновременно, в результате чего результат будет неверным.Я думаю, дело не в том, если вы заснете до или после
u++
. Скорее, операцияu++
преобразуется в код, который - по сравнению с накладными расходами на порождение потоков, вызывающихfoo
- выполняется очень быстро, так что вероятность перехвата маловероятна. Однако если «продлить» операциюu++
, то состояние гонки станет гораздо более вероятным:результат:
694
Кстати: я тоже пробовал
и это давало мне большинство раз
1997
, но иногда1995
.источник
else u -= 1
вообще можно было казнить? Даже в параллельной среде значение никогда не должно не подходить%2
, не так ли?else u -= 1
выполняется один раз, когда в первый раз вызывается foo (), когда u == 0. Остальные 999 раз u является нечетным иu += 2
выполняется, что приводит к u = -1 + 999 * 2 = 1997; т.е. правильный вывод. Состояние гонки иногда приводит к перезаписи одного из + = 2 параллельным потоком, и вы получаете 1995.Он действительно страдает от состояния гонки. Положите
usleep(1000);
перед темu++;
вfoo
и я вижу разный вывод (<1000) каждый раз.источник
Вероятный ответ , почему состояние гонки не проявляется для вас, хотя это действительно существует, является то , что
foo()
так быстро, по сравнению со временем, которое требуется , чтобы начать нить, что каждая нить заканчивается до следующего может даже начать. Но...Даже с вашей исходной версией результат зависит от системы: я пробовал по-вашему на (четырехъядерном) Macbook, и за десять запусков я получил 1000 трижды, 999 шесть раз и 998 один раз. Так что гонка несколько редка, но явно присутствует.
Вы скомпилировали с
'-g'
, что позволяет избавиться от ошибок. Я перекомпилировал ваш код, все еще без изменений, но без символа'-g'
, и гонка стала гораздо более явной: я получил 1000 один раз, 999 три раза, 998 дважды, 997 дважды, 996 один раз и 992 один раз.Re. предложение добавить сон - это помогает, но (а) фиксированное время сна оставляет потоки по-прежнему искаженными по времени начала (в зависимости от разрешения таймера), и (б) случайный сон распределяет их, когда мы хотим, чтобы притянуть их ближе друг к другу. Вместо этого я бы закодировал их так, чтобы они ожидали сигнала запуска, чтобы я мог создать их все, прежде чем позволить им приступить к работе. С этой версией (с или без
'-g'
) я получаю результаты повсюду, начиная с 974 и не выше 998:источник
-g
Флаг не каким - либо образом «делают ошибки исчезают.»-g
Флаг на обоих GNU и лязг компиляторов просто добавляет символы отладки в скомпилированный двоичный файл. Это позволяет вам запускать диагностические инструменты, такие как GDB и Memcheck, в ваших программах с некоторыми удобочитаемыми выводами. Например, когда Memcheck запускается над программой с утечкой памяти, он не сообщит вам номер строки, если программа не была построена с использованием-g
флага.-O2
вместо из-g
». Но при этом, если вы никогда не испытывали радости от поиска ошибки, которая проявлялась бы только при компиляции без нее-g
, считайте, что вам повезло. Это может случиться с некоторыми из самых неприятных и тонких ошибок сглаживания. Я уже видел его, хотя и не в последнее время , и я мог поверить , что может быть , это было причудой старого проприетарного компилятора, поэтому я верю, условно, о современных версиях GNU и Clang.-g
не мешает вам использовать оптимизацию. например,gcc -O3 -g
делает тот же asmgcc -O3
, но с метаданными отладки. Однако gdb скажет "оптимизировано", если вы попытаетесь распечатать некоторые переменные.-g
может изменить относительное расположение некоторых вещей в памяти, если что-то из добавляемых им вещей является частью.text
раздела. Это определенно занимает место в объектном файле, но я думаю, что после связывания все это оказывается на одном конце текстового сегмента (а не в разделе) или вообще не является частью сегмента. Возможно, это может повлиять на то, где что-то отображается для динамических библиотек.