国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型

這篇具有很好參考價(jià)值的文章主要介紹了【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

??作者:阿潤(rùn)菜菜
??專欄:Linux系統(tǒng)編程



線程同步互斥問題是指多線程程序中,如何保證共享資源的正確訪問和線程間的協(xié)作。
因?yàn)榫€程互斥是實(shí)現(xiàn)線程同步的基礎(chǔ)和前提,我們先講解線程互斥問題。

一、線程互斥

1. 為什么要有共享資源臨界保護(hù)?

在多線程中,假設(shè)我們有一個(gè)黃牛搶票的代碼,其中有一份共享資源tickets,如果多個(gè)線程都在搶票也就是對(duì)這個(gè)全局變量tickets做–操作,如果我們沒有對(duì)共享資源做保護(hù)(同一時(shí)間只能一個(gè)線程對(duì)資源進(jìn)行訪問)的話,就會(huì)存在并發(fā)訪問的問題,進(jìn)而導(dǎo)致數(shù)據(jù)不一致問題!這種情況下,票數(shù)最后會(huì)出現(xiàn)負(fù)數(shù)的情況。

【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型
那為什么會(huì)出現(xiàn)并發(fā)訪問導(dǎo)致數(shù)據(jù)不一致問題呢?–

了解上面的問題需要知道線程調(diào)度的特性,實(shí)際線程在被調(diào)度時(shí)他的上下文會(huì)被加載到CPU的寄存器中,而線程在被切換的時(shí)候,線程又會(huì)帶著自己的上下文被切換下去,此時(shí)要進(jìn)行線程的上下文保存,以便于下次該線程被切換上來的時(shí)候能夠進(jìn)行上下文數(shù)據(jù)的恢復(fù)。
除此之外,像tickets- -這樣的操作,對(duì)應(yīng)的匯編指令其實(shí)至少有三條,1.讀取數(shù)據(jù) 2.修改數(shù)據(jù) 3.寫回?cái)?shù)據(jù),而線程函數(shù)我們知道會(huì)在每個(gè)線程的私有棧都存在一份,在上面的例子中多個(gè)線程執(zhí)行同一份線程函數(shù),所以這個(gè)線程函數(shù)就絕對(duì)會(huì)處于被重入的狀態(tài),也就絕對(duì)會(huì)被多個(gè)線程執(zhí)行!今天我們假設(shè)只有一個(gè)CPU(CPU就是核心,處理器芯片會(huì)集成多個(gè)核心)在調(diào)度當(dāng)前進(jìn)程中的線程,那么線程是CPU調(diào)度的基本單位,所以也就會(huì)出現(xiàn)一個(gè)線程可能執(zhí)行一半的時(shí)候被切換下去了,并且該線程的上下文被保存起來,然后CPU又去調(diào)度進(jìn)程中的另一個(gè)線程。
【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型
當(dāng)多個(gè)線程同時(shí)進(jìn)入到分支判斷語句,然后去阻塞等待的情況,假設(shè)tickets已經(jīng)變成了1,然后其余的線程此時(shí)都被調(diào)度上來了,他們都開始執(zhí)行tickets- -,- -之后不滿足循環(huán)條件線程才會(huì)退出,那么如果我們創(chuàng)建出了4個(gè)線程,就會(huì)有3個(gè)線程在票數(shù)已經(jīng)為0的情況下繼續(xù)減減,所以就會(huì)出現(xiàn)票數(shù)為負(fù)數(shù)的情況

要解決以上的問題,我們提出的解決方案就是:加鎖

在學(xué)習(xí)鎖之間先搞清兩個(gè)概念

臨界資源是指一次僅允許一個(gè)進(jìn)程或線程使用的共享資源,如文件、變量等。
臨界區(qū)是指每個(gè)進(jìn)程或線程中訪問臨界資源的那段代碼,需要保證互斥和同步的執(zhí)行。

臨界資源和臨界區(qū)的區(qū)別是:

  • 臨界資源是一種系統(tǒng)資源,需要不同進(jìn)程或線程互斥訪問,而臨界區(qū)則是每個(gè)進(jìn)程或線程中訪問臨界資源的一段代碼,是屬于對(duì)應(yīng)進(jìn)程或線程的。
  • 臨界資源是一種抽象的概念,表示需要保護(hù)的共享數(shù)據(jù)或設(shè)備,而臨界區(qū)是一種具體的實(shí)現(xiàn),表示訪問臨界資源的具體操作和邏輯。
  • 臨界資源是一種靜態(tài)的屬性,表示某種資源是否可以被多個(gè)進(jìn)程或線程同時(shí)使用,而臨界區(qū)是一種動(dòng)態(tài)的狀態(tài),表示某個(gè)進(jìn)程或線程是否正在使用某種臨界資源。
    【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型

2.理解加鎖

2.1 認(rèn)識(shí)鎖,使用鎖

如果我們想讓多個(gè)執(zhí)行流串行的訪問臨界資源,而不是并發(fā)或并行的訪問臨界資源,這樣的線程調(diào)度方案就是互斥式的訪問臨界資源?。ù芯褪侵钢灰粋€(gè)線程開始執(zhí)行這個(gè)任務(wù),那么他就不能中斷,必須得等這個(gè)線程執(zhí)行完這個(gè)任務(wù),你才能切換其他線程執(zhí)行其他的任務(wù))

加鎖后線程的操作是原子性的,怎么理解?
當(dāng)線程在執(zhí)行一個(gè)對(duì)資源訪問的操作時(shí),要么做了這個(gè)操作,要么沒有做這個(gè)操作,只要兩種狀態(tài),不會(huì)出現(xiàn)做了一半這樣的狀態(tài),我們稱這樣的操作是原子性的。(就比如你媽讓你寫作業(yè),你要么給我把作業(yè)寫完了再出去玩,要么就一個(gè)字也別寫給我滾出家門,就這兩種狀態(tài),不會(huì)出現(xiàn)你寫了一半,然后你媽讓你出去玩的這種情況,這樣也是原子性)

我們下面講解互斥鎖

首先鎖實(shí)際就是一種數(shù)據(jù)類型,這個(gè)鎖就像我們平常定義出來的變量或是對(duì)象一樣,只不過這個(gè)鎖的類型是系統(tǒng)給我們封裝好的一種類型,進(jìn)行重定義后為pthread_mutex_t。變量或?qū)ο笤谏臅r(shí)候也是可以初始化的,變量初始化后,就是變量的定義,而不是聲明了。變量和對(duì)象也都有自己的銷毀方案,內(nèi)置類型的變量銷毀時(shí),操作系統(tǒng)會(huì)自動(dòng)回收其資源,而自定義對(duì)象銷毀時(shí),操作系統(tǒng)會(huì)調(diào)用其析構(gòu)函數(shù)進(jìn)行資源的回收。
鎖同樣也是如此,鎖也有自己的初始化和銷毀方案,如果你定義的是一把局部鎖,就需要用pthread_mutex_init()和pthread_mutex_destroy()來進(jìn)行初始化和銷毀,如果你定義的是一把全局鎖或靜態(tài)所,則不需要用init初始化和destroy銷毀,直接用PTHREAD_MUTEX_INITIALIZER進(jìn)行初始化即可,他有自己的初始化和銷毀方案,我們無須關(guān)心靜態(tài)或全局鎖如何銷毀。

