互斥與同步
- 一.線程的局部存儲
- 二.線程的分離
- 三.互斥
- 1.一些概念
- 2.上鎖
- 3.鎖的原理
- 4.死鎖
一.線程的局部存儲
例子
可以看到全局變量是所有線程共享的,如果我們想要每個線程都單獨訪問g_val怎么辦呢?其實我們可以在它前面加上__thread修飾。
這就相當于把g_val從全局變量去儲存到了局部儲存里。每個線程可以單獨訪問自己的g_val。(注意__thread只能定義內置類型)
二.線程的分離
默認情況下,新創建的線程是joinable的,線程退出后,需要對其進行pthread_join操作,否則無法釋放資源,從而造成系統泄漏。
如果不關心線程的返回值,join是一種負擔(因為它會阻塞我們的主線程),這個時候,我們可以告訴系統,當線程退出時,自動釋放線程資源。
這個函數在主函數和當前線程里都可以使用。
三.互斥
1.一些概念
臨界資源:多線程執行流共享的資源就叫做臨界資源 臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區。
互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用。
原子性(后面討論如何實現):不會被任何調度機制打斷的操作,該操作只有兩態,要么完成,要么未完成。
大部分情況,線程使用的數據都是局部變量,變量的地址空間在線程棧空間內,這種情況,變量歸屬單個線程,其他線程無法獲得這種變量。
但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數據的共享,完成線程之間的交互。
多個線程并發的操作共享變量,會帶來一些問題。
例如,一個搶票系統
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket=100;
void *route(void *arg)
{char *id = (char*)arg;while(1) {if (ticket>0) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;} else {break;}}
}
int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);return 0;
}
可以看到已經搶到了負數,很明顯是不符合實際的。這是因為在執行打印ticket操作時,操作系統需要從CPU里讀取ticket數據,而當一個線程已經打印了ticket=0后,再執行了減減操作,ticket變為了-1,將ticket的值再CPU里更新;這時切換到了另一個線程,而該線程又恰好正要執行打印ticket操作,那么它從CPU里讀取了數據,打印出來就為了負數。
要解決以上問題,需要做到三點:
1.代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其他線程進入該臨界區。
2.如果多個線程同時要求執行臨界區的代碼,并且臨界區沒有線程在執行,那么只能允許一個線程進入該臨界區。
3.如果線程不在臨界區中執行,那么該線程不能阻止其他線程進入臨界區。
要做到這三點,本質上就是需要一把鎖。Linux上提供的這把鎖叫互斥量。
2.上鎖
創建鎖
調用函數時可能會出現以下情況:
1.互斥量處于未鎖狀態,該函數會將互斥量鎖定,同時返回成功
2.發起函數調用時,其他線程已經鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競爭到互斥量,那么pthread_ lock調用會陷入阻塞(執行流被掛起),等待互斥量解鎖。
加鎖
修改代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>pthread_mutex_t mutex=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;//初始化
int ticket=100;
void *route(void *arg)
{char *id =(char*)arg;while(1) {pthread_mutex_lock(&mutex);//上鎖if(ticket>0) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex);//解鎖} else {pthread_mutex_unlock(&mutex);//解鎖break;}}
}
int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);//銷毀return 0;
}
我們發現票數問題得到了解決,但是票全被一個線程搶走了,這是怎么回事呢?其實是由于不同線程對于鎖的競爭能力是不同的,這里當線程2釋放鎖后,馬上又去申請了鎖,導致鎖一直被線程2拿著,出現了線程饑餓問題。我們可以在外面加上sleep函數,讓每個線程釋放鎖后休息一段時間,避免鎖一直在某一個線程上。
3.鎖的原理
經過上面的例子,大家已經意識到單純的 i++ 或者 ++i 都不是原子的,有可能會有數據一致性問題。
為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把寄存器和內存單元的數據相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問內存的 總線周期也有先后,一個處理器上的交換指令執行時另一個處理器的交換指令只能等待總線周期。 現在我們把lock和unlock的偽代碼改一下。
movb語句是把al寄存器置零。
xchgb語句就是把al寄存器里的數據交換與內存里的mutex(1)變量進行一次交換(此時mutex就變為了0)。注意mutex是所有線程共享,也就是說其實1只有一份,當第一個進程將mutex里的1交換走后,后面的線程就無法拿到1,也就是上鎖了。
4.死鎖
死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處于的一種永久等待狀態。
死鎖的必要條件
互斥條件:一個資源每次只能被一個執行流使用。
請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪。
循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關系。
避免死鎖
破壞死鎖的四個必要條件。
加鎖順序一致。
避免鎖未釋放的場景。
資源一次性分配。