一、線程同步的引入
通過上面的搶票系統我們發現,有的線程,進行工作(掛鎖),當其馬上結束工作(解鎖),發現外面有很多線程在排隊等著加鎖執行任務,這個線程解鎖后就立馬給自己加鎖(可以想象成自己能直到解鎖的時間,所以加鎖的優勢比其他線程大),但是如果這個線程本身并沒有做什么工作,就會使工作效率低下,線程饑餓等問題。
為了解決這個問題,我們規定:所有線程等待加鎖必須排隊,加鎖的線程解鎖后必須排在隊尾等待下一次加鎖,這樣就能保證所有線程都可以執行任務。這就是線程同步。
二、條件變量
條件變量與互斥鎖的相關接口幾乎完全相同,在這里就不多介紹語法了。
但也有些新的接口。
我們用一個場景來介紹一下這些接口是怎么用的。
我們現在有一個主線程和若干個新線程(共用一個鎖)
主線程負責派發任務,新線程負責執行(如果不派發就無法執行)
所以,當一個線程拿到鎖進行查看時,如果發現有任務就執行,但如果發現任務還沒派發,就會去某個條件變量那里進行等待(pthread_cond_wait接口),后面的線程也是如此,這里是在條件變量這的等待隊列(先進先出)。在等待過程中就不會一直的申請加鎖而阻止主線程派發任務了。在所有新線程等待的情況下,當主線程加鎖后派發任務后,會通知(喚醒)第一個開始等待的新線程,讓它去加鎖執行任務(pthread_cond_signal接口),同時,主線程也可以一次喚醒多個線程,讓他們去搶奪該資源(pthread_cond_broadcast接口),這便是這些接口的使用方法。
三、生產者消費者模型
對于多線程并發的情況,我們可以用生產者消費者模型來解釋一下。
首先,我們把主線程稱為生產者,可以視為派發任務方,然后多個新線程稱為消費者,視為取得任務并執行任務。在二者之間,有一個橋梁——共享或臨界資源。那么生產者就會對這份資源進行派發任務的行為,其他消費者要執行任務時,并不會直接向生產者詢問索取,而是直接訪問該臨界資源、上鎖并執行。通過這個臨界資源,我們就可以講生產和消費進行解耦了。同時,臨界資源可以作為媒介,當生產者派發任務多時可以間接通知消費者并增加消費者數量,反之則減少。
而在這個模型中,我們需要研究多個生產與多個消費的同步互斥關系。
首先,生產者與生產者之間是互斥的,畢竟從現實角度考慮,我派發了就不允許你派發。
消費者與消費者之間也是互斥的,如果任務很少而線程很多時,就會出現我搶到任務了而別人都沒搶到。
還有就是生產者與消費者,他們是既互斥又同步的,互斥在于,生產者在派發任務時不允許消費者進來槍任務干擾生產者,而消費者在搶任務時也不許生產者發任務干擾。同步在于,生產者派發任務時,雖然無法搶,但消費者們可以進行排隊等待,而對于生產者們也是一樣的。
總結就是:2個角色,3個關系,1個交易場所。
有了這個模型,我們就可以把條件變量的接口來解釋了。
首先
wait就相當于創建該媒介,讓線程在該阻塞隊列中等待。signal就是叫醒排在最前面的一個線程執行,broadcast就是一次喚醒所有在隊列的線程。
至于wait的接口為什么還要把鎖傳進去呢?——在線程進入等待的時候要解鎖!即解鎖與等待是原子性操作。
條件等待是線程間同步的?種手段,如果只有?個線程,條件不滿足,?直等下去都不會滿足, 所以必須要有?個線程通過某些操作,改變共享變量,使原先不滿足的條件變得滿足,并且友好 的通知等待在條件變量上的線程。條件不會無緣無故的突然變得滿足了,必然會牽扯到共享數據的變化。所以一定要用互斥鎖來保護。沒有互斥鎖就無法安全的獲取和修改共享數據。由于解鎖和等待不是原子操作。調用解鎖之后, pthread_cond_wait 之前,如果已經有其他線程獲取到互斥量,摒棄條件滿足,發送了信號,那么 pthread_cond_wait 將錯過這個信 號,可能會導致線程永遠阻塞在這個 pthread_cond_wait 。所以解鎖和等待必須是?個原子操作。
同時,被喚醒的線程也會重新申請鎖。
四、信號量
什么是信號量呢?首先,信號量和信號并沒有關系。
對于一塊公共資源(臨界區),我們可以當作一整份使用,比如我們的阻塞隊列。但有時,我們也可以分成多塊,然后以塊為單位訪問這塊資源,就可以讓多個執行流并發訪問該塊資源了。
首先,信號量是一個計數器,用來表明臨界資源中的資源數目。而我們申請信號量,本質是對臨界資源的預訂(并非等同于使用),只要預訂成功即使不使用,其他線程也無法使用。而我們信號量作為保護公共資源的機制,本身自己也屬于臨界資源,因此申請信號量的操作必須是原子的。此外,如果我們的計數器只有0和1,那不就成為了我們在線程互斥提到的鎖了嗎。
五、有關信號量的環形隊列
在上面的生產者消費者模型,我們的交易場所是阻塞隊列,有了信號量的引入,我們介紹一種新的交易場所模型——基于信號量的環形阻塞隊列(首尾相連)。(為空或為滿,都指向一個位置)
在這個隊列中,任何人訪問此臨界資源,必須先申請信號量!對于生產者來說,在意的是剩余空間,而對于消費者來講在意的是剩余數據。我們介紹一下此隊列的兩種情況
1.生產消費同時訪問同一個位置
如果為空,說明隊列無數據,消費者就需要阻塞等待,讓生產者放入數據。
如果為滿,說明隊列滿數據,生產者就需要阻塞等待,讓消費者取得并拿出數據。這兩種情況都可以保證原子性!體現了互斥同步原則。
2.指向不同位置
此時就有點像追及問題,此時生產者和消費者就可以同時進行訪問臨界資源,直到回到1情況再繼續一方等待。這種情況就滿足了并發原則。
因此,我們要設計兩個信號量分別給生產消費,以讓他們看到臨界資源的使用情況。
六、信號量的相關接口
1.sem_init
第一個參數不說了,第二個是是否設置為線程間共享,默認設置為0即可,第三個是設置信號量的初始值。
2.sem_destroy
3. P操作(申請信號量)
申請失敗就會阻塞
4.V操作(釋放信號量)
提一下,假設我們現在是生產者的視角,我們就需要讓空間信號量先進行P操作,等放入數據后,再讓消費者(數據信號量)進行V操作。
關于P、V操作
P、V本質都是對信號量的操作,P操作就是申請信號量,預訂資源(但如果信號量為負就會阻塞等待,直到有其他資源釋放導致該信號量++,然后把信號量--),V操作是釋放資源以及喚醒其他阻塞等待的線程,如果沒有等待的線程,那么信號量的值會累加。
對于空間信號量(生產)和數據信號量(消費)而言,我們簡述一下大致流程:
當要放入數據時,空間信號量會預訂(P)(如果沒有等待就進行插入操作),然后放完數據后,讓數據信號量進行V(因為插入一個數據后導致數據個數增加,消費者對于這塊空間的可訪問量++)。而消費者進行消費過程正好與之相反。 (我們把信號量看成可訪問資源塊的數量就好理解了)