前言:學進程時,學習的重點應該進程間通信,而學習線程時,重點就應該是線程同步了。想過為什么?fork創建子進程之后,子進程有自己的獨立地址空間和PCB,想和父進程或其它進程通信,就需要各種通信方式,例如無名管道(管道,我習慣這么叫無名管道)、有名管道(命名管道)、信號、消息隊列、信號量、共享內存等;而pthread_create創建子線程之后,子線程沒有獨立的地址空間,大部分數據都是共享的,如果同時訪問數據,就是造成混亂,所以要控制,就是線程同步了。
一、同步概念
為什么要特意說一下同步概念呢?因為它跟其他領域的“同步”有些差異。
所謂同步,即同時起步,協調一致。不同的對象,對“同步”的理解方式略有不同。如,設備同步,是指在兩個設備之間規定一個共同的時間參考;數據庫同步,是指讓兩個或多個數據庫內容保持一致,或者按需要部分保持一致;文件同步,是指讓兩個或多個文件夾里的文件保持一致。等等
而,編程中、通信中所說的同步與生活中大家印象中的同步概念略有差異。“同”字應是指協同、協助、互相配合。主旨在協同步調,按預定的先后次序運行。
二、線程同步方式
這篇博客主要介紹四種方式,如下:
方式 | 通用標識 |
互斥鎖(互斥量) | pthread_mutex_ |
讀寫鎖 | pthread_rwlock_ |
條件變量 | pthread_cond_ |
信號量 | sem_ |
表中的“通用標識”,指的是那種同步方式的函數、類型都那么開頭的,方便記憶;還有其他方式,自旋鎖、屏蔽,感覺不常用,有興趣可以閱讀APUE。
三、互斥鎖(互斥量)
1、介紹
先來畫個圖,來簡單說明一下: PS:依舊是全博客園最丑圖,不接受反駁!
Linux中提供一把互斥鎖mutex(也稱之為互斥量)。
每個線程在對資源操作前都嘗試先加鎖,成功加鎖才能操作,操作結束解鎖。
資源還是共享的,線程間也還是競爭的,
但通過“鎖”就將資源的訪問變成互斥操作,而后與時間有關的錯誤也不會再產生了。
2、主要函數
pthread_mutex_init函數 //初始化mutex,默認為1
pthread_mutex_destroy函數 //銷毀鎖
pthread_mutex_lock函數 //加鎖,加鎖不成功,一直阻塞在那等待
pthread_mutex_trylock函數 //嘗試加鎖,加鎖不成功,直接返回
pthread_mutex_unlock函數 //解鎖
以上5個函數的返回值都是:成功返回0, 失敗返回錯誤號。
pthread_mutex_t 類型,其本質是一個結構體。為簡化理解,應用時可忽略其實現細節,簡單當成整數看待。
變量mutex只有兩種取值1、0。
- pthread_mutex_init函數
初始化一個互斥鎖(互斥量) ---> 初值可看作1
原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
參1:傳出參數,調用時應傳 &mutex
這個restrict關鍵字可能第一次遇到,說明一下:只用於限制指針,告訴編譯器,所有修改該指針指向內存中內容的操作,只能通過本指針完成。不能通過除本指針以外的其他變量或指針修改
參2:互斥量屬性。是一個傳入參數,通常傳NULL,選用默認屬性(線程間共享)。互斥鎖也可以用於進程間同步,需要修改屬性為進程間共享。 參APUE.12.4同步屬性
- 靜態初始化:如果互斥鎖 mutex 是靜態分配的(定義在全局,或加了static關鍵字修飾),可以直接使用宏進行初始化。e.g. pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
- 動態初始化:局部變量應采用動態初始化。e.g. pthread_mutex_init(&mutex, NULL)
其他函數就不解釋了,相對比較簡單。
示例程序,主要對標准輸出進行加鎖,使主線程打印大寫“HELLO WORLD”,子線程打印小寫“hello world”,程序如下:

