拾贝杂苑

Linux/Unix 线程同步技术之互斥量(1)

众所周知,互斥量(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. 线程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.\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章的内容,版权相关的问题请联系作者或者相应的出版社。

 

posted on 2014-03-23 11:55  拾贝杂苑  阅读(554)  评论(0编辑  收藏  举报

导航