定義好鎖之后,我們就可以對(duì)某一段代碼進(jìn)行加鎖和解鎖,加鎖與解鎖意味著,這段代碼不是一般的代碼,只有申請(qǐng)到鎖,持有鎖的線程才能訪問這段代碼,加鎖和解鎖之間的代碼可以稱為臨界區(qū),因?yàn)橄胍L問這段空間必須有鎖才可以訪問。

那我們?cè)撊绾螌?duì)共享資源進(jìn)行加鎖和解鎖呢?
手冊(cè)這樣寫的:
【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型

加鎖的使用方法一般包括以下幾個(gè)步驟:

  • 創(chuàng)建并初始化一個(gè)加鎖原語對(duì)象,使用相應(yīng)的API來分配內(nèi)存并設(shè)置屬性,如pthread_mutex_init用于創(chuàng)建并初始化一個(gè)互斥鎖對(duì)象。
  • 在訪問共享資源或臨界區(qū)域前,對(duì)加鎖原語對(duì)象進(jìn)行加鎖操作,使用相應(yīng)的API來獲取鎖的所有權(quán),如pthread_mutex_lock用于以阻塞方式獲取一個(gè)互斥鎖。
  • 在訪問共享資源或臨界區(qū)域后,對(duì)加鎖原語對(duì)象進(jìn)行解鎖操作,使用相應(yīng)的API來釋放鎖的所有權(quán),如pthread_mutex_unlock用于釋放一個(gè)互斥鎖。
  • 在不需要使用共享資源或臨界區(qū)域時(shí),銷毀加鎖原語對(duì)象,使用相應(yīng)的API來釋放內(nèi)存并清理資源,如pthread_mutex_destroy用于銷毀一個(gè)互斥鎖對(duì)象。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

static int g_count = 0;

DEFINE_MUTEX(static_lock); // 靜態(tài)互斥鎖

static void *thread_fun_1(void *data)
{
    mutex_lock(&static_lock); // 加鎖
    g_count++;
    printf("%s g_count: %d\n", __func__, g_count);
    mutex_unlock(&static_lock); // 解鎖
}

static void *thread_fun_2(void *data)
{
    mutex_lock(&static_lock); // 加鎖
    g_count++;
    printf("%s g_count: %d\n", __func__, g_count);
    mutex_unlock(&static_lock); // 解鎖
}

int main(int argc, char const *argv[])
{
    pthread_t pid[2];

    pthread_create(&pid[0], NULL, thread_fun_1, NULL);
    pthread_create(&pid[1], NULL, thread_fun_2, NULL);

    pthread_join(pid[0], NULL);
    pthread_join(pid[1], NULL);

    return 0;
}

}

這個(gè)示例中,兩個(gè)線程都使用同一個(gè)互斥鎖static_lock來保護(hù) g_count 的訪問。這樣,當(dāng)一個(gè)線程獲得了鎖,另一個(gè)線程就必須等待,直到鎖被釋放。這樣就可以保證g_count的操作是串行的,而不會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)。

如果忘記解鎖,即一個(gè)線程在獲取一個(gè)鎖后,沒有正確地釋放鎖,而導(dǎo)致其他線程無法獲取該鎖。為了避免這種情況,可以使用以下方法:

  • 使用RAII技術(shù),即將加鎖和解鎖操作封裝在一個(gè)類中,在構(gòu)造函數(shù)中加鎖,在析構(gòu)函數(shù)中解鎖,這樣可以利用對(duì)象的生命周期來自動(dòng)管理鎖的狀態(tài)
  • 使用異常處理機(jī)制,即在加鎖和解鎖操作之間使用·try-catch語句來捕獲可能拋出的異常,并在catch塊中進(jìn)行解鎖操作,這樣可以避免異常導(dǎo)致的忘記解鎖。

理解局部和全局鎖的兩種加鎖方案
除了代碼使用局部鎖的實(shí)現(xiàn)方案外,我們還可以使用靜態(tài)鎖或全局鎖,局部的靜態(tài)鎖還是需要將鎖的地址傳給線程函數(shù),否則線程函數(shù)無法使用鎖,因?yàn)殒i是局部的嘛!如果是全局鎖,那就不需要將其地址傳給線程函數(shù)了,因?yàn)榫€程函數(shù)可以直接看到這把鎖,所以直接使用即可。

2.2 理解鎖的本質(zhì)

【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型
我們知道,共享資源在被多線程訪問時(shí),是不安全的,所以我們需要加鎖來保護(hù)共享資源。但是我們回過頭來想一想,鎖本身是不是共享資源呢?所有的線程都需要申請(qǐng)鎖和釋放鎖,那不就是在共同的訪問鎖這個(gè)資源嘛?所以鎖本身不就是共享資源嗎?那多個(gè)線程在訪問鎖這個(gè)共享資源的時(shí)候,鎖本身是不是需要被保護(hù)呢?當(dāng)然需要!其他的共享資源可以通過加鎖來進(jìn)行保護(hù),那鎖怎么辦呢?
實(shí)際上,加鎖和解鎖的過程是原子的!也就是說只要你申請(qǐng)了鎖,并且競(jìng)爭(zhēng)能力恰好足夠,那么你就一定能夠拿到這個(gè)鎖,否則你就不會(huì)拿到這個(gè)鎖,不會(huì)說在申請(qǐng)鎖申請(qǐng)一半的時(shí)候,線程被切換下去了,其他線程去申請(qǐng)鎖了,不會(huì)出現(xiàn)這種中間態(tài)的情況!既然加鎖和解鎖的過程是原子的,那其實(shí)訪問鎖就是安全的!

那如果一個(gè)線程申請(qǐng)鎖要是沒成功呢?或者說暫時(shí)申請(qǐng)不到鎖呢?執(zhí)行流又會(huì)怎么樣呢?
我們通過ps -axj命令可以查看其實(shí)這個(gè)線程 會(huì)變成Sl+狀態(tài),也就是阻塞狀態(tài),而不是R運(yùn)行狀態(tài)!

【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型
所以如果申請(qǐng)不到鎖,執(zhí)行流就會(huì)阻塞。
因?yàn)槟憔€程申請(qǐng)鎖的時(shí)候,鎖被別的線程拿走了,那你自然就無法申請(qǐng)到鎖,操作系統(tǒng)會(huì)將這樣的線程暫時(shí)處于休眠狀態(tài)。只有當(dāng)持有鎖的線程釋放鎖的時(shí)候,操作系統(tǒng)會(huì)執(zhí)行POSIX庫的代碼,重新喚醒休眠的線程,讓這個(gè)線程去競(jìng)爭(zhēng)鎖,如果競(jìng)爭(zhēng)到,那就持有鎖繼續(xù)向后運(yùn)行,如果競(jìng)爭(zhēng)不到,那就繼續(xù)休眠

先看看死鎖
如果線程函數(shù)內(nèi)部申請(qǐng)了兩次互斥鎖,這實(shí)際就會(huì)出問題了,我們可以看到代碼不會(huì)繼續(xù)運(yùn)行了。

