多線程爲了同個資源打起架來了,該如何讓他們安定?


前言

先來看看虛構的小故事程序員

已經晚上 11 點了,程序員小明的雙手還在鍵盤上飛舞着,眼神依然注視着的電腦屏幕。面試

沒辦法這段時間公司業績增加中,需求天然也多了起來,加班天然也少不了。算法

天氣變化莫測,這時窗外下起了蓬勃大雨,同時閃電轟鳴。數據庫

但這一絲都沒有影響到小明,始料未及,忽然一道巨大的雷一閃而過,辦公樓就這麼停電了,隨後整棟樓都在迴盪着的小明那一聲撕心裂肺的「臥槽」。數組

此時,求小明的內心面積有多大?微信

等小明內心平復後,忽然肚子很是的痛,想上廁所,小明心想確定是晚上吃的某堡王有問題。數據結構

整棟樓都停了電,小明兩眼一抹黑,啥都看不見,只能靠摸牆的方法,一步一步的來到了廁所門口。多線程

到了廁所(共享資源),因爲實在太急,小明直接衝入了廁所裏,用手摸索着恰好第一個門沒鎖門,便奪門而入。併發

這就荒唐了,這個門裏面正好小紅在上着廁所,正好這個廁所門是壞了的,沒辦法鎖門。app

黑暗中,小紅雖然看不見,但靠着聲音,發現本身面前的這扇門有動靜,以爲不對勁,因而鉚足了力氣,用她穿着高跟鞋腳,用力地一腳踢了過去。

小明很幸運,被踢中了「命根子」,撕心裂肺地喊出了一個字「痛」!

故事說完了,扯了那麼多,其實是爲了說明,對於共享資源,若是沒有上鎖,在多線程的環境裏,那麼就可能會發生翻車現場。

接下來,用 30+ 張圖,帶你們走進操做系統中避免多線程資源競爭的互斥、同步的方法。


正文

競爭與協做

在單核 CPU 系統裏,爲了實現多個程序同時運行的假象,操做系統一般以時間片調度的方式,讓每一個進程執行每次執行一個時間片,時間片用完了,就切換下一個進程運行,因爲這個時間片的時間很短,因而就形成了「併發」的現象。

併發併發

另外,操做系統也爲每一個進程建立巨大、私有的虛擬內存的假象,這種地址空間的抽象讓每一個程序好像擁有本身的內存,而實際上操做系統在背後祕密地讓多個地址空間「複用」物理內存或者磁盤。

虛擬內存管理-換入換出虛擬內存管理-換入換出

若是一個程序只有一個執行流程,也表明它是單線程的。固然一個程序能夠有多個執行流程,也就是所謂的多線程程序,線程是調度的基本單位,進程則是資源分配的基本單位。

因此,線程之間是能夠共享進程的資源,好比代碼段、堆空間、數據段、打開的文件等資源,但每一個線程都有本身獨立的棧空間。

多線程多線程

那麼問題就來了,多個線程若是競爭共享資源,若是不採起有效的措施,則會形成共享數據的混亂。

咱們作個小實驗,建立兩個線程,它們分別對共享變量 i 自增 1 執行 10000 次,以下代碼(雖說是 C++ 代碼,可是沒學過 C++ 的同窗也是看到懂的):

按理來講,i 變量最後的值應該是 20000,但很不幸,並非如此。咱們對上面的程序執行一下:

運行了兩次,發現出現了 i 值的結果是 15173,也會出現 20000 的 i 值結果。

每次運行不但會產生錯誤,並且獲得不一樣的結果。在計算機裏是不能容忍的,雖然是小几率出現的錯誤,可是小几率事件它必定是會發生的,「墨菲定律」你們都懂吧。

爲何會發生這種狀況?

爲了理解爲何會發生這種狀況,咱們必須瞭解編譯器爲更新計數器 i 變量生成的代碼序列,也就是要了解彙編指令的執行順序。

