你正在以 C 思维方式思考,但如果你想考虑竞争条件,你必须在较低的层面上思考。
在调试器中,您通常在单行代码上设置断点,并且可以通过单步执行程序来观察每行代码的执行情况。但这不是机器的工作方式,机器可能会为每一行代码执行多条指令,并且线程可以在任何地方中断。
让我们看看这一行。
printf(" %d", c);
在机器代码中,它看起来像这样:
load pointer to " %d" string constant
load value of c global
# <- thread might get interrupted here
call printf
因此,这种行为并不意外。您必须加载的值c
在你打电话之前printf
,所以如果线程被中断的话always有机会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
do?
问题中的代码可以转换为汇编代码,如下所示:
load the current value of c
add 1 to it
store it in c
call printf
不需要重新加载c
在它递增之后,因为 C 编译器可以假设除了当前线程之外没有其他人(没有其他线程或设备)更改内存。
如果你使用volatile
,编译器将严格保留每个加载和存储操作,并且程序集将如下所示:
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
,并且对于编写多线程代码几乎没有用处。对于编写信号处理程序、内存映射IO(设备驱动/嵌入式编程)很有用,对于正确使用setjmp
/longjmp
.
脚注:
编译器无法缓存以下值c
打电话给printf
,因为据编译器所知,printf
可以换c
(c
毕竟是一个全局变量)。有一天,编译器可能会变得更加复杂,它可能知道printf
没有改变c
,因此程序可能会崩溃得更严重。