那為什么會(huì)出問題呢?實(shí)際是因?yàn)?,?dāng)前線程已經(jīng)申請(qǐng)到鎖了,但是他又去申請(qǐng)鎖了,而這個(gè)鎖其實(shí)他自己正持有著呢,但是他又不知道自己持有鎖,因?yàn)槲覀冎饔^讓線程執(zhí)行了兩次申請(qǐng)鎖的語句,是我們讓他這么干的,他自己拿著鎖,然后他現(xiàn)在又要去申請(qǐng)鎖,但鎖實(shí)際已經(jīng)被持有了,那么當(dāng)前線程必然就會(huì)申請(qǐng)鎖失敗,也就是處于休眠狀態(tài),什么時(shí)候他才會(huì)喚醒呢?當(dāng)然是鎖被釋放的時(shí)候!當(dāng)鎖被釋放時(shí),操作系統(tǒng)才會(huì)喚醒當(dāng)前線程,但是鎖會(huì)釋放嗎?當(dāng)然是不會(huì)啦!因?yàn)槟阕约喊焰i拿著,你還等其他線程釋放鎖,人家其他線程又沒有鎖,你自己還運(yùn)行不到pthread_mutex_unlock這段代碼,也就是說你自己又不釋放鎖,你還讓沒有這個(gè)鎖的線程去釋放鎖,這不就是自己把自己給搞阻塞了嗎?這其實(shí)就是產(chǎn)生死鎖了,線程永遠(yuǎn)都無法等待鎖成功釋放,那么這個(gè)線程將永遠(yuǎn)處于阻塞狀態(tài),無法運(yùn)行,同樣其他線程道理也如此!
后面會(huì)詳細(xì)講解死鎖問題

實(shí)際上關(guān)于鎖的使用總結(jié)下來也就一句話,誰持有鎖誰才能進(jìn)入臨界區(qū),你沒有鎖那就只能在臨界區(qū)外面乖乖的阻塞等待,等待鎖被釋放,然后你去競(jìng)爭(zhēng)這把鎖,競(jìng)爭(zhēng)到就拿著鎖進(jìn)入臨界區(qū)執(zhí)行代碼,競(jìng)爭(zhēng)不到就老樣子,繼續(xù)乖乖的在臨界區(qū)外面阻塞等待!

那么!對(duì)于其他未持有鎖的線程而言,實(shí)際有意義的鎖的狀態(tài),無非就兩種!一種是申請(qǐng)鎖前,一種是釋放鎖后!申請(qǐng)鎖前,鎖還沒有被申請(qǐng)到,那么對(duì)于其他未持有鎖的線程來說,當(dāng)然是有意義的。釋放鎖后,鎖此時(shí)處于未被申請(qǐng)到的狀態(tài),那未持有鎖的線程當(dāng)然有可能競(jìng)爭(zhēng)到這把鎖,所以這也是一種有意義的狀態(tài)!
而我們站在未持有鎖的線程角度來看的話,當(dāng)前持有鎖的線程不就是原子的嗎?他們看到的鎖只有在未申請(qǐng)前和持有鎖線程釋放鎖之后這兩種有意義的狀態(tài),那這就是原子的,不會(huì)出現(xiàn)中間態(tài)的情況。
所以,在未來使用鎖的時(shí)候,一定要保證臨界區(qū)的代碼粒度非常小,因?yàn)榧渔i之后,線程會(huì)串行執(zhí)行,如果粒度非常大,那么執(zhí)行這段臨界區(qū)所耗費(fèi)的時(shí)間就越多,整體代碼運(yùn)行的效率自然就會(huì)降下來,因?yàn)槠溆喾桥R界區(qū)是并發(fā)或并行執(zhí)行,而臨界區(qū)是串行,所以整體效率會(huì)由于臨界區(qū)的執(zhí)行效率受較大影響,那么在平常加鎖和解鎖時(shí),我們就要保證臨界區(qū)的粒度較小,為此能夠讓程序整體的運(yùn)行效率依舊保持較高的狀態(tài)!

下面看看硬件層面該如何理解加鎖?

我們談到過單純的i++和++i的語句都不是原子的,因?yàn)檫@樣的語句實(shí)際還要至少對(duì)應(yīng)三條匯編語句,從內(nèi)存中讀取數(shù)據(jù),在寄存器中修改數(shù)據(jù),最后再將修改后的數(shù)據(jù)寫回內(nèi)存,所以++i和i++這樣的語句一定不是原子的,因?yàn)樗趫?zhí)行的時(shí)候是有中間態(tài)的,可能在執(zhí)行一半的時(shí)候由于某些原因被切換下去,這樣就會(huì)停下來。這種非原子性的操作就會(huì)導(dǎo)致數(shù)據(jù)不一致性的問題,也就是前面我們常談的共享資源訪問不安全的問題!隨之而來的解決方案就是我們所說的加鎖,對(duì)共享資源進(jìn)行互斥式的訪問,以保證其安全性。
而加鎖和解鎖的過程實(shí)際也是訪問共享資源鎖的過程,那么加鎖和解鎖是如何保證其訪問鎖的原子性呢?答案是通過一條匯編語句來實(shí)現(xiàn)。
為了實(shí)現(xiàn)互斥鎖的加鎖過程,大多數(shù)CPU架構(gòu)都提供了swap和exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)進(jìn)行交換,因?yàn)橹挥幸粭l匯編指令,保證了其原子性。并且即便是多處理器平臺(tái),訪問內(nèi)存的總線周期也有先后,一個(gè)處理器上的交換指令執(zhí)行時(shí),另一個(gè)處理器的交換指令只能等待總線周期就緒后才能訪問。

實(shí)際上除我們語言所說的一條匯編語句交換數(shù)據(jù),而保證的原子性外,在操作系統(tǒng)內(nèi)還有另一種硬件層面上的實(shí)現(xiàn)原子性的簡(jiǎn)單做法。因?yàn)榫€程在執(zhí)行過程中,有可能出現(xiàn)線程執(zhí)行一半被切換了,那么線程完成任務(wù)就不是原子的了,所以我們能不能讓線程在執(zhí)行的時(shí)候,壓根就不能被切換,只要你線程上了CPU的賊船就不能下去,必須得等你完全執(zhí)行完代碼之后才可以被切換下去。
至于線程在執(zhí)行一半的時(shí)候被切換走,原因有很多,可能是時(shí)間片到了,來了更高優(yōu)先級(jí)的線程,線程由于訪問某些外設(shè)或自己的原因等等,需要進(jìn)行阻塞等待,這些情況下,都有可能在線程執(zhí)行一半的時(shí)候被切換下去!
所以在系統(tǒng)層面,我們只要禁止一切中斷,對(duì)線程的中斷不做任何響應(yīng),禁止中斷的總線做出任何響應(yīng),關(guān)閉外部中斷以達(dá)到線程不被切換下去的效果,從而實(shí)現(xiàn)訪問共享資源的原子性。
當(dāng)然這樣的方案比較偏底層,算是一個(gè)比較重量級(jí)的方案,在硬件層面實(shí)現(xiàn)這樣的方案的話,成本還是挺高的,除非線程要完成的工作優(yōu)先級(jí)特別高且必須是原子性的,我們才會(huì)這么做,否則一半情況下,不會(huì)采用這樣的方案來實(shí)現(xiàn)原子性。

