你真的懂執行緒同步麼?
前言:學程序時,學習的重點應該程序間通訊,而學習執行緒時,重點就應該是執行緒同步了。想過為什麼?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可以將整個程序退出 } View Code
編譯時也要記得鏈上-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; } View Code
另兩種方式,還有條件變數和訊號量,條件變數比較難理解,篇幅比較多,所以會另寫一篇部落格來寫,敬請期待哦!