同步概念
所謂同步,即同時起步,協調一致。不同的對象,對“同步”的理解方式略有不同。如,設備同步,是指在兩 個設備之間規定一個共同的時間參考;數據庫同步,是指讓兩個或多個數據庫內容保持一致,或者按需要部分保持 一致;文件同步,是指讓兩個或多個文件夾里的文件保持一致。等等
而,編程中、通信中所說的同步與生活中大家印象中的同步概念略有差異。“同”字應是指協同、協助、互相 配合。主旨在協同步調,按預定的先后次序運行。
線程同步
同步即協同步調,按預定的先后次序運行。
線程同步,指一個線程發出某一功能調用時,在沒有得到結果之前,該調用不返回。同時其它線程為保證數據 一致性,不能調用該功能。
-
舉例 1: 銀行存款 5000。柜臺,折:取 3000;提款機,卡:取 3000。剩余:2000
-
舉例 2: 內存中 100 字節,線程 T1 欲填入全 1, 線程 T2 欲填入全 0。但如果 T1 執行了 50 個字節失去 cpu,T2 執行,會將 T1 寫過的內容覆蓋。當 T1 再次獲得 cpu 繼續 從失去 cpu 的位置向后寫入 1,當執行結束,內存中的 100 字節,既不是全 1,也不是全 0。
產生的現象叫做“與時間有關的錯誤”(
time related
)。為了避免這種數據混亂,線程需要同步。
“同步”的目的,是為了避免數據混亂,解決與時間有關的錯誤。實際上,不僅線程間需要同步,進程間、信 號間等等都需要同步機制。
因此,所有“多個控制流,共同操作一個共享資源”的情況,都需要同步。
數據混亂原因
- 資源共享(獨享資源則不會)
- 調度隨機(意味著數據訪問會出現競爭)
- 線程間缺乏必要的同步機制
- 以上 3 點中,前兩點不能改變,欲提高效率,傳遞數據,資源必須共享。只要共享資源,就一定會出現競爭。 只要存在競爭關系,數據就很容易出現混亂。
- 所以只能從第三點著手解決。使多個線程在訪問共享資源的時候,出現互斥。
互斥量 mutex
Linux 中提供一把互斥鎖 mutex(也稱之為互斥量) 。
每個線程在對資源操作前都嘗試先加鎖,成功加鎖才能操作,操作結束解鎖。
- 資源還是共享的,線程間也還是競爭的,
- 但通過“鎖”就將資源的訪問變成互斥操作,而后與時間有關的錯誤也不會再產生了。
但,應注意:同一時刻,只能有一個線程持有該鎖。
當 A 線程對某個全局變量加鎖訪問,B 在訪問前嘗試加鎖,拿不到鎖,B 阻塞。C 線程不去加鎖,而直接訪問 該全局變量,依然能夠訪問,但會出現數據混亂。
所以,互斥鎖實質上是操作系統提供的一把“建議鎖”(又稱“協同鎖”),建議程序中有多線程訪問共享資源 的時候使用該機制。但,并沒有強制限定。
因此,即使有了 mutex,如果有線程不按規則來訪問數據,依然會造成數據混亂。
主要應用函數:
pthread_mutex_init
函數pthread_mutex_destroy
函數pthread_mutex_lock
函數pthread_mutex_trylock
函數pthread_mutex_unlock
函數- 以上 5 個函數的返回值都是:成功返回 0, 失敗返回錯誤號。
pthread_mutex_t
類型,其本質是一個結構體。為簡化理解,應用時可忽略其實現細節,簡單當成整數看待。pthread_mutex_tmutex
; 變量 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_tmuetx=PTHREAD_MUTEX_INITIALIZER
; - 動態初始化:局部變量應采用動態初始化。e.g.
pthread_mutex_init(&mutex,NULL)
pthread_mutex_destroy 函數
銷毀一個互斥鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock 函數
加鎖成功。可理解為將 mutex–(或-1)
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_unlock 函數
解鎖成功。可理解為將 mutex++(或+1)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_trylock 函數
嘗試加鎖
int pthread_mutex_trylock(pthread_mutex_t *mutex);
示例:子線程打印小寫,主控線程打印大寫
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>pthread_mutex_t mutex; //鎖的定義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)
{pthread_t tid;srand(time(NULL));pthread_mutex_init(&mutex,NULL); //mutex == 1pthread_create(&tid,NULL,tfn,NULL);while(1){pthread_mutex_lock(&mutex);printf("HELLO");sleep(rand()%3);printf("WORLD\n");pthread_mutex_unlock(&mutex); sleep(rand() % 3);}pthread_mutex_destroy(&mutex);}
- 定義全局互斥量,初始化
init(&m,NULL)
互斥量,添加對應的 destry - 兩個線程 while 中,兩次 printf 前后,分別加 lock 和 unlock
- 將 unlock 挪至第二個 sleep 后,發現交替現象很難出現。
線程在操作完共享資源后本應該立即解鎖,但修改后,線程抱著鎖睡眠。睡醒解鎖后又立即加鎖,這兩個 庫函數本身不會阻塞。
所以在這兩行代碼之間失去 cpu 的概率很小。因此,另外一個線程很難得到加鎖的機會 - main 中加 flag=5 將 flg 在 while 中-- 這時,主線程輸出 5 次后試圖銷毀鎖,但子線程未將鎖釋放,無法 完成。
- main 中加 pthread_cancel()將子線程取消。
注意事項:在訪問共享資源前加鎖,訪問結束后立即解鎖。鎖的“粒度”應越小越好。
死鎖
- 線程試圖對同一個互斥量 A 加鎖兩次。
- 線程 1 擁有 A 鎖,請求獲得 B 鎖;線程 2 擁有 B 鎖,請求獲得 A 鎖
- 當不能獲得所有的鎖時,放棄已經占有的鎖
讀寫鎖
與互斥量類似,但讀寫鎖允許更高的并行性。其特性為:寫獨占,讀共享。
讀寫鎖狀態
一把讀寫鎖具備三種狀態:
- 讀模式下加鎖狀態 (讀鎖)
- 寫模式下加鎖狀態 (寫鎖)
- 不加鎖狀態
讀寫鎖特性:
- 讀寫鎖是“寫模式加鎖”時, 解鎖前,所有對該鎖加鎖的線程都會被阻塞。
- 讀寫鎖是“讀模式加鎖”時, 如果線程以讀模式對其加鎖會成功;如果線程以寫模式加鎖會阻塞。
- 讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那么讀寫鎖 會阻塞隨后的讀模式鎖請求。優先滿足寫模式鎖。讀鎖、寫鎖并行阻塞,寫鎖優先級高
- 讀寫鎖也叫共享-獨占鎖。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨 占模式鎖住的。寫獨占、讀共享。
- 讀寫鎖非常適合于對數據結構讀的次數遠大于寫的情況。
主要應用函數:
- 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_trwlock;
pthread_rwlock_init 函數
初始化一把讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t *restrictrwlock,const pthread_rwlockattr_t *restrictattr);
參 2:attr 表讀寫鎖屬性,通常使用默認屬性,傳 NULL 即可。
pthread_rwlock_destroy 函數
銷毀一把讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock 函數
以讀方式請求讀寫鎖。(常簡稱為:請求讀鎖)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock 函數
以寫方式請求讀寫鎖。(常簡稱為:請求寫鎖)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock 函數
解鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock 函數
非阻塞以讀方式請求讀寫鎖(非阻塞請求讀鎖)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock 函數
非阻塞以寫方式請求讀寫鎖(非阻塞請求寫鎖)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
同時有多個線程對同一全局數據讀、寫操作
#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);//三個寫線程,5個讀線程for(i = 0; i < 8; i++)pthread_join(tid[i],NULL);pthread_rwlock_destroy(&rwlock); //釋放讀寫鎖return 0;
}