【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型
在談?wù)摷渔i過程的匯編代碼之前,我們先來談幾個(gè)共識(shí)性的話題,CPU內(nèi)寄存器只有一套,被所有的執(zhí)行流共享,并且CPU內(nèi)寄存器的內(nèi)容是每個(gè)執(zhí)行流都私有的,稱為運(yùn)行時(shí)的上下文??梢钥吹郊渔i的匯編語句就是將0放到al寄存器內(nèi)部,然后就是執(zhí)行只有一條的匯編語句xchgb,將al寄存器的內(nèi)容和物理內(nèi)存單元進(jìn)行數(shù)據(jù)交換,此時(shí)al寄存器內(nèi)容就會(huì)變?yōu)?,物理內(nèi)存中的mutex互斥量的值變?yōu)?,將物理內(nèi)存中mutex的1和al寄存器內(nèi)0進(jìn)行交換,我們可以形象化的表示為線程A把鎖拿走了,在拿走鎖之后,線程A有沒有可能被切換走呢?當(dāng)然有可能,但線程A在切換的時(shí)候,他是帶著自己的上下文數(shù)據(jù)被切換走的。
此時(shí)線程B被重新調(diào)度上來后,他也會(huì)先將0加載到自己上下文中的al寄存器內(nèi)部,然后再執(zhí)行xchgb匯編語句,但此時(shí)物理內(nèi)存的mutex是0,代表鎖已經(jīng)被申請(qǐng)了,所以交換以后,al寄存器內(nèi)部的值依舊是0,繼續(xù)判斷之后會(huì)進(jìn)入else分支語句,該線程就會(huì)由于等待鎖被持有鎖的線程釋放而處于掛起等待的狀態(tài)。
所以,只要線程A申請(qǐng)鎖成功了,即使線程A的運(yùn)行被中斷了,我們也不擔(dān)心,因?yàn)榻粨Q寄存器和內(nèi)存的匯編語句只有一條,這能保證加鎖過程,也就是申請(qǐng)鎖過程的原子性。并且在線程A被切走時(shí),線程A是持有鎖被切走的,那么即使其他線程此時(shí)被調(diào)度上來,他們也一定無法申請(qǐng)到鎖,那就必須進(jìn)行阻塞等待!只有重新調(diào)度線程A,將線程A的上下文加載到寄存器內(nèi)部,此時(shí)al內(nèi)容就會(huì)變?yōu)?,則返回return 0代表申請(qǐng)鎖成功,線程A就可以持有鎖式的訪問臨界區(qū)。

上面說的加鎖過程是原子的,交換寄存器和mutex內(nèi)容僅由一條匯編語句來完成,而mutex是我們所說的共享資源,所以一條匯編語句保證了mutex操作的原子性。
而解鎖的過程也非常簡(jiǎn)單,直接將1mov到mutex里面就完成了釋放鎖的過程,然后喚醒阻塞等待鎖的線程,讓他們現(xiàn)在去競(jìng)爭(zhēng)鎖,因?yàn)殒i已經(jīng)被釋放了,所以同樣的,釋放鎖的匯編語句也只有一條,這也能保證釋放鎖過程的原子性!

3.RAII風(fēng)格的封裝鎖

我們可以先定義一個(gè)互斥量的類,類中實(shí)現(xiàn)構(gòu)造函數(shù)將鎖的地址進(jìn)行初始化,然后定義出加鎖和解鎖的兩個(gè)接口,這樣就可以定義出來一個(gè)內(nèi)部能夠進(jìn)行加鎖和解鎖的類。
然后我們?cè)偌右粚臃庋b,實(shí)現(xiàn)出RAII( Resource Acquisition Is Initialization)風(fēng)格的加鎖,即為構(gòu)造函數(shù)處進(jìn)行加鎖,析構(gòu)函數(shù)處進(jìn)行解鎖!
至于鎖的初始化和銷毀方案,是類外面的事情,使用時(shí)需要自己先初始化好一把鎖,確定初始化和銷毀的方案,然后利用Mutex.hpp這個(gè)小組件來進(jìn)行加鎖和解鎖的過程!
利用對(duì)象的生命周期是隨代碼塊兒的特性,當(dāng)對(duì)象離開代碼塊兒的時(shí)候,會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)銷毀鎖,這樣對(duì)我們就方便了很多!

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex // 自己不維護(hù)鎖,有外部傳入
{
public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}
    void lock()
    {
    pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
      pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *_pmutex;
};

class LockGuard // 自己不維護(hù)鎖,有外部傳入
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)//指針構(gòu)造Mutex類對(duì)象
    {
        _mutex.lock(); //構(gòu)造函數(shù)里加鎖了
    }
    ~LockGuard()
    {
        _mutex.unlock();//解鎖
    }
private:
    Mutex _mutex;
};

實(shí)際上std::lock_guard就是C++11標(biāo)準(zhǔn)庫提供的一個(gè)RAII風(fēng)格的互斥鎖包裝器,它在構(gòu)造時(shí)接收一個(gè)互斥鎖并嘗試加鎖,在析構(gòu)時(shí)釋放互斥鎖。

4.死鎖

死鎖是指一個(gè)進(jìn)程中的各個(gè)線程,都持有著鎖,但同時(shí)又去申請(qǐng)其他線程的鎖,而每個(gè)線程持有的鎖都是占有不會(huì)釋放的,所以大家都會(huì)等著,等對(duì)方先釋放鎖,但是呢,大家又都不釋放鎖,全都占有著鎖,所以大家就會(huì)處于一種永久等待的狀態(tài),也就是永久性的阻塞狀態(tài),所有執(zhí)行流都不會(huì)被運(yùn)行,這樣的問題就是死鎖!
之前搶票的代碼中,多個(gè)線程使用的是同一把鎖,未來有些場(chǎng)景一定是要使用多把鎖的,在多把鎖的情況下,如果某些線程持有鎖不釋放,還要去申請(qǐng)其他線程正持有的鎖,而每個(gè)線程都是這樣的狀態(tài),那就是死鎖問題。

產(chǎn)生死鎖的四個(gè)必要條件

  1. 互斥條件:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用,互斥其實(shí)就是加鎖之后線程的串行執(zhí)行。
  2. 請(qǐng)求與保持條件:一個(gè)執(zhí)行流由于請(qǐng)求資源而阻塞時(shí),對(duì)自己已經(jīng)獲得的資源保持不放。說白了就是我自己的東西不釋放,我還要你的東西,你不給我就一直等,等到你給我為止。
  3. 不剝奪條件:一個(gè)線程在未使用完自己獲得的資源之前,是不能夠強(qiáng)行剝奪其他線程的資源的。說白了就是你先在還有資源呢,你想要?jiǎng)e人的自由你就得等,不能強(qiáng)行剝奪!當(dāng)你使用完自己的資源后,你可以去等待申請(qǐng)別人的資源??傊褪遣荒軓?qiáng)行剝奪其他線程的資源,想要就必須阻塞等待別人釋放資源才可以。
  4. 循環(huán)等待條件:若干個(gè)執(zhí)行流之間,形成一種頭尾相接的互相等待對(duì)方資源的關(guān)系。我們也稱這樣的現(xiàn)象為環(huán)路等待。

如何避免死鎖? 核心思想:破壞死鎖的4個(gè)必要條件的任意一個(gè)!
【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型

二、線程同步

1. 問題引入

