Linux系統程式設計(28)——執行緒間同步
多個執行緒同時訪問共享資料時可能會衝突,這跟前面講訊號時所說的可重入性是同樣的問題。比如兩個執行緒都要把某個全域性變數增加1,這個操作在某平臺需要三條指令完成:
從記憶體讀變數值到暫存器
暫存器的值加1
將暫存器的值寫回記憶體
假設兩個執行緒在多處理器平臺上同時執行這三條指令,則可能導致下圖所示的結果,最後變數只加了一次而非兩次
我們通過一個簡單的程式觀察這一現象。上圖所描述的現象從理論上是存在這種可能的,但實際執行程式時很難觀察到,為了使現象更容易觀察到,我們把上述三條指令做的事情用更多條指令來做:
val= counter;
printf("%x:%d\n", (unsigned int)pthread_self(), val + 1);
counter= val + 1;
我們在“讀取變數的值”和“把變數的新值儲存回去”這兩步操作之間插入一個printf呼叫,它會執行write系統呼叫進核心,為核心排程別的執行緒執行提供了一個很好的時機。我們在一個迴圈中重複上述操作幾千次,就會觀察到訪問衝突的現象。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter; /* incremented by threads */
void *doit(void *);
int main(int argc, char **argv)
{
pthread_ttidA, tidB;
pthread_create(&tidA,NULL, &doit, NULL);
pthread_create(&tidB,NULL, &doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA,NULL);
pthread_join(tidB,NULL);
return0;
}
void *doit(void *vptr)
{
int i, val;
/*
* Each thread fetches, prints, and incrementsthe counter NLOOP times.
* The value of the counter should increasemonotonically.
*/
for(i = 0; i < NLOOP; i++) {
val= counter;
printf("%x:%d\n", (unsigned int)pthread_self(), val + 1);
counter= val + 1;
}
returnNULL;
}
我們建立兩個執行緒,各自把counter增加5000次,正常情況下最後counter應該等於10000,但事實上每次執行該程式的結果都不一樣,有時候數到5000多,有時候數到6000多。
對於多執行緒的程式,訪問衝突的問題是很普遍的,解決的辦法是引入互斥鎖(Mutex,Mutual Exclusive Lock),獲得鎖的執行緒可以完成“讀-修改-寫”的操作,然後釋放鎖給其它執行緒,沒有獲得鎖的執行緒只能等待而不能訪問共享資料,這樣“讀-修改-寫”三步操作組成一個原子操作,要麼都執行,要麼都不執行,不會執行到中間被打斷,也不會在其它處理器上並行做這個操作。
Mutex用pthread_mutex_t型別的變數表示,可以這樣初始化和銷燬:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t*mutex);
int pthread_mutex_init(pthread_mutex_t*restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
返回值:成功返回0,失敗返回錯誤號。
pthread_mutex_init函式對Mutex做初始化,引數attr設定Mutex的屬性,如果attr為NULL則表示預設屬性,本章不詳細介紹Mutex屬性,感興趣的讀者可以參考[APUE2e]。用pthread_mutex_init函式初始化的Mutex可以用pthread_mutex_destroy銷燬。如果Mutex變數是靜態分配的(全域性變數或static變數),也可以用巨集定義PTHREAD_MUTEX_INITIALIZER來初始化,相當於用pthread_mutex_init初始化並且attr引數為NULL。Mutex的加鎖和解鎖操作可以用下列函式:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t*mutex);
int pthread_mutex_trylock(pthread_mutex_t*mutex);
int pthread_mutex_unlock(pthread_mutex_t*mutex);
返回值:成功返回0,失敗返回錯誤號。
一個執行緒可以呼叫pthread_mutex_lock獲得Mutex,如果這時另一個執行緒已經呼叫pthread_mutex_lock獲得了該Mutex,則當前執行緒需要掛起等待,直到另一個執行緒呼叫pthread_mutex_unlock釋放Mutex,當前執行緒被喚醒,才能獲得該Mutex並繼續執行。
如果一個執行緒既想獲得鎖,又不想掛起等待,可以呼叫pthread_mutex_trylock,如果Mutex已經被另一個執行緒獲得,這個函式會失敗返回EBUSY,而不會使執行緒掛起等待。
現在我們用Mutex解決先前的問題:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter; /* incremented by threads */
pthread_mutex_t counter_mutex =PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv)
{
pthread_ttidA, tidB;
pthread_create(&tidA,NULL, doit, NULL);
pthread_create(&tidB,NULL, doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA,NULL);
pthread_join(tidB,NULL);
return0;
}
void *doit(void *vptr)
{
int i, val;
/*
* Each thread fetches, prints, and incrementsthe counter NLOOP times.
* The value of the counter should increasemonotonically.
*/
for(i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex);
val= counter;
printf("%x:%d\n", (unsigned int)pthread_self(), val + 1);
counter= val + 1;
pthread_mutex_unlock(&counter_mutex);
}
returnNULL;
}
這樣執行結果就正常了,每次執行都能數到10000。
那麼掛起等待”和“喚醒等待執行緒”的操作如何實現?每個Mutex有一個等待佇列,一個執行緒要在Mutex上掛起等待,首先在把自己加入等待佇列中,然後置執行緒狀態為睡眠,然後呼叫排程器函式切換到別的執行緒。一個執行緒要喚醒等待佇列中的其它執行緒,只需從等待佇列中取出一項,把它的狀態從睡眠改為就緒,加入就緒佇列,那麼下次排程器函式執行時就有可能切換到被喚醒的執行緒。
一般情況下,如果同一個執行緒先後兩次呼叫lock,在第二次呼叫時,由於鎖已經被佔用,該執行緒會掛起等待別的執行緒釋放鎖,然而鎖正是被自己佔用著的,該執行緒又被掛起而沒有機會釋放鎖,因此就永遠處於掛起等待狀態了,這叫做死鎖(Deadlock)。另一種典型的死鎖情形是這樣:執行緒A獲得了鎖1,執行緒B獲得了鎖2,這時執行緒A呼叫lock試圖獲得鎖2,結果是需要掛起等待執行緒B釋放鎖2,而這時執行緒B也呼叫lock試圖獲得鎖1,結果是需要掛起等待執行緒A釋放鎖1,於是執行緒A和B都永遠處於掛起狀態了。不難想象,如果涉及到更多的執行緒和更多的鎖,有沒有可能死鎖的問題將會變得複雜和難以判斷。
寫程式時應該儘量避免同時獲得多個鎖,如果一定有必要這麼做,則有一個原則:如果所有執行緒在需要多個鎖時都按相同的先後順序(常見的是按Mutex變數的地址順序)獲得鎖,則不會出現死鎖。比如一個程式中用到鎖1、鎖2、鎖3,它們所對應的Mutex變數的地址是鎖1<鎖2<鎖3,那麼所有執行緒在需要同時獲得2個或3個鎖時都應該按鎖1、鎖2、鎖3的順序獲得。如果要為所有的鎖確定一個先後順序比較困難,則應該儘量使用pthread_mutex_trylock呼叫代替pthread_mutex_lock呼叫,以免死鎖。
相關文章
- Linux系統程式設計(29)——執行緒間同步(續篇)Linux程式設計執行緒
- 【linux】系統程式設計-5-執行緒Linux程式設計執行緒
- Linux系統程式設計(27)——執行緒控制Linux程式設計執行緒
- iOS多執行緒程式設計:執行緒同步總結iOS執行緒程式設計
- .NET多執行緒程式設計(3):執行緒同步 (轉)執行緒程式設計
- POSIX執行緒程式設計起步(2)-執行緒同步 (轉)執行緒程式設計
- Posix執行緒程式設計指南(3)-執行緒同步 (轉)執行緒程式設計
- .NET多執行緒程式設計(4):執行緒池和非同步程式設計 (轉)執行緒程式設計非同步
- java併發程式設計——執行緒同步Java程式設計執行緒
- 執行緒同步(C#程式設計指南)執行緒C#程式設計
- Python系統程式設計之執行緒Python程式設計執行緒
- C#多執行緒程式設計-基元執行緒同步構造C#執行緒程式設計
- Linux核心同步,程式,執行緒同步薦Linux執行緒
- linux 多執行緒程式設計Linux執行緒程式設計
- JavaScript 單執行緒之非同步程式設計JavaScript執行緒非同步程式設計
- Linux系統下的多執行緒程式設計入門(轉)Linux執行緒程式設計
- python多執行緒程式設計3: 使用互斥鎖同步執行緒Python執行緒程式設計
- Q:你瞭解非同步程式設計、程式、單執行緒、多執行緒嗎?非同步程式設計執行緒
- linux系統時間程式設計(9) 計算程式片段執行時間clock函式Linux程式設計函式
- 作業系統的程式/執行緒同步問題作業系統執行緒
- Java併發程式設計,互斥同步和執行緒之間的協作Java程式設計執行緒
- Java多執行緒程式設計之同步器Java執行緒程式設計
- Linux 多執行緒程式設計(不限Linux)Linux執行緒程式設計
- PHP系統程式設計--01.多程式與多執行緒PHP程式設計執行緒
- 4、Linux多執行緒,執行緒同步(2)Linux執行緒
- 【轉】1.1非同步程式設計:執行緒概述及使用非同步程式設計執行緒
- Linux下的多執行緒程式設計Linux執行緒程式設計
- Linux C++ 多執行緒程式設計LinuxC++執行緒程式設計
- 程式設計思想之多執行緒與多程式(1):以作業系統的角度述說執行緒與程式程式設計執行緒作業系統
- 多執行緒程式設計執行緒程式設計
- 執行緒程式設計(一)執行緒程式設計
- 併發程式設計之多執行緒執行緒安全程式設計執行緒
- [短文速讀 -5] 多執行緒程式設計引子:程式、執行緒、執行緒安全執行緒程式設計
- .NET非同步程式設計——給執行緒傳遞資料非同步程式設計執行緒
- Java多執行緒程式設計筆記2:synchronized同步方法Java執行緒程式設計筆記synchronized
- Linux下的多執行緒程式設計(轉)Linux執行緒程式設計
- linux程式和執行緒之間通訊方法和同步方法總結Linux執行緒
- 一、(LINUX 執行緒同步) 引入Linux執行緒