当前位置: C语言 -- 基础 -- 多线程执行和数据竞争

多线程执行和数据竞争(二)


1、同步操作

如果存在数据竞争,两个表达式的评估会发生冲突。为避免数据竞争(data races),可以通过同步操作(synchronization operation)来获得确定性行为。同步操作包括原子操作(atomic operations)、互斥操作(operations on mutex)等。

下面的例子比较了非同步操作、原子操作、互斥操作的异同。这些操作都是通过两个线程对初始值为0的数分别执行1000次加11000次减1操作。

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
/*非同步操作的范例*/

#ifdef __STDC_NO_THREADS__
#error "Implementation does not support multi-threads."
#endif

#include <stdio.h>
#include <stdlib.h>
#include <threads.h>

int number = 0;

/*新线程中执行的函数。*/
int func1(void *arg)
{
    for(int i=0; i<1000; i++)
    {
        number += 1;
    }
    
    thrd_exit(0);
}

int func2(void *arg)
{
    for(int i=0; i<1000; i++)
    {
        number -= 1;
    }
    
    thrd_exit(0);
}

int main(void)
{
    thrd_t threadId1, threadId2;

    /*创建新线程。*/
    if(thrd_create(&threadId1, func1, NULL) != thrd_success)
        exit(EXIT_FAILURE);

    if(thrd_create(&threadId2, func2, NULL) != thrd_success)
        exit(EXIT_FAILURE);

    /*连接新线程。*/
    thrd_join(threadId1, NULL);
    thrd_join(threadId2, NULL);

    printf("%d\n", number);     
    
    return 0;
}

输出结果可能是0,也可能是其它值。

C语言是高级语言,C程序执行前会编译成机器指令。语句number += 1;将编译成3条机器指令,对应以下3个步骤(以number初始值0为例。):

步骤 内容 number值
s1   将变量number值读取到寄存器。 0
s2   对寄存器执行加1操作。 0
s3   将加1后的值写入变量number。 1

语句number -= 1;将编译成3条机器指令,对应以下3个步骤(以number初始值0为例。):

步骤 内容 number值
s1   将变量number值读取到寄存器。 0
s2   对寄存器执行减1操作。 0
s3   将减1后的值写入变量number。 -1

多线程程序的执行通常可以看作是所有线程的交错。在交错执行的情况下,以单次循环为例,上述步骤可能按以下顺序执行:

执行加1操作的线程 执行减1操作的线程 number值
s1   0
s2 s1 0
s3 s2 1
  s3 -1

由于执行减1操作的线程不是读取执行加1操作的线程加1操作后的更新值,导致结果是-1,而不是0


1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
/*互斥操作的范例*/

#ifdef __STDC_NO_THREADS__
#error "Implementation does not support multi-threads."
#endif

#include <stdio.h>
#include <stdlib.h>
#include <threads.h>

int number = 0;
mtx_t mutex;

/*新线程中执行的函数。*/
int func1(void *arg)
{
    for(int i=0; i<1000; i++)
    {
        mtx_lock(&mutex);
        number += 1;
        mtx_unlock(&mutex);
    }
    
    thrd_exit(0);
}

int func2(void *arg)
{
    for(int i=0; i<1000; i++)
    {
        mtx_lock(&mutex);
        number -= 1;
        mtx_unlock(&mutex);
    }
    
    thrd_exit(0);
}

int main(void)
{
    thrd_t threadId1, threadId2;

    /*创建互斥。*/
    if(mtx_init(&mutex, mtx_plain) != thrd_success)
    {
        perror("mtx_init error");
        exit(EXIT_FAILURE);
    }

    /*创建新线程。*/
    if(thrd_create(&threadId1, func1, NULL) != thrd_success)
        exit(EXIT_FAILURE);

    if(thrd_create(&threadId2, func2, NULL) != thrd_success)
        exit(EXIT_FAILURE);

    /*连接新线程。*/
    thrd_join(threadId1, NULL);
    thrd_join(threadId2, NULL);

    /*销毁互斥。*/
    mtx_destroy(&mutex);

    printf("%d\n", number);     
    
    return 0;
}

输出结果总是0

对于特定互斥对象的所有操作都是以单一总序发生的。每次获取互斥都会读取上次互斥释放时写入的值。一个线程修改number值、解锁互斥后,修改的number值对另一个加锁互斥的线程是可见的。


1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
/*原子操作的范例*/

#ifdef __STDC_NO_ATOMICS__
#error "Implementation does not support atomic types."
#endif

#ifdef __STDC_NO_THREADS__
#error "Implementation does not support multi-threads."
#endif

#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <threads.h>

atomic_int aNumber = 0;

/*新线程中执行的函数。*/
int func1(void *arg)
{
    for(int i=0; i<1000; i++)
    {
        atomic_fetch_add(&aNumber, 1);
    }
    
    thrd_exit(0);
}

int func2(void *arg)
{
    for(int i=0; i<1000; i++)
    {
        atomic_fetch_sub(&aNumber, 1);
    }
    
    thrd_exit(0);
}

int main(void)
{
    thrd_t threadId1, threadId2;

    /*创建新线程。*/
    if(thrd_create(&threadId1, func1, NULL) != thrd_success)
        exit(EXIT_FAILURE);

    if(thrd_create(&threadId2, func2, NULL) != thrd_success)
        exit(EXIT_FAILURE);

    /*连接新线程。*/
    thrd_join(threadId1, NULL);
    thrd_join(threadId2, NULL);

    printf("%d\n", atomic_load(&aNumber));
    
    return 0;
}

输出结果总是0

对于特定原子对象,其所有修改都是按照某种特定总序进行的。如果操作A和操作B都是修改原子对象M,并且A发生在B之前,则A应在M的修改顺序中先于B。这也表明修改顺序遵守“之前发生”(happens before)规则。


通过上述三个例子可以发现:使用同步操作可以得到确定结果。