我們可以舉一個(gè)例子來理解條件變量是如何實(shí)現(xiàn)線程同步的。
假設(shè)現(xiàn)在學(xué)校開了一間學(xué)霸vip自習(xí)室,學(xué)校規(guī)定這間自習(xí)室一次只能進(jìn)去一個(gè)人上自習(xí),自習(xí)室門口掛著一把鑰匙,誰來的早先拿到這把鑰匙,就可以打開門進(jìn)入自習(xí)室學(xué)習(xí),并且進(jìn)入自習(xí)室之后,把門一反鎖,其他人誰都不能進(jìn)來。然后你第二天準(zhǔn)備去學(xué)習(xí)了,卷的不行,直接凌晨三點(diǎn)就跑過來,拿著鑰匙進(jìn)入自習(xí)室上自習(xí)了,然后卷了3小時(shí)之后,你想出來上個(gè)廁所,一打開門發(fā)現(xiàn)外面站的一堆人,都在嘰嘰喳喳的討論誰先來的,怎么來的這么早?這么卷?然后你怕自己等會(huì)兒把鑰匙放到墻上之后,上完廁所回來之后有人拿著鑰匙進(jìn)入了自習(xí)室,你就又卷不了了,所以你把鑰匙揣兜里,拿著鑰匙去上廁所了,其他人當(dāng)然進(jìn)入不了自習(xí)室,因?yàn)槟隳弥€匙去上廁所了。等你回來的時(shí)候,你又打開門,又來里面上了3小時(shí)自習(xí),你感覺自己餓的不行了,在不吃飯就餓死在里面了,所以你打開門,準(zhǔn)備出去吃飯了,然后突然你自己感覺負(fù)罪感直接拉滿,我凌晨3點(diǎn)好不容易搶到自習(xí)室,現(xiàn)在離開是不太虧了,所以你又打開自習(xí)室回去上自習(xí)去了,別人當(dāng)然競(jìng)爭(zhēng)不過你呀!因?yàn)殍€匙一直都在你兜里,你出來之后把鑰匙放到墻上,你發(fā)現(xiàn)有點(diǎn)負(fù)罪感,你又拿起來鑰匙回去上自習(xí),因?yàn)槟汶x鑰匙最近,所以你的競(jìng)爭(zhēng)能力最強(qiáng)。結(jié)果你來自習(xí)室上了1分鐘自習(xí)又出來了,然后又負(fù)罪的不行,又回去了,周而復(fù)始的這么干,結(jié)果別人連自習(xí)室長(zhǎng)啥樣都沒見到。
像這樣由于長(zhǎng)時(shí)間無法得到鎖的線程,沒辦法進(jìn)入臨界區(qū)訪問臨界資源,我們稱這樣的線程處于饑餓狀態(tài)!

所以學(xué)校推出了新政策,所有剛剛從自習(xí)室出來的人,都必須回到隊(duì)列的尾部重新排隊(duì)等待進(jìn)入自習(xí)室,這樣的話,其他人也就可以拿到鑰匙進(jìn)入自習(xí)室了。
所以,在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序來訪問臨界資源,從而有效避免其他線程的饑餓問題,這就叫做線程同步!

2. 條件變量

為了能夠讓多線程協(xié)同工作,就需要實(shí)現(xiàn)多線程的同步關(guān)系,為了維護(hù)同步關(guān)系,就需要引入條件變量。那條件變量是一個(gè)什么東西呢?他其實(shí)和互斥鎖一樣,都是一個(gè)數(shù)據(jù)類型定義出來的對(duì)象。初始化和銷毀方案和互斥鎖一模一樣。唯一不同的是,條件變量在使用時(shí)有兩個(gè)高頻使用的接口,一個(gè)是pthread_cond_wait,該函數(shù)的作用是將等待某一個(gè)具體鎖的線程放入條件變量的等待隊(duì)列中進(jìn)行等待,另一個(gè)是pthread_cond_signal,該函數(shù)的作用是喚醒條件變量中等待隊(duì)列的第一個(gè)等待線程,另一個(gè)用的不怎么高頻,但也偶爾會(huì)用一下的接口就是pthread_cond_broadcast,該函數(shù)將條件變量中的所有等待線程都會(huì)喚醒,讓所有線程重新回歸競(jìng)爭(zhēng)鎖的狀態(tài)。而不是像signal那樣,喚醒cond隊(duì)列中任意一個(gè)阻塞等待鎖的線程
【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型

條件變量的功能是阻塞線程,直到某個(gè)特定條件為真為止。條件變量始終與互斥鎖一起使用,對(duì)條件的測(cè)試是在互斥鎖(互斥)的保護(hù)下進(jìn)行的。如果條件為假,線程通常會(huì)基于條件變量阻塞,并以原子方式釋放等待條件變化的互斥鎖。

條件變量的類型和初始化

在Linux中,條件變量用pthread_cond_t類型的變量表示,此類型定義在<pthread.h>頭文件中。例如:

#include <pthread.h>
pthread_cond_t cond; // 創(chuàng)建一個(gè)條件變量

要想使用cond條件變量,還需要進(jìn)行初始化操作。初始化條件變量的方式有兩種,一種是直接將PTHREAD_COND_INITIALIZER賦值給條件變量,例如:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 靜態(tài)初始化

還可以借助pthread_cond_init()函數(shù)初始化條件變量,語法格式如下:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); // 動(dòng)態(tài)初始化
// 成功返回0,失敗返回錯(cuò)誤碼

參數(shù)cond用于指明要初始化的條件變量;參數(shù)attr用于自定義條件變量的屬性,通常我們將它賦值為NULL,表示以系統(tǒng)默認(rèn)的屬性完成初始化操作。

當(dāng)attr參數(shù)為NULL時(shí),以上兩種初始化方式完全等價(jià)。

條件變量的使用

等待和喚醒

使用條件變量可以分為兩部分:等待線程和激活線程。

等待線程

有兩種等待方式,無條件等待pthread_cond_wait()和計(jì)時(shí)等待pthread_cond_timedwait()。接口為:

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);
// 成功返回0,失敗返回錯(cuò)誤碼

cond參數(shù)表示已初始化好的條件變量;mutex參數(shù)表示與條件變量配合使用的互斥鎖;abstime參數(shù)表示阻塞線程的時(shí)間。

注意,abstime參數(shù)指的是絕對(duì)時(shí)間,例如您打算阻塞線程5秒鐘,那么首先要得到當(dāng)前系統(tǒng)的時(shí)間,然后再加上5秒,最終得到的時(shí)間才是傳遞的實(shí)參值。

無論哪種等待方式,都必須和一個(gè)互斥鎖配合,以防止多個(gè)線程同時(shí)請(qǐng)求pthread_cond_wait()(或pthread_cond_timedwait())的競(jìng)爭(zhēng)條件(Race Condition)。mutex互斥鎖必須是普通鎖或者適應(yīng)鎖,并在調(diào)用pthread_cond_wait()前必須由本線程加鎖,而在更新條件等待隊(duì)列以前,mutex保持鎖定狀態(tài),并在線程掛起進(jìn)入等待前解鎖。在條件滿足從而離開pthread_cond_wait()之前,mutex將被重新加鎖,以與進(jìn)入pthread_cond_wait()前的加鎖動(dòng)作對(duì)應(yīng)。

pthread_cond_wait()函數(shù)的返回并不意味著條件的值一定發(fā)生了變化,必須重新檢查條件的值。阻塞在條件變量上的線程被喚醒以后,直到pthread_cond_wait()函數(shù)返回之前條件的值都有可能發(fā)生變化。所以函數(shù)返回以后,在鎖定相應(yīng)的互斥鎖之前,必須重新測(cè)試條件值。最好的測(cè)試方法是循環(huán)調(diào)用pthread_cond_wait函數(shù),并把滿足條件的表達(dá)式置為循環(huán)的終止條件。如:

pthread_mutex_lock(&mutex); // 加鎖
while (condition_is_false) // 循環(huán)檢查條件
    pthread_cond_wait(&cond, &mutex); // 等待條件成立