在這個例子中,咱們只是想給 i 加上數字 1,那麼它對應的彙編指令執行過程是這樣的:

能夠發現,只是單純給 i 加上數字 1,在 CPU 運行的時候,實際上要執行 3 條指令。

設想咱們的線程 1 進入這個代碼區域,它將 i 的值(假設此時是 50 )從內存加載到它的寄存器中,而後它向寄存器加 1,此時在寄存器中的 i 值是 51。

如今,一件不幸的事情發生了:時鐘中斷髮生。所以,操做系統將當前正在運行的線程的狀態保存到線程的線程控制塊 TCP。

如今更糟的事情發生了,線程 2 被調度運行,並進入同一段代碼。它也執行了第一條指令,從內存獲取 i 值並將其放入到寄存器中,此時內存中 i 的值仍爲 50,所以線程 2 寄存器中的 i 值也是 50。假設線程 2 執行接下來的兩條指令,將寄存器中的 i 值 + 1,而後將寄存器中的 i 值保存到內存中,因而此時全局變量 i 值是 51。

最後,又發生一次上下文切換,線程 1 恢復執行。還記得它已經執行了兩條彙編指令,如今準備執行最後一條指令。回憶一下, 線程 1 寄存器中的 i 值是51,所以,執行最後一條指令後,將值保存到內存,全局變量 i 的值再次被設置爲 51。

簡單來講,增長 i (值爲 50 )的代碼被運行兩次,按理來講,最後的 i 值應該是 52,可是因爲不可控的調度,致使最後 i 值倒是 51。

針對上面線程 1 和線程 2 的執行過程,我畫了一張流程圖,會更明確一些:

藍色表示線程 1 ,紅色表示線程 2藍色表示線程 1 ,紅色表示線程 2

互斥的概念

