多线程执行和数据竞争(二)
1、同步操作
如果存在数据竞争,两个表达式的评估会发生冲突。为避免数据竞争(data races),可以通过同步操作(synchronization operation)来获得确定性行为。同步操作包括原子操作(atomic operations)、互斥操作(operations on mutex)等。
下面的例子比较了非同步操作、原子操作、互斥操作的异同。这些操作都是通过两个线程对初始值为0的数分别执行1000次加1、1000次减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)规则。
通过上述三个例子可以发现:使用同步操作可以得到确定结果。