pthread_mutex_unlock(&mutex); // 解鎖

阻塞在同一個(gè)條件變量上的不同線程被釋放的次序是不一定的。

注意:pthread_cond_wait()函數(shù)是退出點(diǎn),如果在調(diào)用這個(gè)函數(shù)時(shí),已有一個(gè)掛起的退出請(qǐng)求,且線程允許退出,這個(gè)線程將被終止并開始執(zhí)行善后處理函數(shù),而這時(shí)和條件變量相關(guān)的互斥鎖仍將處在鎖定狀態(tài)。

激活線程 – 廣播

喚醒條件有兩種形式,pthread_cond_signal()喚醒一個(gè)等待該條件的線程,存在多個(gè)等待線程時(shí)按入隊(duì)順序喚醒其中一個(gè);而pthread_cond_broadcast()則喚醒所有等待線程。

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 成功返回0,失敗返回錯(cuò)誤碼

cond參數(shù)表示初始化好的條件變量。

必須在互斥鎖的保護(hù)下使用相應(yīng)的條件變量。否則對(duì)條件變量的解鎖有可能發(fā)生在鎖定條件變量之前,從而造成死鎖。

喚醒阻塞在條件變量上的所有線程的順序由調(diào)度策略決定,如果線程的調(diào)度策略是SCHED_OTHER類型的,系統(tǒng)將根據(jù)線程的優(yōu)先級(jí)喚醒線程。

如果沒有線程被阻塞在條件變量上,那么調(diào)用pthread_cond_signal()將沒有作用。

由于pthread_cond_broadcast函數(shù)喚醒所有阻塞在某個(gè)條件變量上的線程,這些線程被喚醒后將再次競(jìng)爭(zhēng)相應(yīng)的互斥鎖,所以必須小心使用pthread_cond_broadcast函數(shù)。

銷毀

對(duì)于初始化好的條件變量,我們可以調(diào)用pthread_cond_destroy()函數(shù)銷毀它。只有在沒有線程在該條件變量上等待的時(shí)候才能銷毀這個(gè)條件變量,否則返回EBUSY。因?yàn)長(zhǎng)inux實(shí)現(xiàn)的條件變量沒有分配什么資源,所以注銷動(dòng)作只包括檢查是否有等待線程。

int pthread_cond_destroy(pthread_cond_t *cond); // 成功返回0,失敗返回錯(cuò)誤碼

下面是一個(gè)使用條件變量實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模型(后面講解)的示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

#define MAX 10 // 緩沖區(qū)大小

// 緩沖區(qū)結(jié)構(gòu)體
typedef struct {
    int buffer[MAX]; // 存放數(shù)據(jù)
    int len; // 當(dāng)前數(shù)據(jù)長(zhǎng)度
    pthread_mutex_t mutex; // 互斥鎖
    pthread_cond_t not_full; // 條件變量:緩沖區(qū)未滿
    pthread_cond_t not_empty; // 條件變量:緩沖區(qū)非空
} pool_t;

// 初始化緩沖區(qū)
void pool_init(pool_t *pool) {
    pool->len = 0; // 初始長(zhǎng)度為0
    pthread_mutex_init(&pool->mutex, NULL); // 初始化互斥鎖
    pthread_cond_init(&pool->not_full, NULL); // 初始化條件變量
    pthread_cond_init(&pool->not_empty, NULL);
}

// 銷毀緩沖區(qū)
void pool_destroy(pool_t *pool) {
    pthread_mutex_destroy(&pool->mutex); // 銷毀互斥鎖
    pthread_cond_destroy(&pool->not_full); // 銷毀條件變量
    pthread_cond_destroy(&pool->not_empty);
}

// 生產(chǎn)者線程函數(shù)
void *producer(void *arg) {
    pool_t *pool = (pool_t *)arg; // 獲取緩沖區(qū)指針
    int i;
    for (i = 0; i < 20; i++) { // 循環(huán)生產(chǎn)20個(gè)數(shù)據(jù)
        pthread_mutex_lock(&pool->mutex); // 加鎖
        while (pool->len == MAX) { // 如果緩沖區(qū)已滿,等待條件變量
            printf("producer is waiting...\n");
            pthread_cond_wait(&pool->not_full, &pool->mutex);
        }
        pool->buffer[pool->len] = i; // 向緩沖區(qū)中添加數(shù)據(jù)
        pool->len++; // 長(zhǎng)度加一
        printf("producer produces %d\n", i);
        pthread_cond_signal(&pool->not_empty); // 發(fā)送條件變量信號(hào),通知消費(fèi)者
        pthread_mutex_unlock(&pool->mutex); // 解鎖
        sleep(1); // 模擬生產(chǎn)時(shí)間
    }
}

// 消費(fèi)者線程函數(shù)
void *consumer(void *arg) {
    pool_t *pool = (pool_t *)arg; // 獲取緩沖區(qū)指針
    int data;
    while (1) { // 循環(huán)消費(fèi)數(shù)據(jù)
        pthread_mutex_lock(&pool->mutex); // 加鎖
        while (pool->len == 0) { // 如果緩沖區(qū)為空,等待條件變量
            printf("consumer is waiting...\n");
            pthread_cond_wait(&pool->not_empty, &pool->mutex);
        }
        data = pool->buffer[pool->len - 1]; // 從緩沖區(qū)中取出數(shù)據(jù)
        pool->len--; // 長(zhǎng)度減一
        printf("consumer consumes %d\n", data);
        pthread_cond_signal(&pool->not_full); // 發(fā)送條件變量信號(hào),通知生產(chǎn)者
        pthread_mutex_unlock(&pool->mutex); // 解鎖
        sleep(2); // 模擬消費(fèi)時(shí)間
    }
}

int main() {
    pool_t pool; // 創(chuàng)建一個(gè)緩沖區(qū)對(duì)象
    pool_init(&pool); // 初始化緩沖區(qū)

    pthread_t tid1, tid2; // 創(chuàng)建兩個(gè)線程ID

    pthread_create(&tid1, NULL, producer, &pool); // 創(chuàng)建生產(chǎn)者線程,傳入緩沖區(qū)指針作為參數(shù)
    pthread_create(&tid2, NULL, consumer, &pool); // 創(chuàng)建消費(fèi)者線程,傳入緩沖區(qū)指針作為參數(shù)

    pthread_join(tid1, NULL); // 等待生產(chǎn)者線程結(jié)束
    pthread_join(tid2, NULL); // 等待消費(fèi)者線程結(jié)束

    pool_destroy(&pool); // 銷毀緩沖區(qū)

    return 0;
}

3.生產(chǎn)消費(fèi)模型的概念理解 — 321原則

