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章的內容,版權相關的問題請聯系作者或者相應的出版社。
浙公網安備 33010602011771號