7.10-第十課:執行緒同步
阿新 • • 發佈:2018-11-29
================
第十課 執行緒同步
================
一、競爭與同步 --------------
當多個執行緒同時訪問其所共享的程序資源時, 需要相互協調,以防止出現數據不一致、 不完整的問題。這就叫執行緒同步。
範例:vie.c
理想中的原子++:
-----------------+-----------------+------ 執行緒1 | 執行緒2 | 記憶體 --------+--------+--------+--------+------ 指 令 | 暫存器 | 指 令 | 暫存器 | g_cn --------+--------+--------+--------+------ 讀記憶體 | 0 | | | 0 算加法 | 1 | | | 0 寫記憶體 | 1 | | | 1 | | 讀記憶體 | 1 | 1 | | 算加法 | 2 | 1 | | 寫記憶體 | 2 | 2 --------+--------+--------+--------+------
現實中的非原子++:
-----------------+-----------------+------ 執行緒1 | 執行緒2 | 記憶體 --------+--------+--------+--------+------ 指 令 | 暫存器 | 指 令 | 暫存器 | g_cn --------+--------+--------+--------+------ 讀記憶體 | 0 | | | 0 | | 讀記憶體 | 0 | 0 算加法 | 1 | | | 0 | | 算加法 | 1 | 0 寫記憶體 | 1 | | | 1 | | 寫記憶體 | 1 | 1 --------+--------+--------+--------+------
二、互斥量 ----------
int pthread_mutex_init (pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
亦可
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock (pthread_mutex_t* mutex);
int pthread_mutex_unlock (pthread_mutex_t* mutex);
int pthread_mutex_destroy (pthread_mutex_t* mutex);
1) 互斥量被初始化為非鎖定狀態;
2) 執行緒1呼叫pthread_mutex_lock函式,立即返回, 互斥量呈鎖定狀態;
3) 執行緒2呼叫pthread_mutex_lock函式,阻塞等待;
4) 執行緒1呼叫pthread_mutex_unlock函式, 互斥量呈非鎖定狀態;
5) 執行緒2被喚醒,從pthread_mutex_lock函式中返回, 互斥量呈鎖定狀態;
...
範例:mutex.c
三、訊號量 ----------
訊號量是一個計數器,用於控制訪問有限共享資源的執行緒數。
#include <semaphore.h>
// 建立訊號量 int sem_init (sem_t* sem, int pshared, unsigned int value);
sem - 訊號量ID,輸出。
pshared - 一般取0,表示呼叫程序的訊號量。 非0表示該訊號量可以共享記憶體的方式, 為多個程序所共享(Linux暫不支援)。
value - 訊號量初值。
// 訊號量減1,不夠減即阻塞 int sem_wait (sem_t* sem);
// 訊號量減1,不夠減即返回-1,errno為EAGAIN int sem_trywait (sem_t* sem);
// 訊號量減1,不夠減即阻塞, // 直到abs_timeout超時返回-1,errno為ETIMEDOUT int sem_timedwait (sem_t* sem, const struct timespec* abs_timeout);
struct timespec { time_t tv_sec; // Seconds long tv_nsec; // Nanoseconds [0 - 999999999] };
// 訊號量加1 int sem_post (sem_t* sem);
// 銷燬訊號量 int sem_destroy (sem_t* sem);
範例:sem.c
注意:
1) 訊號量APIs沒有宣告在pthread.h中, 而是宣告在semaphore.h中,失敗也不返回錯誤碼, 而是返回-1,同時設定errno。
2) 互斥量任何時候都只允許一個執行緒訪問共享資源, 而訊號量則允許最多value個執行緒同時訪問共享資源, 當value為1時,與互斥量等價。
範例:pool.c
四、死鎖問題 ------------
執行緒1 執行緒2 | | 獲取A 獲取B | | 獲取B 獲取A <- 死鎖 \ / 釋放B X 釋放A / \ 釋放A 釋放B
範例:dead.c
五、條件變數 ------------
生產者消費者模型
生產者:產生資料的執行緒。 消費者:使用資料的執行緒。
通過緩衝區隔離生產者和消費者,與二者直連相比, 避免相互等待,提高執行效率。
生產快於消費,緩衝區滿,撐死。 消費快於生產,緩衝區空,餓死。
條件變數可以讓呼叫執行緒在滿足特定條件的情況下暫停。
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* attr);
亦可
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 使呼叫執行緒睡入條件變數cond,同時釋放互斥鎖mutex int pthread_cond_wait (pthread_cond_t* cond, pthread_mutex_t* mutex);
int pthread_cond_timedwait (pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);
struct timespec { time_t tv_sec; // Seconds long tv_nsec; // Nanoseconds [0 - 999999999] };
// 從條件變數cond中喚出一個執行緒, // 令其重新獲得原先的互斥鎖 int pthread_cond_signal (pthread_cond_t* cond);
注意:被喚出的執行緒此刻將從pthread_cond_wait函式中返回, 但如果該執行緒無法獲得原先的鎖,則會繼續阻塞在加鎖上。
// 從條件變數cond中喚出所有執行緒 int pthread_cond_broadcast (pthread_cond_t* cond);
int pthread_cond_destroy (pthread_cond_t* cond);
範例:cond.c
注意:當一個執行緒被從條件變數中喚出以後, 導致其睡入條件變數的條件可能還需要再判斷一次, 因其隨時有可能別其它執行緒修改。
範例:bc.c (if->while)
六、哲學家就餐問題 ------------------
1965年,著名電腦科學家艾茲格·迪科斯徹,提出並解決 了一個他稱之為哲學家就餐的同步問題。從那時起,每個發 明同步原語的人,都希望通過解決哲學家就餐問題來展示其 同步原語的精妙之處。
這個問題可以簡單地描述如下:
五個哲學家圍坐在一張圓桌周圍,每個哲學家面前都有一盤 通心粉。由於通心粉很滑,所以需要兩把叉子才能夾住。相 鄰兩個盤子之間放有一把叉子。哲學家的生活中有兩種交替 活動時段:即吃飯和思考。當一個哲學家覺得餓了時,他就 試圖分兩次去取其左邊和右邊的叉子,每次拿一把,但不分 次序。如果成功地得到了兩把叉子,就開始吃飯,吃完後放 下叉子繼續思考。
圖示:dining.png
關鍵問題是:能為每一個哲學家寫一段描述其行為的程式, 且決不會死鎖嗎?
提示:
如果五位哲學家同時拿起左面的叉子,就沒有人能夠拿到他 們各自右面的叉子,於是發生了死鎖。
如果每位哲學家在拿到左面的叉子後,發現其右面的叉子不 可用,那麼就先放下左面的叉的,等待一段時間,再重複此 過程。可能在某一個瞬間,所有的哲學家都同時拿起左叉, 看到右叉不可用,又都放下左叉,等一會兒,又都同時拿起 左叉,如此重複下去。雖然程式在不停執行,但都無法取得 進展,於是發生了活鎖。
思路:
解決問題的關鍵在於,必須保證任意一位哲學家只有在其左 右兩個鄰居都沒有在進餐時,才允許其進入進餐狀態。這樣 做不僅不會發生死鎖,而且對於任意數量的哲學家都能獲得 最大限度的並行性。
範例:dining.c
來自為知筆記(Wiz)
一、競爭與同步 --------------
當多個執行緒同時訪問其所共享的程序資源時, 需要相互協調,以防止出現數據不一致、 不完整的問題。這就叫執行緒同步。
範例:vie.c
理想中的原子++:
-----------------+-----------------+------ 執行緒1 | 執行緒2 | 記憶體 --------+--------+--------+--------+------ 指 令 | 暫存器 | 指 令 | 暫存器 | g_cn --------+--------+--------+--------+------ 讀記憶體 | 0 | | | 0 算加法 | 1 | | | 0 寫記憶體 | 1 | | | 1 | | 讀記憶體 | 1 | 1 | | 算加法 | 2 | 1 | | 寫記憶體 | 2 | 2 --------+--------+--------+--------+------
現實中的非原子++:
-----------------+-----------------+------ 執行緒1 | 執行緒2 | 記憶體 --------+--------+--------+--------+------ 指 令 | 暫存器 | 指 令 | 暫存器 | g_cn --------+--------+--------+--------+------ 讀記憶體 | 0 | | | 0 | | 讀記憶體 | 0 | 0 算加法 | 1 | | | 0 | | 算加法 | 1 | 0 寫記憶體 | 1 | | | 1 | | 寫記憶體 | 1 | 1 --------+--------+--------+--------+------
二、互斥量 ----------
int pthread_mutex_init (pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
亦可
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock (pthread_mutex_t* mutex);
int pthread_mutex_unlock (pthread_mutex_t* mutex);
int pthread_mutex_destroy (pthread_mutex_t* mutex);
1) 互斥量被初始化為非鎖定狀態;
2) 執行緒1呼叫pthread_mutex_lock函式,立即返回, 互斥量呈鎖定狀態;
3) 執行緒2呼叫pthread_mutex_lock函式,阻塞等待;
4) 執行緒1呼叫pthread_mutex_unlock函式, 互斥量呈非鎖定狀態;
5) 執行緒2被喚醒,從pthread_mutex_lock函式中返回, 互斥量呈鎖定狀態;
...
範例:mutex.c
三、訊號量 ----------
訊號量是一個計數器,用於控制訪問有限共享資源的執行緒數。
#include <semaphore.h>
// 建立訊號量 int sem_init (sem_t* sem, int pshared, unsigned int value);
sem - 訊號量ID,輸出。
pshared - 一般取0,表示呼叫程序的訊號量。 非0表示該訊號量可以共享記憶體的方式, 為多個程序所共享(Linux暫不支援)。
value - 訊號量初值。
// 訊號量減1,不夠減即阻塞 int sem_wait (sem_t* sem);
// 訊號量減1,不夠減即返回-1,errno為EAGAIN int sem_trywait (sem_t* sem);
// 訊號量減1,不夠減即阻塞, // 直到abs_timeout超時返回-1,errno為ETIMEDOUT int sem_timedwait (sem_t* sem, const struct timespec* abs_timeout);
struct timespec { time_t tv_sec; // Seconds long tv_nsec; // Nanoseconds [0 - 999999999] };
// 訊號量加1 int sem_post (sem_t* sem);
// 銷燬訊號量 int sem_destroy (sem_t* sem);
範例:sem.c
注意:
1) 訊號量APIs沒有宣告在pthread.h中, 而是宣告在semaphore.h中,失敗也不返回錯誤碼, 而是返回-1,同時設定errno。
2) 互斥量任何時候都只允許一個執行緒訪問共享資源, 而訊號量則允許最多value個執行緒同時訪問共享資源, 當value為1時,與互斥量等價。
範例:pool.c
四、死鎖問題 ------------
執行緒1 執行緒2 | | 獲取A 獲取B | | 獲取B 獲取A <- 死鎖 \ / 釋放B X 釋放A / \ 釋放A 釋放B
範例:dead.c
五、條件變數 ------------
生產者消費者模型
生產者:產生資料的執行緒。 消費者:使用資料的執行緒。
通過緩衝區隔離生產者和消費者,與二者直連相比, 避免相互等待,提高執行效率。
生產快於消費,緩衝區滿,撐死。 消費快於生產,緩衝區空,餓死。
條件變數可以讓呼叫執行緒在滿足特定條件的情況下暫停。
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* attr);
亦可
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 使呼叫執行緒睡入條件變數cond,同時釋放互斥鎖mutex int pthread_cond_wait (pthread_cond_t* cond, pthread_mutex_t* mutex);
int pthread_cond_timedwait (pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);
struct timespec { time_t tv_sec; // Seconds long tv_nsec; // Nanoseconds [0 - 999999999] };
// 從條件變數cond中喚出一個執行緒, // 令其重新獲得原先的互斥鎖 int pthread_cond_signal (pthread_cond_t* cond);
注意:被喚出的執行緒此刻將從pthread_cond_wait函式中返回, 但如果該執行緒無法獲得原先的鎖,則會繼續阻塞在加鎖上。
// 從條件變數cond中喚出所有執行緒 int pthread_cond_broadcast (pthread_cond_t* cond);
int pthread_cond_destroy (pthread_cond_t* cond);
範例:cond.c
注意:當一個執行緒被從條件變數中喚出以後, 導致其睡入條件變數的條件可能還需要再判斷一次, 因其隨時有可能別其它執行緒修改。
範例:bc.c (if->while)
六、哲學家就餐問題 ------------------
1965年,著名電腦科學家艾茲格·迪科斯徹,提出並解決 了一個他稱之為哲學家就餐的同步問題。從那時起,每個發 明同步原語的人,都希望通過解決哲學家就餐問題來展示其 同步原語的精妙之處。
這個問題可以簡單地描述如下:
五個哲學家圍坐在一張圓桌周圍,每個哲學家面前都有一盤 通心粉。由於通心粉很滑,所以需要兩把叉子才能夾住。相 鄰兩個盤子之間放有一把叉子。哲學家的生活中有兩種交替 活動時段:即吃飯和思考。當一個哲學家覺得餓了時,他就 試圖分兩次去取其左邊和右邊的叉子,每次拿一把,但不分 次序。如果成功地得到了兩把叉子,就開始吃飯,吃完後放 下叉子繼續思考。
圖示:dining.png
關鍵問題是:能為每一個哲學家寫一段描述其行為的程式, 且決不會死鎖嗎?
提示:
如果五位哲學家同時拿起左面的叉子,就沒有人能夠拿到他 們各自右面的叉子,於是發生了死鎖。
如果每位哲學家在拿到左面的叉子後,發現其右面的叉子不 可用,那麼就先放下左面的叉的,等待一段時間,再重複此 過程。可能在某一個瞬間,所有的哲學家都同時拿起左叉, 看到右叉不可用,又都放下左叉,等一會兒,又都同時拿起 左叉,如此重複下去。雖然程式在不停執行,但都無法取得 進展,於是發生了活鎖。
思路:
解決問題的關鍵在於,必須保證任意一位哲學家只有在其左 右兩個鄰居都沒有在進餐時,才允許其進入進餐狀態。這樣 做不僅不會發生死鎖,而且對於任意數量的哲學家都能獲得 最大限度的並行性。
範例:dining.c