先來理解關(guān)于串行、并發(fā)、并行的概念:
【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型
實(shí)際我們的計(jì)算機(jī)在工作時(shí),是一定要進(jìn)行并發(fā)的,因?yàn)椴l(fā)能很好解決用戶同時(shí)想要運(yùn)行多個(gè)程序的需求,也就是我們所說的多任務(wù)處理,但同時(shí)也需要進(jìn)行并行。就比如上面圖中舉得例子,每個(gè)大核跑不同的程序,但同時(shí)某一個(gè)大核在跑程序時(shí),也可以時(shí)間片輪轉(zhuǎn)的去執(zhí)行另一個(gè)程序,所以并行和并發(fā)在計(jì)算中是同時(shí)存在的。
而并發(fā)一定要比并行效率高的前提是多任務(wù)情況,如果你站在多任務(wù)處理的角度去看待串行和并發(fā),你一定可以理解為什么并發(fā)效率要更高,因?yàn)榇性诰€程被切換下去或者等鎖被釋放的時(shí)候,這段時(shí)間CPU什么都做不了,那這段時(shí)間就會(huì)被白白浪費(fèi)掉,在多任務(wù)處理的情況下,效率一定就會(huì)下降。而對(duì)于并發(fā)來講,如果某個(gè)線程被切換下去或者他在等待鎖被釋放的時(shí)候,是完全沒有關(guān)系的,因?yàn)镃PU會(huì)調(diào)度運(yùn)行其他線程,所以被切換下去的線程在等待的時(shí)候,時(shí)間完全不會(huì)被浪費(fèi)掉,而是會(huì)被CPU利用起來去跑其他的線程。

再看看生產(chǎn)消費(fèi)模型的概念

實(shí)際生活中,我們作為消費(fèi)者,一般都會(huì)去超市這樣的地方去購(gòu)買產(chǎn)品,而不是去生產(chǎn)者那里購(gòu)買產(chǎn)品,因?yàn)楣┴浬桃话悴涣闶郛a(chǎn)品,他們都會(huì)統(tǒng)一將大量的商品供貨到超市,然后我們消費(fèi)者從超市這樣的交易場(chǎng)所中購(gòu)買產(chǎn)品。
而當(dāng)我們?cè)谫?gòu)買產(chǎn)品的時(shí)候,生產(chǎn)者在做什么呢?生產(chǎn)者可能正在生產(chǎn)商品呢,或者正在放假呢,也可能正在干著別的事情,所以生產(chǎn)和消費(fèi)的過程互相并不怎么影響,這就實(shí)現(xiàn)了生產(chǎn)者和消費(fèi)者之間的解耦。
而超市充當(dāng)著一個(gè)什么樣的角色呢?比如當(dāng)放假期間,消費(fèi)爆棚的季節(jié)中,來超市購(gòu)買東西的人就會(huì)非常的多,所以就容易出現(xiàn)供不應(yīng)求的情況,但超市一般也會(huì)有對(duì)策,因?yàn)槌械膫}(cāng)庫中都會(huì)預(yù)先屯一批貨,所以在消費(fèi)爆棚的時(shí)間段內(nèi),超市也不用擔(dān)心沒有貨賣的情況。而當(dāng)工作期間,大家由于忙著通過勞動(dòng)來換取報(bào)酬,可能來消費(fèi)的人就會(huì)比較少,商品流量也會(huì)比較低,那此時(shí)供貨商如果還是給超市供大量的貨呢?雖然超市可能最近確實(shí)賣不出去東西,但是超市還是可以把供貨商的商品先存儲(chǔ)到倉(cāng)庫中,以備在消費(fèi)爆棚的季節(jié)時(shí),能夠應(yīng)對(duì)大量消費(fèi)的場(chǎng)景。所以超市其實(shí)就是充當(dāng)一個(gè)緩沖區(qū)的角色,在計(jì)算機(jī)中充當(dāng)?shù)木褪菙?shù)據(jù)緩沖區(qū)的角色。
而計(jì)算機(jī)中哪些場(chǎng)景是強(qiáng)耦合的呢?其實(shí)函數(shù)調(diào)用就是強(qiáng)耦合的一個(gè)場(chǎng)景,例如當(dāng)main調(diào)用func的時(shí)候,func在執(zhí)行代碼的時(shí)候,main在做什么呢?main什么都做不了,他只能等待func調(diào)用完畢返回之后,main才能繼續(xù)向后執(zhí)行代碼,所以我們稱main和func之間就是一種強(qiáng)耦合的關(guān)系,而上面所說的生產(chǎn)者和消費(fèi)者并不是一種強(qiáng)耦合的關(guān)系。
【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型
所以為什么要有超市? 核心理由:效率高 ! 忙閑不均 — 允許生產(chǎn)消費(fèi)的步調(diào)不一致
超市其實(shí)就是典型的共享資源,因?yàn)樯a(chǎn)者和消費(fèi)者都要訪問超市,所以對(duì)于超市這個(gè)共享資源,他在被訪問的時(shí)候,也是需要被保護(hù)起來的,而保護(hù)其實(shí)就是通過加鎖來實(shí)現(xiàn)互斥式的訪問共享資源,從而保證安全性。
在只有一份超市共享資源的情況下,生產(chǎn)和生產(chǎn),消費(fèi)和消費(fèi),以及生產(chǎn)和消費(fèi)都需要進(jìn)行串行的訪問共享資源。但為了提高效率我們搞出了同步這樣的關(guān)系,因?yàn)橛锌赡芟M(fèi)者一直霸占著鎖,一直在那里消費(fèi),但實(shí)際超市已經(jīng)沒有物資了,此時(shí)消費(fèi)者由于競(jìng)爭(zhēng)能力過強(qiáng),也會(huì)造成不合理的問題,因?yàn)橄M(fèi)者消費(fèi)過多之后,應(yīng)該輪到生產(chǎn)者來生產(chǎn)了,所以對(duì)于生產(chǎn)者和消費(fèi)者之間僅僅只有互斥關(guān)系是不夠的,還需要有同步關(guān)系。

我們可以從生產(chǎn)消費(fèi)模型中可以提取出來一個(gè)321原則幫助記憶。即為3種關(guān)系,兩個(gè)角色,1個(gè)交易場(chǎng)所。對(duì)應(yīng)的其實(shí)是消費(fèi)線程和消費(fèi)線程的關(guān)系,消費(fèi)線程和生產(chǎn)線程的關(guān)系,生產(chǎn)線程和生產(chǎn)線程的關(guān)系,交易場(chǎng)所就是阻塞隊(duì)列blockqueue。而實(shí)現(xiàn)線程同步就需要一個(gè)條件變量,比如生產(chǎn)者生產(chǎn)完之后,超市給消費(fèi)者打個(gè)電話,讓消費(fèi)者過來消費(fèi),消費(fèi)完之后,超市在給生產(chǎn)者打個(gè)電話,讓生產(chǎn)者來生產(chǎn),這樣就不會(huì)存在由于某一個(gè)線程競(jìng)爭(zhēng)能力過強(qiáng),一直生產(chǎn)或一直消費(fèi)的情況產(chǎn)生,從而導(dǎo)致其他線程饑餓的問題。
【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型

實(shí)際上只要我們想寫生產(chǎn)消費(fèi)模型,本質(zhì)上做的工作就是在維護(hù)”321“原則嘛!

生產(chǎn)消費(fèi)模型都有哪些好處:
a.他實(shí)現(xiàn)了生產(chǎn)和消費(fèi)的解耦,使他們之間并不互相影響。
b.支持生產(chǎn)和消費(fèi)一段時(shí)間的忙閑不均的問題。因?yàn)榫彌_區(qū)可以預(yù)留一部分?jǐn)?shù)據(jù),進(jìn)行數(shù)據(jù)的緩沖。
c.由于生產(chǎn)和消費(fèi)的互斥與同步關(guān)系,提升了生產(chǎn)消費(fèi)模型的效率。文章來源地址http://www.zghlxwxcb.cn/news/detail-477651.html

