Ответ 1
foo()
настолько короток, что каждый поток, вероятно, заканчивается до того, как следующий будет порожден. Если вы добавляете спать в случайное время в foo()
до u++
, вы можете начать видеть, что вы ожидаете.
Я использую 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;
}
foo()
настолько короток, что каждый поток, вероятно, заканчивается до того, как следующий будет порожден. Если вы добавляете спать в случайное время в foo()
до u++
, вы можете начать видеть, что вы ожидаете.
Важно понимать, что условие гонки не гарантирует, что код будет работать некорректно, просто чтобы он мог что-либо сделать, поскольку это поведение undefined. Включая работу как ожидалось.
В частности, в условиях гонки X86 и AMD64 в некоторых случаях редко возникают проблемы, так как многие из инструкций являются атомарными, а гарантии согласованности очень велики. Эти гарантии несколько снижаются в многопроцессорных системах, где префикс блокировки необходим для того, чтобы многие инструкции были атомарными.
Если на вашем компьютере приращение является атомарным, это, скорее всего, будет работать корректно, хотя в соответствии со стандартом языка это undefined Поведение.
В частности, я ожидаю, что в этом случае код может быть скомпилирован в атомную команду Fetch and Add (ADD или XADD в сборке X86), которая действительно является атомой в одиночном но на многопроцессорных системах это не гарантируется атомарным, и для этого потребуется блокировка. Если вы работаете в многопроцессорной системе, появится окно, в котором потоки могут вмешиваться и создавать неверные результаты.
В частности, я скомпилировал ваш код для сборки с помощью https://godbolt.org/ и foo()
для компиляции:
foo():
add DWORD PTR u[rip], 1
ret
Это означает, что он выполняет только команду добавления, которая для одного процессора будет атомарной (хотя, как упоминалось выше, не так для многопроцессорной системы).
Я думаю, что это не так, если вы спали до или после u++
. Скорее всего, операция u++
преобразуется в код, который - по сравнению с накладными потоками нереста, которые вызывают foo
- очень быстро выполняется так, что вряд ли его перехватят. Однако, если вы "продлеваете" операцию u++
, то условие гонки станет намного более вероятным:
void foo()
{
unsigned i = u;
for (int s=0;s<10000;s++);
u = i+1;
}
результат: 694
Кстати: я также пробовал
if (u % 2) {
u += 2;
} else {
u -= 1;
}
и он дал мне больше всего времени 1997
, но иногда 1995
.
Он страдает от состояния гонки. Поместите usleep(1000);
до u++;
в foo
, и каждый раз я вижу разные выходные данные (< 1000).
Вероятный ответ на вопрос о том, почему состояние гонки не проявилось для вас, хотя оно существует, заключается в том, что foo()
работает так быстро, по сравнению с тем временем, которое требуется для начала потока, чтобы каждый поток заканчивался до следующего может даже начаться. Но...
Даже с вашей исходной версией результат зависит от системы: я пробовал его по своему (на четырехъядерном) Macbook, и за десять прогонов я получил 1000 три раза, 999 шесть раз и 998 раз, Таким образом, гонка несколько редка, но отчетливо присутствует.
Вы скомпилированы с помощью '-g'
, у которого есть способ устранения ошибок. Я перекомпилировал ваш код, все еще без изменений, но без '-g'
, и гонка стала намного более выраженной: я получил 1000 раз, 999 три раза, 998 дважды, 997 дважды, 996 один раз и 992 раз.
Re. предложение о добавлении сна - это помогает, но (а) фиксированное время сна оставляет потоки, все еще искаженные временем начала (при условии разрешения по таймеру), и (б) случайный сон распространяет их, когда мы хотим, чтобы сблизить их. Вместо этого я бы закодировал их, чтобы дождаться сигнала начала, поэтому я могу создать их все, прежде чем позволить им работать. С этой версией (с или без '-g'
) я получаю результаты по всему месту, как 974, и не выше 998:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
unsigned u = 0;
bool start = false;
void foo()
{
while (!start) {
std::this_thread::yield();
}
u++;
}
int main()
{
vector<thread> threads;
for(int i = 0; i < 1000; i++) {
threads.push_back (thread (foo));
}
start = true;
for (auto& t : threads) t.join();
cout << u << endl;
return 0;
}