int loops = *((int *) arg);
int loc, j;
for (j = 0; j < loops; j++) {
loc = glob;
loc++;
glob = loc;
}
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops, s;
loops = (argc > 1)? getInt(argv[1], GN_GT_0, "num-loops"): 10000000;
s = pthread_create(&t1, NULL, threadFunc, &loops);
if (s!= 0)
errExitEN(s, "pthread_create");
s = pthread_create(&t2, NULL, threadFunc, &loops);
if (s!= 0)
errExitEN(s, "pthread_create");
s = pthread_join(t1, NULL);
if (s!= 0)
errExitEN(s, "pthread_join");
s = pthread_join(t2, NULL);
if (s!= 0)
errExitEN(s, "pthread_join");
printf("glob = %d\n", glob);
exit(EXIT_SUCCESS);
}
threads/thread_incr.c
Если запустить программу из листинга 30.1, указав, что каждый поток должен инкрементировать переменную 1000 раз, все должно выглядеть нормально:
$ ./thread_incr 1000
glob = 2000
Что же произошло? Первый поток, скорее всего, успел завершить всю свою работу до того, как стартовал второй поток. Если существенно увеличить объем работы, мы увидим совсем другой результат:
$ ./thread_incr 10000000
glob = 16517656
В конце этой последовательности переменная glob должна быть равна 20 миллионам. Проблема здесь кроется в способе выполнения потоков (см. также рис. 30.1).
1. Поток 1 копирует текущее значение glob в свою локальную переменную loc. Будем считать, что это значение равно 2000.
2. Временной отрезок, отведенный планировщиком для потока 1, исчерпывается, после чего начинает выполнение поток 2.
3. Поток 2 выполняет множество итераций, в которых он копирует текущее значение glob в свою локальную переменную loc, инкрементирует эту переменную и передает результат обратно в glob. В первой из этих итераций значение, полученное из glob, будет равно 2000. Представим, что на момент исчерпания времени, отведенного потоку 2, значение glob увеличилось до 3000.
4. Поток 1 получает еще один временной отрезок и продолжает выполнение с того места, на котором он был прерван. Имея в переменной loc ранее скопированное значение glob (шаг 1), равное 2000, он теперь инкрементирует loc и передает результат (2001) обратно в glob. На этом этапе результат инкрементации значения потоком 2 теряется.
Рис. 30.1.
Если несколько раз запустить программу из листинга 30.1 с одним и тем же аргументом командной строки, можно увидеть, что выводимое значение glob существенно варьируется:
$ ./thread_incr 10000000
glob = 10880429
$ ./thread_incr 10000000
glob = 13493953
Такое непредсказуемое поведение является результатом причудливой работы планировщика ядра. В сложных программах подобные ошибки возникают редко и их бывает сложно воспроизвести — а значит, сложно найти.
На первый взгляд может показаться, что проблема легко решается путем замены трех инструкций функции threadFunc() внутри цикла for одним-единственным выражением:
glob++; /* или ++glob; */
Однако во многих программных архитектурах (таких как RISC) компилятору все равно бы пришлось преобразовать это выражение в команды машинного кода, которые являются эквивалентом трех инструкций из цикла в функции threadFunc(). Иными словами, несмотря на внешнюю простоту, даже простая операция инкремента может оказаться неатомарной и демонстрировать поведение, описанное выше.
Чтобы избежать проблем, которые возникают при попытке обновления разделяемой переменной из разных потоков, следует использовать мьютекс (от
Мьютекс имеет два состояния:
В целом для каждого разделяемого ресурса (который может состоять из нескольких связанных между собой переменных) устанавливается отдельный мьютекс. Для доступа к такому ресурсу каждый поток использует такую последовательность действий:
• закрыть мьютекс для разделяемого ресурса;
• получить доступ к разделяемому ресурсу;
• открыть мьютекс.