到了這里,關(guān)于【Linux】多線程02 --- 線程的同步互斥問題及生產(chǎn)消費(fèi)模型的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 【Linux】詳解線程第三篇——線程同步和生產(chǎn)消費(fèi)者模型

    【Linux】詳解線程第三篇——線程同步和生產(chǎn)消費(fèi)者模型

    本篇線程同步的內(nèi)容是完全基于線程互斥來講的,如果屏幕前的你對(duì)于線程互斥還不是很了解的話,可以看看我上一篇博客:【Linux】詳解線程第二篇——用黃牛搶陳奕迅演唱會(huì)門票的例子來講解【 線程互斥與鎖 】 上篇線程互斥中重點(diǎn)講了互斥鎖,雖然解決了多線程并發(fā)導(dǎo)

    2024年02月07日
    瀏覽(23)
  • 【linux】線程同步+基于BlockingQueue的生產(chǎn)者消費(fèi)者模型

    【linux】線程同步+基于BlockingQueue的生產(chǎn)者消費(fèi)者模型

    喜歡的點(diǎn)贊,收藏,關(guān)注一下把! 在線程互斥寫了一份搶票的代碼,我們發(fā)現(xiàn)雖然加鎖解決了搶到負(fù)數(shù)票的問題,但是一直都是一個(gè)線程在搶票,它錯(cuò)了嗎,它沒錯(cuò)但是不合理。那我們應(yīng)該如何安全合理的搶票呢? 講個(gè)小故事。 假設(shè)學(xué)校有一個(gè)VIP學(xué)霸自習(xí)室,這個(gè)自習(xí)室有

    2024年02月03日
    瀏覽(24)
  • 【Linux】線程同步 -- 條件變量 | 生產(chǎn)者消費(fèi)者模型 | 自旋鎖 |讀寫鎖

    【Linux】線程同步 -- 條件變量 | 生產(chǎn)者消費(fèi)者模型 | 自旋鎖 |讀寫鎖

    舉一個(gè)例子: 學(xué)生去超市消費(fèi)的時(shí)候,與廠家生產(chǎn)的時(shí)候,兩者互不相沖突。 生產(chǎn)的過程與消費(fèi)的過程 – 解耦 臨時(shí)的保存產(chǎn)品的場(chǎng)所(超時(shí)) – 緩沖區(qū) 模型總結(jié)“321”原則: 3種關(guān)系:生產(chǎn)者和生產(chǎn)者(互斥),消費(fèi)者和消費(fèi)者(互斥),生產(chǎn)者和消費(fèi)者(互斥[保證共享資

    2024年02月14日
    瀏覽(25)
  • 【Linux學(xué)習(xí)】多線程——同步 | 條件變量 | 基于阻塞隊(duì)列的生產(chǎn)者消費(fèi)者模型

    【Linux學(xué)習(xí)】多線程——同步 | 條件變量 | 基于阻塞隊(duì)列的生產(chǎn)者消費(fèi)者模型

    ??作者:一只大喵咪1201 ??專欄:《Linux學(xué)習(xí)》 ??格言: 你只管努力,剩下的交給時(shí)間! 以生活中消費(fèi)者生產(chǎn)者為例: 生活中,我們大部分人都扮演著消費(fèi)者的角色,會(huì)經(jīng)常在超市買東西,比如買方便面,而超市的方便面是由供應(yīng)商生成的。所以我們就是消費(fèi)者,供應(yīng)商

    2024年02月05日
    瀏覽(16)
  • 線程同步--生產(chǎn)者消費(fèi)者模型

    線程同步--生產(chǎn)者消費(fèi)者模型

    條件變量是 線程間共享的全局變量 ,線程間可以通過條件變量進(jìn)行同步控制 條件變量的使用必須依賴于互斥鎖以確保線程安全,線程申請(qǐng)了互斥鎖后,可以調(diào)用特定函數(shù) 進(jìn)入條件變量等待隊(duì)列(同時(shí)釋放互斥鎖) ,其他線程則可以通過條件變量在特定的條件下喚醒該線程( 喚醒后線

    2024年01月19日
    瀏覽(25)
  • 線程同步--生產(chǎn)者消費(fèi)者模型--單例模式線程池

    線程同步--生產(chǎn)者消費(fèi)者模型--單例模式線程池

    條件變量是 線程間共享的全局變量 ,線程間可以通過條件變量進(jìn)行同步控制 條件變量的使用必須依賴于互斥鎖以確保線程安全,線程申請(qǐng)了互斥鎖后,可以調(diào)用特定函數(shù) 進(jìn)入條件變量等待隊(duì)列(同時(shí)釋放互斥鎖) ,其他線程則可以通過條件變量在特定的條件下喚醒該線程( 喚醒后線

    2024年01月20日
    瀏覽(22)
  • 線程同步、生產(chǎn)者消費(fèi)模型和POSIX信號(hào)量

    線程同步、生產(chǎn)者消費(fèi)模型和POSIX信號(hào)量

    gitee倉(cāng)庫: 1.阻塞隊(duì)列代碼:https://gitee.com/WangZihao64/linux/tree/master/BlockQueue 2.環(huán)形隊(duì)列代碼:https://gitee.com/WangZihao64/linux/tree/master/ringqueue 概念 : 利用線程間共享的全局變量進(jìn)行同步的一種機(jī)制,主要包括兩個(gè)動(dòng)作:一個(gè)線程等待\\\"條件變量的條件成立\\\"而掛起;另一個(gè)線程使“

    2024年02月03日
    瀏覽(23)
  • 12.3用信號(hào)量進(jìn)行線程同步——生產(chǎn)者與消費(fèi)者問題

    1.shell程序設(shè)計(jì) 2.內(nèi)存管理 3.鏈接庫 4.文件操作

    2024年02月04日
    瀏覽(26)
  • 【Linux】線程安全-生產(chǎn)者消費(fèi)者模型

    【Linux】線程安全-生產(chǎn)者消費(fèi)者模型

    1個(gè)線程安全的隊(duì)列:只要保證先進(jìn)先出特性的數(shù)據(jù)結(jié)構(gòu)都可以稱為隊(duì)列 這個(gè)隊(duì)列要保證互斥(就是保證當(dāng)前只有一個(gè)線程對(duì)隊(duì)列進(jìn)行操作,其他線程不可以同時(shí)來操作),還要保證同步,當(dāng)生產(chǎn)者將隊(duì)列中填充滿了之后要通知消費(fèi)者來進(jìn)行消費(fèi),消費(fèi)者消費(fèi)之后通知生產(chǎn)者

    2024年02月10日
    瀏覽(25)
  • Linux基于多線程和任務(wù)隊(duì)列實(shí)現(xiàn)生產(chǎn)消費(fèi)模型

    Linux基于多線程和任務(wù)隊(duì)列實(shí)現(xiàn)生產(chǎn)消費(fèi)模型

    目錄 一、生產(chǎn)者消費(fèi)者模型 二、代碼實(shí)現(xiàn)模型 2.1 BlockQueue.hpp 2.2 MainCP.cc 2.3 執(zhí)行結(jié)果 三、效率優(yōu)勢(shì) 將上述圖片邏輯轉(zhuǎn)換成代碼邏輯就是,一批線程充當(dāng)生產(chǎn)者角色,一批線程充當(dāng)消費(fèi)者角色,倉(cāng)庫是生產(chǎn)者和消費(fèi)者獲取的公共資源!下面我想用 321原則 來解釋這個(gè)模型。 既

    2024年02月09日
    瀏覽(23)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包