состояние гонки pthread, подозрительное поведение

Я написал следующий код, чтобы показать состояние гонки между двумя streamами одного и того же процесса.

`

#include  #include  #include  int c = 0; void *fnC() { int i; for(i=0;i<10;i++) { c++; printf(" %d", c); } } int main() { int rt1, rt2; pthread_t t1, t2; /* Create two threads */ if( (rt1=pthread_create( &t1, NULL, &fnC, NULL)) ) printf("Thread creation failed: %d\n", rt1); if( (rt2=pthread_create( &t2, NULL, &fnC, NULL)) ) printf("Thread creation failed: %d\n", rt2); /* Wait for both threads to finish */ pthread_join( t1, NULL); pthread_join( t2, NULL); printf ("\n"); return 0; } 

`

Я запустил эту программу и ожидал, что состояние гонки будет происходить между двумя streamами (но я понимаю, что вероятность для состояния гонки очень мала, так как основная функция streamа очень мала). Я побежал в этом 50000 раз. Следующим был вывод,

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - 49657 times (no race condition) 1 3 4 5 6 7 8 9 10 11 2 12 13 14 15 16 17 18 19 20 - 244 times (race condition occurs) 2 3 4 5 6 7 8 9 10 11 1 12 13 14 15 16 17 18 19 20 - 99 times (race condition occurs) 

Вопрос заключается в том, что, когда условие гонки встречается так же, как и на выходе 2, stream 1 печатает 1 и выгружается из процессора, и идет stream 2. Он начинает работать, и после того, как stream 2 печатает 11, он поменяется, вступает нить 1. Он должен печатать 12, но, скорее, он печатает 2 (фактически 2 должно отсутствовать). Я не могу понять, как это сделать. Пожалуйста, помогите мне понять, что здесь происходит.

Вы думаете в менталитете C, но если вы хотите думать о гоночных условиях, вы должны думать на более низком уровне.

В отладчике вы обычно устанавливаете точку останова на одной строке кода, и вы можете наблюдать за каждой строкой кода, выполняемой по вашей программе. Но машина работает не так, машина может выполнять несколько инструкций для каждой строки кода, а streamи могут быть прерваны в любом месте.

Давайте посмотрим на эту линию.

 printf(" %d", c); 

В машинный код это выглядит примерно так:

 load pointer to " %d" string constant load value of c global # <- thread might get interrupted here call printf 

Поэтому поведение не является неожиданным. Вы должны загрузить значение c прежде чем вы сможете вызвать printf , поэтому, если stream прерывается, всегда есть вероятность, что c устаревает к моменту, когда printf что-то делает. Если вы не сделаете что-то, чтобы остановить его.

Исправление состояния гонки:

 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int c = 0; void *func(void *param) { int i; for (i=0; i<10; i++) { pthread_mutex_lock(&mutex); c++; printf(" %d", c); pthread_mutex_unlock(&mutex); } return NULL; } 

Что делает volatile ?

Код в вопросе может перевести на ассемблерный код следующим образом:

 load the current value of c add 1 to it store it in c call printf 

Он не должен перезагружать c после того, как он увеличивает его, поскольку компилятор C допускает, что никто другой (ни один другой stream или устройство) не изменяет память, кроме текущего streamа.

Если вы используете volatile , компилятор будет строго соблюдать каждую операцию загрузки и хранения, а assembly будет выглядеть примерно так:

 load the current value of c add 1 to it store it in c # compiler is not allowed to cache c load the current value of c call printf 

Это не помогает. На самом деле, volatile почти никогда не помогают. Большинство программистов на C не понимают volatile , и это почти бесполезно для написания многопоточного кода. Это полезно для написания обработчиков сигналов, ввода-вывода с памятью (драйверов устройств / встроенного программирования), и это полезно для правильного использования setjmp / longjmp .

Сноска:

Компилятор не может кэшировать значение c по вызову printf , поскольку, насколько известно компилятору, printf может изменить c ( c - глобальная переменная, в конце концов). Когда-нибудь компилятор может стать более сложным, и он может знать, что printf не изменяет c , поэтому программа может сломаться еще сильнее.

Я бы предположил, что значение 2 кэшируется в регистре, поэтому stream 1 не видит правильное текущее значение c которое было последним установлено другим streamом. Попробуйте использовать ключевое слово volatile в объявлении c , что может иметь значение. Смотрите, почему в C требуется летучесть? для некоторых хороших обсуждений волатильности.

Я думаю, что вы полностью ошибаетесь. Вероятно, большую часть времени вы не участвуете в гонке на доступ к c а на вашем доступе к stdout . В современных ОС доступ к функциям stdio является мьютексированным. Если в streamе есть несколько streamов, похожих на ваш пример, существует высокая вероятность того, что они будут отменены. Это явление, которое вы наблюдаете.

Измерение или учет реальных условий гонки намного сложнее, чем вы себе представляете. Одним из способов было бы, например, отслеживать все значения, которые встречались в массиве или около того.

Как минимум, вам нужно будет префикс каждого вывода с чем-то вроде идентификатора streamа, чтобы знать, откуда этот вывод. Попробуйте что-нибудь вроде

 void *fnC(void * arg) { int id = *(int*)arg; for(int i=0;i<10;i++) { c++; printf(" %d:%d", id, c); } } 

и создайте свои streamи с указателем на аргумент int используя массив, такой как

 int ids[] = { 0, 1 }; 

вместо NULL .