上面展現的狀況稱爲競爭條件(race condition,當多線程相互競爭操做共享變量時,因爲運氣很差,即在執行過程當中發生了上下文切換,咱們獲得了錯誤的結果,事實上,每次運行均可能獲得不一樣的結果,所以輸出的結果存在不肯定性(indeterminate

因爲多線程執行操做共享變量的這段代碼可能會致使競爭狀態,所以咱們將此段代碼稱爲臨界區(critical section),它是訪問共享資源的代碼片斷,必定不能給多線程同時執行。

咱們但願這段代碼是互斥(mutualexclusion)的,也就說保證一個線程在臨界區執行時,其餘線程應該被阻止進入臨界區,說白了,就是這段代碼執行過程當中,最多隻能出現一個線程。

互斥互斥

另外,說一下互斥也並非只針對多線程。在多進程競爭共享資源的時候,也一樣是可使用互斥的方式來避免資源競爭形成的資源混亂。

同步的概念

互斥解決了併發進程/線程對臨界區的使用問題。這種基於臨界區控制的交互做用是比較簡單的,只要一個進程/線程進入了臨界區,其餘試圖想進入臨界區的進程/線程都會被阻塞着,直到第一個進程/線程離開了臨界區。

咱們都知道在多線程裏,每一個線程並必定是順序執行的,它們基本是以各自獨立的、不可預知的速度向前推動,但有時候咱們又但願多個線程能密切合做,以實現一個共同的任務。

例子,線程 1 是負責讀入數據的,而線程 2 是負責處理數據的,這兩個線程是相互合做、相互依賴的。線程 2 在沒有收到線程 1 的喚醒通知時,就會一直阻塞等待,當線程 1 讀完數據須要把數據傳給線程 2 時,線程 1 會喚醒線程 2,並把數據交給線程 2 處理。

所謂同步,就是併發進程/線程在一些關鍵點上可能須要互相等待與互通消息,這種相互制約的等待與互通訊息稱爲進程/線程同步

舉個生活的同步例子,你肚子餓了想要吃飯,你叫媽媽早點作菜,媽媽聽到後就開始作菜,可是在媽媽沒有作完飯以前,你必須阻塞等待,等媽媽作完飯後,天然會通知你,接着你吃飯的事情就能夠進行了。

吃飯與作菜的同步關係吃飯與作菜的同步關係

注意,同步與互斥是兩種不一樣的概念:

  • 同步就比如:「操做 A 應在操做 B 以前執行」,「操做 C 必須在操做 A 和操做 B 都完成以後才能執行」等;
  • 互斥就比如:「操做 A 和操做 B 不能在同一時刻執行」;

互斥與同步的實現和使用

在進程/線程併發執行的過程當中,進程/線程之間存在協做的關係,例若有互斥、同步的關係。

爲了實現進程/線程間正確的協做,操做系統必須提供實現進程協做的措施和方法,主要的方法有兩種:

  • :加鎖、解鎖操做;
  • 信號量:P、V 操做;

這兩個均可以方便地實現進程/線程互斥,而信號量比鎖的功能更強一些,它還能夠方便地實現進程/線程同步。

使用加鎖操做和解鎖操做能夠解決併發線程/進程的互斥問題。

任何想進入臨界區的線程,必須先執行加鎖操做。若加鎖操做順利經過,則線程可進入臨界區;在完成對臨界資源的訪問後再執行解鎖操做,以釋放該臨界資源。

加鎖-解鎖加鎖-解鎖

根據鎖的實現不一樣,能夠分爲「忙等待鎖」和「無忙等待鎖」。

咱們先來看看「忙等待鎖」的實現

在說明「忙等待鎖」的實現以前,先介紹現代 CPU 體系結構提供的特殊原子操做指令 —— 測試和置位(Test-and-Set)指令

若是用 C 代碼表示 Test-and-Set 指令,形式以下:

測試並設置指令作了下述事情:

  • old_ptr 更新爲 new 的新值
  • 返回 old_ptr 的舊值;

固然,關鍵是這些代碼是原子執行。由於既能夠測試舊值,又能夠設置新值,因此咱們把這條指令叫做「測試並設置」。

那什麼是原子操做呢?原子操做就是要麼所有執行,要麼都不執行,不能出現執行到一半的中間狀態

咱們能夠運用 Test-and-Set 指令來實現「忙等待鎖」,代碼以下:

忙等待鎖的實現忙等待鎖的實現

咱們來確保理解爲何這個鎖能工做:

  • 第一個場景是,首先假設一個線程在運行,調用 lock(),沒有其餘線程持有鎖,因此 flag 是 0。當調用 TestAndSet(flag, 1) 方法,返回 0,線程會跳出 while 循環,獲取鎖。同時也會原子的設置 flag 爲1,標誌鎖已經被持有。當線程離開臨界區,調用 unlock()flag 清理爲 0。

  • 第二種場景是,當某一個線程已經持有鎖(即 flag 爲1)。本線程調用 lock(),而後調用 TestAndSet(flag, 1),這一次返回 1。只要另外一個線程一直持有鎖,TestAndSet() 會重複返回 1,本線程會一直忙等。當 flag 終於被改成 0,本線程會調用 TestAndSet(),返回 0 而且原子地設置爲 1,從而得到鎖,進入臨界區。

很明顯,當獲取不到鎖時,線程就會一直 wile 循環,不作任何事情,因此就被稱爲「忙等待鎖」,也被稱爲自旋鎖(spin lock

這是最簡單的一種鎖,一直自旋,利用 CPU 週期,直到鎖可用。在單處理器上,須要搶佔式的調度器(即不斷經過時鐘中斷一個線程,運行其餘線程)。不然,自旋鎖在單 CPU 上沒法使用,由於一個自旋的線程永遠不會放棄 CPU。

再來看看「無等待鎖」的實現

無等待鎖顧明思議就是獲取不到鎖的時候,不用自旋。

既然不想自旋,那當沒獲取到鎖的時候,就把當前線程放入到鎖的等待隊列,而後執行調度程序,把 CPU 讓給其餘線程執行。

無等待鎖的實現無等待鎖的實現

本次只是提出了兩種簡單鎖的實現方式。固然,在具體操做系統實現中,會更復雜,但也離不開本例子兩個基本元素。

若是你想要對鎖的更進一步理解,推薦你們能夠看《操做系統導論》第 28 章鎖的內容,這本書在「微信讀書」就能夠免費看。

信號量

信號量是操做系統提供的一種協調共享資源訪問的方法。

一般信號量表示資源的數量,對應的變量是一個整型(sem)變量。

另外,還有兩個原子操做的系統調用函數來控制信號量的,分別是:

  • P 操做:將 sem1,相減後,若是 sem < 0,則進程/線程進入阻塞等待,不然繼續,代表 P 操做可能會阻塞;
  • V 操做:將 sem1,相加後,若是 sem <= 0,喚醒一個等待中的進程/線程,代表 V 操做不會阻塞;

P 操做是用在進入臨界區以前,V 操做是用在離開臨界區以後,這兩個操做是必須成對出現的。

舉個類比,2 個資源的信號量,至關於 2 條火車軌道,PV 操做以下圖過程:

信號量與火車軌道信號量與火車軌道

操做系統是如何實現 PV 操做的呢?

信號量數據結構與 PV 操做的算法描述以下圖:

PV 操做的算法描述PV 操做的算法描述

PV 操做的函數是由操做系統管理和實現的,因此操做系統已經使得執行 PV 函數時是具備原子性的。

PV 操做如何使用的呢?

信號量不只能夠實現臨界區的互斥訪問控制,還能夠線程間的事件同步。

咱們先來講說如何使用信號量實現臨界區的互斥訪問

爲每類共享資源設置一個信號量 s,其初值爲 1,表示該臨界資源未被佔用。

只要把進入臨界區的操做置於 P(s)V(s) 之間,便可實現進程/線程互斥:

此時,任何想進入臨界區的線程,必先在互斥信號量上執行 P 操做,在完成對臨界資源的訪問後再執行 V 操做。因爲互斥信號量的初始值爲 1,故在第一個線程執行 P 操做後 s 值變爲 0,表示臨界資源爲空閒,可分配給該線程,使之進入臨界區。

若此時又有第二個線程想進入臨界區,也應先執行 P 操做,結果使 s 變爲負值,這就意味着臨界資源已被佔用,所以,第二個線程被阻塞。

而且,直到第一個線程執行 V 操做,釋放臨界資源而恢復 s 值爲 0 後,才喚醒第二個線程,使之進入臨界區,待它完成臨界資源的訪問後,又執行 V 操做,使 s 恢復到初始值 1。

對於兩個併發線程,互斥信號量的值僅取 一、0 和 -1 三個值,分別表示:

  • 若是互斥信號量爲 1,表示沒有線程進入臨界區;
  • 若是互斥信號量爲 0,表示有一個線程進入臨界區;
  • 若是互斥信號量爲 -1,表示一個線程進入臨界區,另外一個線程等待進入。

經過互斥信號量的方式,就能保證臨界區任什麼時候刻只有一個線程在執行,就達到了互斥的效果。

再來,咱們說說如何使用信號量實現事件同步

同步的方式是設置一個信號量,其初值爲 0

咱們把前面的「吃飯-作飯」同步的例子,用代碼的方式實現一下:

媽媽一開始詢問兒子要不要作飯時,執行的是 P(s1) ,至關於詢問兒子需不須要吃飯,因爲 s1 初始值爲 0,此時 s1 變成 -1,代表兒子不須要吃飯,因此媽媽線程就進入等待狀態。

當兒子肚子餓時,執行了 V(s1),使得 s1 信號量從 -1 變成 0,代表此時兒子須要吃飯了,因而就喚醒了阻塞中的媽媽線程,媽媽線程就開始作飯。

接着,兒子線程執行了 P(s2),至關於詢問媽媽飯作完了嗎,因爲 s2 初始值是 0,則此時 s2 變成 -1,說明媽媽還沒作完飯,兒子線程就等待狀態。

最後,媽媽終於作完飯了,因而執行 V(s2)s2 信號量從 -1 變回了 0,因而就喚醒等待中的兒子線程,喚醒後,兒子線程就能夠進行吃飯了。

生產者-消費者問題

生產者-消費者模型生產者-消費者模型

生產者-消費者問題描述:

  • 生產者在生成數據後,放在一個緩衝區中;
  • 消費者從緩衝區取出數據處理;
  • 任什麼時候刻,只能有一個生產者或消費者能夠訪問緩衝區;

咱們對問題分析能夠得出:

  • 任什麼時候刻只能有一個線程操做緩衝區,說明操做緩衝區是臨界代碼,須要互斥
  • 緩衝區空時,消費者必須等待生產者生成數據;緩衝區滿時,生產者必須等待消費者取出數據。說明生產者和消費者須要同步

那麼咱們須要三個信號量,分別是:

  • 互斥信號量 mutex:用於互斥訪問緩衝區,初始化值爲 1;
  • 資源信號量 fullBuffers:用於消費者詢問緩衝區是否有數據,有數據則讀取數據,初始化值爲 0(代表緩衝區一開始爲空);
  • 資源信號量 emptyBuffers:用於生產者詢問緩衝區是否有空位,有空位則生成數據,初始化值爲 n (緩衝區大小);

具體的實現代碼:

若是消費者線程一開始執行 P(fullBuffers),因爲信號量 fullBuffers 初始值爲 0,則此時 fullBuffers 的值從 0 變爲 -1,說明緩衝區裏沒有數據,消費者只能等待。

接着,輪到生產者執行 P(emptyBuffers),表示減小 1 個空槽,若是當前沒有其餘生產者線程在臨界區執行代碼,那麼該生產者線程就能夠把數據放到緩衝區,放完後,執行 V(fullBuffers) ,信號量 fullBuffers 從 -1 變成 0,代表有「消費者」線程正在阻塞等待數據,因而阻塞等待的消費者線程會被喚醒。

消費者線程被喚醒後,若是此時沒有其餘消費者線程在讀數據,那麼就能夠直接進入臨界區,從緩衝區讀取數據。最後,離開臨界區後,把空槽的個數 + 1。


經典同步問題

哲學家就餐問題

當初我在校招的時候,面試官也問過「哲學家就餐」這道題目,我當時聽的一臉懵逼,不管面試官怎麼講述這個問題,我也始終沒聽懂,就莫名其妙的說這個問題會「死鎖」。

固然,我這回答槽透了,因此當場 game over,殘酷又悲慘故事,就很少說了,反正當時菜就是菜。

時至今日,看我來圖解這道題。

哲學家就餐的問題哲學家就餐的問題

先來看看哲學家就餐的問題描述:

  • 5 個老大哥哲學家,閒着沒事作,圍繞着一張圓桌吃麪;
  • 巧就巧在,這個桌子只有 5 支叉子,每兩個哲學家之間放一支叉子;
  • 哲學家圍在一塊兒先思考,思考中途餓了就會想進餐;
  • 奇葩的是,這些哲學家要兩支叉子才願意吃麪,也就是須要拿到左右兩邊的叉子才進餐
  • 吃完後,會把兩支叉子放回原處,繼續思考

那麼問題來了,如何保證哲 學家們的動做有序進行,而不會出現有人永遠拿不到叉子呢?

方案一

咱們用信號量的方式,也就是 PV 操做來嘗試解決它,代碼以下:

上面的程序,好似很天然。拿起叉子用 P 操做,表明有叉子就直接用,沒有叉子時就等待其餘哲學家放回叉子。

方案一的問題方案一的問題

不過,這種解法存在一個極端的問題:假設五位哲學家同時拿起左邊的叉子,桌面上就沒有叉子了, 這樣就沒有人可以拿到他們右邊的叉子,也就說每一位哲學家都會在 P(fork[(i + 1) % N ]) 這條語句阻塞了,很明顯這發生了死鎖的現象

方案二

既然「方案一」會發生同時競爭左邊叉子致使死鎖的現象,那麼咱們就在拿叉子前,加個互斥信號量,代碼以下:

上面程序中的互斥信號量的做用就在於,只要有一個哲學家進入了「臨界區」,也就是準備要拿叉子時,其餘哲學家都不能動,只有這位哲學家用完叉子了,才能輪到下一個哲學家進餐。

方案二的問題方案二的問題

方案二雖然能讓哲學家們按順序吃飯,可是每次進餐只能有一位哲學家,而桌面上是有 5 把叉子,按道理是能能夠有兩個哲學家同時進餐的,因此從效率角度上,這不是最好的解決方案。

方案三

那既然方案二使用互斥信號量,會致使只能容許一個哲學家就餐,那麼咱們就不用它。

另外,方案一的問題在於,會出現全部哲學家同時拿左邊刀叉的可能性,那咱們就避免哲學家能夠同時拿左邊的刀叉,採用分支結構,根據哲學家的編號的不一樣,而採起不一樣的動做。

即讓偶數編號的哲學家「先拿左邊的叉子後拿右邊的叉子」,奇數編號的哲學家「先拿右邊的叉子後拿左邊的叉子」。

上面的程序,在 P 操做時,根據哲學家的編號不一樣,拿起左右兩邊叉子的順序不一樣。另外,V 操做是不須要分支的,由於 V 操做是不會阻塞的。

方案三可解決問題方案三可解決問題

方案三即不會出現死鎖,也能夠兩人同時進餐。

方案四

在這裏再提出另一種可行的解決方案,咱們用一個數組 state 來記錄每一位哲學家在進程、思考仍是飢餓狀態(正在試圖拿叉子)。

那麼,一個哲學家只有在兩個鄰居都沒有進餐時,才能夠進入進餐狀態。

i 個哲學家的左鄰右舍,則由宏 LEFTRIGHT 定義:

  • LEFT : ( i + 5 - 1 ) % 5
  • RIGHT : ( i + 1 ) % 5

好比 i 爲 2,則 LEFT 爲 1,RIGHT 爲 3。

具體代碼實現以下:

上面的程序使用了一個信號量數組,每一個信號量對應一位哲學家,這樣在所需的叉子被佔用時,想進餐的哲學家就被阻塞。

注意,每一個進程/線程將 smart_person 函數做爲主代碼運行,而其餘 take_forksput_forkstest 只是普通的函數,而非單獨的進程/線程。

方案四也可解決問題方案四也可解決問題

方案四一樣不會出現死鎖,也能夠兩人同時進餐。

讀者-寫者問題

前面的「哲學家進餐問題」對於互斥訪問有限的競爭問題(如 I/O 設備)一類的建模過程十分有用。

另外,還有個著名的問題是「讀者-寫者」,它爲數據庫訪問創建了一個模型。

讀者只會讀取數據,不會修改數據,而寫者便可以讀也能夠修改數據。

讀者-寫者的問題描述:

  • 「讀-讀」容許:同一時刻,容許多個讀者同時讀
  • 「讀-寫」互斥:沒有寫者時讀者才能讀,沒有讀者時寫者才能寫
  • 「寫-寫」互斥:沒有其餘寫者時,寫者才能寫

接下來,提出幾個解決方案來分析分析。

方案一

使用信號量的方式來嘗試解決:

  • 信號量 wMutex:控制寫操做的互斥信號量,初始值爲 1 ;
  • 讀者計數 rCount:正在進行讀操做的讀者個數,初始化爲 0;
  • 信號量 rCountMutex:控制對 rCount 讀者計數器的互斥修改,初始值爲 1;

接下來看看代碼的實現:

上面的這種實現,是讀者優先的策略,由於只要有讀者正在讀的狀態,後來的讀者均可以直接進入,若是讀者持續不斷進入,則寫者會處於飢餓狀態。

方案二

那既然有讀者優先策略,天然也有寫者優先策略:

  • 只要有寫者準備要寫入,寫者應儘快執行寫操做,後來的讀者就必須阻塞;
  • 若是有寫者持續不斷寫入,則讀者就處於飢餓;

在方案一的基礎上新增以下變量:

  • 信號量 rMutex:控制讀者進入的互斥信號量,初始值爲 1;
  • 信號量 wDataMutex:控制寫者寫操做的互斥信號量,初始值爲 1;
  • 寫者計數 wCount:記錄寫者數量,初始值爲 0;
  • 信號量 wCountMutex:控制 wCount 互斥修改,初始值爲 1;

具體實現以下代碼:

注意,這裏 rMutex 的做用,開始有多個讀者讀數據,它們所有進入讀者隊列,此時來了一個寫者,執行了 P(rMutex) 以後,後續的讀者因爲阻塞在 rMutex 上,都不能再進入讀者隊列,而寫者到來,則能夠所有進入寫者隊列,所以保證了寫者優先。

同時,第一個寫者執行了 P(rMutex) 以後,也不能立刻開始寫,必須等到全部進入讀者隊列的讀者都執行完讀操做,經過 V(wDataMutex) 喚醒寫者的寫操做。

方案三

既然讀者優先策略和寫者優先策略都會形成飢餓的現象,那麼咱們就來實現一下公平策略。

公平策略:

  • 優先級相同;
  • 寫者、讀者互斥訪問;
  • 只能一個寫者訪問臨界區;
  • 能夠有多個讀者同時訪問臨街資源;

具體代碼實現:

看完代碼不知你是否有這樣的疑問,爲何加了一個信號量 flag,就實現了公平競爭?

對比方案一的讀者優先策略,能夠發現,讀者優先中只要後續有讀者到達,讀者就能夠進入讀者隊列, 而寫者必須等待,直到沒有讀者到達。

沒有讀者到達會致使讀者隊列爲空,即 rCount==0,此時寫者才能夠進入臨界區執行寫操做。

而這裏 flag 的做用就是阻止讀者的這種特殊權限(特殊權限是隻要讀者到達,就能夠進入讀者隊列)。

好比:開始來了一些讀者讀數據,它們所有進入讀者隊列,此時來了一個寫者,執行 P(falg) 操做,使得後續到來的讀者都阻塞在 flag 上,不能進入讀者隊列,這會使得讀者隊列逐漸爲空,即 rCount 減爲 0。

這個寫者也不能立馬開始寫(由於此時讀者隊列不爲空),會阻塞在信號量 wDataMutex 上,讀者隊列中的讀者所有讀取結束後,最後一個讀者進程執行 V(wDataMutex),喚醒剛纔的寫者,寫者則繼續開始進行寫操做。


嘮叨嘮叨

不負衆望,小林又拖延了,又沒有達到「周更」,慚愧慚愧,估計不少讀者都快忘記小林是誰了,小林求求你們不要取關呀!

是的,就在上上週,大好的週末,小林偷懶了,重溫了兩天的「七龍珠 Z 」動漫,看的確實很爽啊,雖然已經看過不少遍了,誰叫我是龍珠迷。

沒辦法這就是生活,時而鬆時而緊。

小林是專爲你們圖解的工具人,Goodbye,咱們下次見!


好文推薦:

進程和線程基礎知識全家桶,30 張圖一套帶走

20 張圖揭開內存管理的迷霧,瞬間豁然開朗

相關文章
相關標籤/搜索