众所周知,互斥量(mutex)是同步线程对共享资源访问的技术,用来防止下面这种情况:线程A试图访问某个共享资源时,线程B正在对其进行修改,从而造成资源状态不一致。与之相关的一个术语临界区(critical section)是指访问某一共享资源的代码片段,并且这段代码的执行为原子(atomic)操作,即同时访问同一共享资源的其他线程不应中断该片段的执行。
我们先来看看不使用临界区技术保护共享资源的例子,该例子使用2个线程来同时递增同一个全局变量。
代码示例1:不使用临界区技术访问共享资源
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4
5 static int g_n = 0;
6
7 static void *
8 thread_routine(void *arg)
9 {
10 int n_loops = (int)(arg);
11 int loc;
12 int j;
13
14 for (j = 0; j < n_loops; j++)
15 {
16 loc = g_n;
17 loc++;
18 g_n = loc;
19 }
20
21 return 0;
22 }
23
24 int
25 main(int argc, char *argv[])
26 {
27 int n_loops, s;
28 pthread_t t1, t2;
29 void *args[2];
30
31 n_loops = (argc > 1) ? atoi(argv[1]) : 10000000;
32
33 args[0] = (void *)n_loops;
34 s = pthread_create(&t1, 0, thread_routine, &args);
35 if (s != 0)
36 {
37 perror("error pthread_create.\n");
38 exit(EXIT_FAILURE);
39 }
40
41 s = pthread_create(&t2, 0, thread_routine, &args);
42 if (s != 0)
43 {
44 perror("error pthread_create.\n");
45 exit(EXIT_FAILURE);
46 }
47
48 s = pthread_join(t1, 0);
49 if (s != 0)
50 {
51 perror("error pthread_join.\n");
52 exit(EXIT_FAILURE);
53 }
54
55 s = pthread_join(t2, 0);
56 if (s != 0)
57 {
58 perror("error pthread_join.\n");
59 exit(EXIT_FAILURE);
60 }
61
62 printf("Loops [%d] times by 2 threads without critical section.\n", n_loops);
63 printf("Var g_n is [%d].\n", g_n);
64 exit(EXIT_SUCCESS);
65 }
运行以上代码生成的程序,若循环次数较少,比如每个线程都对全局变量g_n递增1000次,结果看起来很正常:
$ ./thdincr_nosync 1000
Loops [1000] times by 2 threads without critical section.
Var g_n is [2000].
如果加大每个线程的循环次数,结果将大不相同:
$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [18655665].
造成以上问题的原因在于下面的执行序列:
-
线程1将g_n的值赋给局部变量loc。假设g_n的当前值为1000。
-
线程1的时间片用尽,线程2开始执行。
-
线程2执行多次循环:将g_n的值改为其他的值,例如3000,线程2的时间片用尽。
-
线程1重新获得时间片,并从上次停止处恢复执行。线程1在上次运行时,已将g_n的值(1000)赋给loc,现在递增loc,再将loc的值1001赋给g_n。此时线程2之前递增操作的结果遭到覆盖。
如果使用上面同样的命令行参数运行该程序多次,g_n的值会出现很大波动:
$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [14085995].
$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [13590133].
$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [20000000].
$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [16550684].
这一行为结果的不确定性,原因在于内核CPU调度顺序的不可预测性。若在复杂的程序中发生这种不确定结果的行为,意味着此类错误将偶尔发作,难以复现,因此也很难发现。如果使用如下语句:
g_n++; /* 或者: ++g_n */
来替换thread_routine内for循环中的3条语句,似乎可以解决这一问题,不过在很多硬件架构上,编译器在将这条语句转换成机器码时,其效果仍等同于原先thread_routine内for循环中的3条语句。即换成一条语句并非意味着该操作就是原子操作。
为了避免上述同一行为的结果不确定性,必须使用某种技术来确保同一时刻只有一个线程可以访问共享资源,在Linux/Unix系统中,互斥量mutex(mutual exclusion的缩写)就是为这种情况设计的一种线程间同步技术,可以使用互斥量来保证对任意共享资源的原子访问。
互斥量有两种状态:已锁定和未锁定。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的互斥量再次加锁,将可能阻塞线程或者报错,具体取决于加锁时使用的方法。
静态分配的互斥量:
互斥量既可以像静态变量那样分配,也可以在运行时动态创建,例如,通过malloc在堆中分配,或者在栈上的自动变量,下面的语句展示了如何初始化静态分配的互斥量:
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
互斥量的加锁和解锁操作:
初始化之后,互斥量处于未锁定状态。函数pthread_mutex_lock()可以锁定某一互斥量,而函数pthread_mutex_unlock()可以将一个已经锁定的互斥量解锁。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* 两个函数在成功时返回值为0,失败时返回一个正值代表错误号。 */
代码示例2:使用静态分配的互斥量保护对全局变量的访问
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4
5 static int g_n = 0;
6 static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
7
8 static void *
9 thread_routine(void *arg)
10 {
11 int n_loops = *((int *)arg);
12 int loc;
13 int j;
14 int s;
15
16 for (j = 0; j < n_loops; j++)
17 {
18 s = pthread_mutex_lock(&mtx);
19 if (s != 0)
20 {
21 perror("error pthread_mutex_lock.\n");
22 exit(EXIT_FAILURE);
23 }
24
25 loc = g_n;
26 loc++;
27 g_n = loc;
28
29 s = pthread_mutex_unlock(&mtx);
30 if (s != 0)
31 {
32 perror("error pthread_mutex_unlock.\n");
33 exit(EXIT_FAILURE);
34 }
35 }
36
37 return 0;
38 }
39
40 int
41 main(int argc, char *argv[])
42 {
43 pthread_t t1, t2;
44 int n_loops, s;
45
46 n_loops = (argc > 1) ? atoi(argv[1]) : 10000000;
47
48 s = pthread_create(&t1, 0, thread_routine, &n_loops);
49 if (s != 0)
50 {
51 perror("error pthread_create.\n");
52 exit(EXIT_FAILURE);
53 }
54
55 s = pthread_create(&t2, 0, thread_routine, &n_loops);
56 if (s != 0)
57 {
58 perror("error pthread_create.\n");
59 exit(EXIT_FAILURE);
60 }
61
62 s = pthread_join(t1, 0);
63 if (s != 0)
64 {
65 perror("error pthread_join.\n");
66 exit(EXIT_FAILURE);
67 }
68
69 s = pthread_join(t2, 0);
70 if (s != 0)
71 {
72 perror("error pthread_join.\n");
73 exit(EXIT_FAILURE);
74 }
75
76 printf("Var g_n is [%d].\n", g_n);
77 exit(EXIT_SUCCESS);
78 }
运行此示例代码生成的程序,从结果中可以看出对g_n的递增操作总能保持正确:
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
代码示例3:使用动态分配的互斥量保护对全局变量的访问
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <errno.h>
5
6 static int g_n = 0;
7
8 static void *
9 thread_routine(void *arg)
10 {
11 void **args = (void **)arg;
12 int n_loops = (int)(args[0]);
13 int loc;
14 int j;
15 int s;
16 pthread_mutex_t *mtx = (pthread_mutex_t *)(args[1]);
17
18 for (j = 0; j < n_loops; j++)
19 {
20 s = pthread_mutex_lock(mtx);
21 if (s != 0)
22 {
23 printf("error pthread_mutex_lock. return:[%d] errno:[%d]\n", s, errno);
24 exit(EXIT_FAILURE);
25 }
26
27 loc = g_n;
28 loc++;
29 g_n = loc;
30
31 s = pthread_mutex_unlock(mtx);
32 if (s != 0)
33 {
34 perror("error pthread_mutex_unlock.\n");
35 exit(EXIT_FAILURE);
36 }
37 }
38
39 return 0;
40 }
41
42 int
43 main(int argc, char *argv[])
44 {
45 int n_loops, s;
46 pthread_t t1, t2;
47 pthread_mutex_t mtx;
48 pthread_mutexattr_t mtx_attr;
49 void *args[2];
50
51 s = pthread_mutexattr_init(&mtx_attr);
52 if (s != 0)
53 {
54 perror("error pthread_mutexattr_init.\n");
55 exit(EXIT_FAILURE);
56 }
57
58 s = pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_ERRORCHECK);
59 if (s != 0)
60 {
61 perror("error pthread_mutexattr_settype.\n");
62 exit(EXIT_FAILURE);
63 }
64
65 s = pthread_mutex_init(&mtx, &mtx_attr);
66 if (s != 0)
67 {
68 perror("error pthread_mutex_init.\n");
69 exit(EXIT_FAILURE);
70 }
71
72 s = pthread_mutexattr_destroy(&mtx_attr);
73 if (s != 0)
74 {
75 perror("error pthread_mutexattr_destroy.\n");
76 exit(EXIT_FAILURE);
77 }
78
79 n_loops = (argc > 1) ? atoi(argv[1]) : 10000000;
80
81 args[0] = (void *)n_loops;
82 args[1] = (void *)&mtx;
83 s = pthread_create(&t1, 0, thread_routine, &args);
84 if (s != 0)
85 {
86 perror("error pthread_create.\n");
87 exit(EXIT_FAILURE);
88 }
89
90 s = pthread_create(&t2, 0, thread_routine, &args);
91 if (s != 0)
92 {
93 perror("error pthread_create.\n");
94 exit(EXIT_FAILURE);
95 }
96
97 s = pthread_join(t1, 0);
98 if (s != 0)
99 {
100 perror("error pthread_join.\n");
101 exit(EXIT_FAILURE);
102 }
103
104 s = pthread_join(t2, 0);
105 if (s != 0)
106 {
107 perror("error pthread_join.\n");
108 exit(EXIT_FAILURE);
109 }
110
111 s = pthread_mutex_destroy(&mtx);
112 if (s != 0)
113 {
114 perror("error pthread_mutex_destroy.\n");
115 exit(EXIT_FAILURE);
116 }
117
118 printf("Var g_n is [%d].\n", g_n);
119 exit(EXIT_SUCCESS);
120 }
多次运行示例3代码生成的程序会看到与示例2代码的程序同样的结果。
本文展示了Linux/Unix线程间同步技术---互斥量的基本功能和基础使用方法,在后面的文章中将会讨论互斥量的其他内容,如锁定互斥量的另外2个API: pthread_mutex_trylock()和pthread_mutex_timedlock() ,互斥量的性能,互斥量的死锁等。欢迎大家参与讨论。
本文参考了Michael Kerrisk的著作《The Linux Programming Interface》(中文版名为:Linux/Unix系统编程手册)第30章的内容,版权相关的问题请联系作者或者相应的出版社。
原文链接: https://www.cnblogs.com/technic-emotion/p/3618742.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/124547
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!