我编写了以下代码来演示同一进程的2个线程之间的竞争条件。
`
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
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;
}
`
我运行了这个程序,并且预计在2个线程之间会发生竞争条件(但是,据我所知,竞争条件的可能性非常小,因为线程主函数非常小)。 我跑了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中发生竞争条件时,线程1打印1并从处理器换出并且线程2进入。它开始工作并且在线程2打印11之后,它被换出,线程1进入。它必须打印12,而是打印2(实际上应该丢失2)。我无法弄清楚如何。请帮助我理解这里发生的事情。
答案 0 :(得分:10)
你正在思考C语言,但如果你想考虑竞争条件,你必须考虑较低的水平。
在调试器中,您通常在一行代码上设置断点,并且可以通过单步执行程序来查看正在执行的每行代码。但这不是机器的工作方式,机器可以为每行代码执行几条指令,线程可以在任何地方中断。
让我们来看看这一行。
printf(" %d", c);
在机器代码中,它看起来像这样:
load pointer to " %d" string constant
load value of c global
# <- thread might get interrupted here
call printf
所以这种行为并不意外。您必须先加载c
的值才能调用printf
,因此如果线程被中断,总是可能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编译器假定没有其他人(没有其他线程或设备)更改除当前线程之外的内存。
如果使用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
({{毕竟,1}}是一个全局变量。有一天,编译器可能会变得更复杂,并且可能知道c
不会更改printf
,因此程序可能会更加严重。
答案 1 :(得分:1)
我猜测2的值被缓存在寄存器中,因此线程1没有看到由另一个线程最后设置的c
的正确当前值。尝试在volatile
声明中使用c
关键字,这可能会有所不同。有关volatile的一些讨论,请参阅Why is volatile needed in C?。
答案 2 :(得分:0)
我认为你完全走错了路。可能大多数情况下,您不是在访问c
,而是访问stdout
。在现代操作系统上,对stdio
函数的访问是互斥的。如果像你的例子那样在流上有几个线程,那么很有可能它们不按规定服务。这是你正在观察的现象。
衡量或计算真实的竞争条件要比你想象的要困难得多。例如,一种方法是跟踪数组中遇到的所有值。
至少你必须在每个输出前面添加类似线程ID的内容,以了解此输出的来源。尝试像
这样的东西void *fnC(void * arg)
{
int id = *(int*)arg;
for(int i=0;i<10;i++)
{
c++;
printf(" %d:%d", id, c);
}
}
使用数组(例如
)创建带有指向int
参数的指针的线程
int ids[] = { 0, 1 };
而不是NULL
。