众所周知,互斥量(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. "); 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. "); 45 exit(EXIT_FAILURE); 46 } 47 48 s = pthread_join(t1, 0); 49 if (s != 0) 50 { 51 perror("error pthread_join. "); 52 exit(EXIT_FAILURE); 53 } 54 55 s = pthread_join(t2, 0); 56 if (s != 0) 57 { 58 perror("error pthread_join. "); 59 exit(EXIT_FAILURE); 60 } 61 62 printf("Loops [%d] times by 2 threads without critical section. ", n_loops); 63 printf("Var g_n is [%d]. ", 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. 线程1将g_n的值赋给局部变量loc。假设g_n的当前值为1000。
2. 线程1的时间片用尽,线程2开始执行。
3. 线程2执行多次循环:将g_n的值改为其他的值,例如3000,线程2的时间片用尽。
4. 线程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. "); 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. "); 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. "); 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. "); 59 exit(EXIT_FAILURE); 60 } 61 62 s = pthread_join(t1, 0); 63 if (s != 0) 64 { 65 perror("error pthread_join. "); 66 exit(EXIT_FAILURE); 67 } 68 69 s = pthread_join(t2, 0); 70 if (s != 0) 71 { 72 perror("error pthread_join. "); 73 exit(EXIT_FAILURE); 74 } 75 76 printf("Var g_n is [%d]. ", 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] ", 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. "); 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. "); 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. "); 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. "); 69 exit(EXIT_FAILURE); 70 } 71 72 s = pthread_mutexattr_destroy(&mtx_attr); 73 if (s != 0) 74 { 75 perror("error pthread_mutexattr_destroy. "); 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. "); 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. "); 94 exit(EXIT_FAILURE); 95 } 96 97 s = pthread_join(t1, 0); 98 if (s != 0) 99 { 100 perror("error pthread_join. "); 101 exit(EXIT_FAILURE); 102 } 103 104 s = pthread_join(t2, 0); 105 if (s != 0) 106 { 107 perror("error pthread_join. "); 108 exit(EXIT_FAILURE); 109 } 110 111 s = pthread_mutex_destroy(&mtx); 112 if (s != 0) 113 { 114 perror("error pthread_mutex_destroy. "); 115 exit(EXIT_FAILURE); 116 } 117 118 printf("Var g_n is [%d]. ", 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章的内容,版权相关的问题请联系作者或者相应的出版社。