pthread竞争条件,可疑行为

Ann*_*lai 1 c linux posix

我编写了以下代码来演示同一进程的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;

}
Run Code Online (Sandbox Code Playgroud)

`

我运行了这个程序,并期望在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)
Run Code Online (Sandbox Code Playgroud)

问题是,当输出2中出现竞争条件时,线程1打印1并被交换出处理器并且线程2进入.它开始工作,并且在线程2打印11之后,它被换出,线程1进入.它必须打印12,而是打印2(实际上应该丢失2).我无法弄清楚如何.请帮我理解这里发生的事情.

Die*_*Epp 10

你在思考C语言,但如果你想考虑竞争条件,你必须考虑较低的水平.

在调试器中,通常在单行代码上设置断点,并且可以通过单步执行程序来查看正在执行的每行代码.但这不是机器的工作原理,机器可以为每行代码执行几条指令,线程可以在任何地方中断.

我们来看看这一行.

printf(" %d", c);
Run Code Online (Sandbox Code Playgroud)

在机器代码中,它看起来像这样:

load pointer to " %d" string constant
load value of c global
# <- thread might get interrupted here
call printf
Run Code Online (Sandbox Code Playgroud)

所以这种行为并不出乎意料.您必须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;
}
Run Code Online (Sandbox Code Playgroud)

怎么volatile办?

问题中的代码可以转换为汇编代码,如下所示:

load the current value of c
add 1 to it
store it in c
call printf
Run Code Online (Sandbox Code Playgroud)

它不需要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
Run Code Online (Sandbox Code Playgroud)

这没有用.事实上,volatile几乎从来没有帮助.大多数C程序员都不理解volatile,而且编写多线程代码几乎没用.它对于编写信号处理程序,内存映射IO(设备驱动程序/嵌入式编程)非常有用,并且对于正确使用setjmp/ 非常有用longjmp.

脚注:

编译器无法缓存c调用的值printf,因为只要编译器知道,printf就可以更改c(c毕竟是一个全局变量).有一天,编译器可能会变得更复杂,并且可能知道printf它不会改变c,因此程序可能会更加严重.