#include <stdio.h> #include <string.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> pthread_mutex_t mutex; void err_thread(int ret, char *str) { if (ret != 0) { fprintf(stderr, "%s:%s\n", str, strerror(ret)); pthread_exit(NULL); } } void *tfn(void *arg) { srand(time(NULL)); while (1) { pthread_mutex_lock(&mutex); printf("hello "); sleep(rand() % 3); /*模擬長時間操作共享資源,導致cpu易主,產生與時間有關的錯誤*/ printf("world\n"); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } return NULL; } int main(void) { int flag = 5; pthread_t tid; srand(time(NULL)); pthread_mutex_init(&mutex, NULL); pthread_create(&tid, NULL, tfn, NULL); while (flag--) { pthread_mutex_lock(&mutex); printf("HELLO "); sleep(rand() % 3); printf("WORLD\n"); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } pthread_cancel(tid); // 將子線程殺死,子線程中自帶取消點 pthread_join(tid, NULL); pthread_mutex_destroy(&mutex); return 0; //main中的return可以將整個進程退出 }
編譯時也要記得鏈上-pthread。
四、讀寫鎖
1、特性
(1)讀寫鎖是“寫模式加鎖”時, 解鎖前,所有對該鎖加鎖的線程都會被阻塞。
(2)讀寫鎖是“讀模式加鎖”時, 如果線程以讀模式對其加鎖會成功;如果線程以寫模式加鎖會阻塞。
(3)讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那么讀寫鎖會阻塞隨后的讀模式鎖請求。優先滿足寫模式鎖。讀鎖、寫鎖並行阻塞,寫鎖優先級高
讀寫鎖也叫共享-獨占鎖。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨占模式鎖住的。寫獨占、讀共享。
讀寫鎖非常適合於對數據結構讀的次數遠大於寫的情況。
敲重點了,記住12個字:寫獨占、讀共享;寫鎖優先級高。
2、主要函數
pthread_rwlock_init函數 //初始化
pthread_rwlock_destroy函數 //銷毀鎖
pthread_rwlock_rdlock函數 //讀加鎖,阻塞
pthread_rwlock_wrlock函數 //寫解鎖,阻塞
pthread_rwlock_tryrdlock函數 //嘗試讀解鎖
pthread_rwlock_trywrlock函數 //嘗試寫加鎖
pthread_rwlock_unlock函數 //解鎖
以上7 個函數的返回值都是:成功返回0, 失敗直接返回錯誤號。
pthread_rwlock_t類型 用於定義一個讀寫鎖變量。
pthread_rwlock_t rwlock;
這些參考互斥鎖的函數,進行對比學習,只是多了讀鎖和寫鎖,就不過多解釋了。
實例程序,3個線程“寫”全局變量,5個全局變量“讀”全局變量,程序如下:

/* 3個線程不定時 "寫" 全局資源,5個線程不定時 "讀" 同一全局資源 */ #include <stdio.h> #include <unistd.h> #include <pthread.h> int counter; //全局資源 pthread_rwlock_t rwlock; void *th_write(void *arg) { int t; int i = (int)arg; while (1) { t = counter; usleep(1000); pthread_rwlock_wrlock(&rwlock); printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter); pthread_rwlock_unlock(&rwlock); usleep(5000); } return NULL; } void *th_read(void *arg) { int i = (int)arg; while (1) { pthread_rwlock_rdlock(&rwlock); printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter); pthread_rwlock_unlock(&rwlock); usleep(900); } return NULL; } int main(void) { int i; pthread_t tid[8]; pthread_rwlock_init(&rwlock, NULL); for (i = 0; i < 3; i++) pthread_create(&tid[i], NULL, th_write, (void *)i); for (i = 0; i < 5; i++) pthread_create(&tid[i+3], NULL, th_read, (void *)i); for (i = 0; i < 8; i++) pthread_join(tid[i], NULL); pthread_rwlock_destroy(&rwlock); //釋放讀寫瑣 return 0; }
另兩種方式,還有條件變量和信號量,條件變量比較難理解,篇幅比較多,所以會另寫一篇博客來寫,敬請期待哦!