您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關線程安全是什么,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
帶薪蹲坑,相信都是大伙都愛做的事情,阿星也不例外,但是我司所在的樓層的坑位較少,粥少僧多,十分煩惱。
阿星(線程A)每次去廁所(共享資源),門都是鎖著的,說明有同事在里面占著坑(線程B持有鎖),只能無奈的在外面乖乖的等著,不久后沖水聲響起,同事爽完出來(線程B釋放鎖),阿星一個健步進入廁所把門鎖住(線程A持有鎖),享受屬于自己的空間,晚來的其他同事只能乖乖排隊,一切都是那么井然有序。
假設門鎖壞了,井然有序就不存在了,上廁所不再是享受,而是高度緊張,防止門突然被打開,更糟糕的是,開門時,是個妹子,這下不僅僅是線程安全問題,還有數組越界了。
故事說完,扯了那么多,就是想說明,在多線程環境里,對共享資源進行操作,如果多線程之間不做合理的協作(互斥與同步),那么一定會發生翻車現場。
因為多線程共享進程資源,在操作系統調度進程內的多線程時,必然會出現多線程競爭共享資源問題,如果不采取有效的措施,則會造成共享資源的混亂!
來寫個小例子,創建兩個線程,它們分別對共享變量 i
自增 1
執行 1000
次,如下代碼
正常來說,i
變量最后的值是 2000
,可是并非如此,我們執行下代碼看看結果
結果:2000
結果:1855
運行了兩次,結果分別是1855、2000,我們發現每次運行的結果不同,這在計算機里是不能容忍的,雖然是小概率出現的錯誤,但是小概率它一定是會發生的。
為了搞明白到底發生了什么事情,我們必須要了解匯編指令執行,以 i
加 1
為例子,匯編指令的執行過程如下
好家伙,一個加法動作,在 C P U 運行,實際要執行 3
條指令。
現在模擬下線程A與線程B的運行,假設此時內存變量 i
的值是 0
,線程A加載內存的 i
值到寄存器,對寄存器 i
值加 1
,此時 i
值是 1
,正準備執行下一步寄存器 i
值回寫內存,時間片使用完了,發生線程上下文切換,保存線程的私有信息到線程控制塊T C P。
操作系統調度線程B執行,此時的內存變量 i
依然還是 0
,線程B執行與線程A一樣的步驟,它很幸運,在時間片使用完前,執行完了加 1
,最終回寫內存,內存變量 i
值是 1
。
線程B時間片使用完后,發生線程上下文切換,回到線程A上次的狀態繼續執行,寄存器中的 i
值回寫內存,內存變量再次被設置成 1
。
按理說,最后的 i
值應該是 2
,但是由于不可控的調度,導致最后 i
值是 1
,下面是線程A與線程B的流程圖
第一步:內存取出 i
值,加載進寄存器
第二步:對寄存器內的 i
值加 1
第三步:寄存器內的 i
值取出 加載進內存
這種情況稱為競爭條件(race condition),多線程相互競爭操作共享資源時,由于運氣不好,在執行過程中發生線程上下文切換,最后得到錯誤的結果,事實上,每次運行都可能得到不同的結果,因此輸出的結果存在不確定性(indeterminate)。
為了解決因競爭條件出現的線程安全,操作系統是通過互斥與同步來解決此類問題。
多線程執行共享變量的這段代碼可能會導致競爭狀態,因此我們將此段代碼稱為臨界區(critical section),它是執行共享資源的代碼片段,一定不能給多線程同時執行。
所以我們希望這段代碼是互斥(mutualexclusion)的,也就說執行臨界區(critical section)代碼段的只能有一個線程,其他線程阻塞等待,達到排隊效果。
互斥并不只是針對多線程的競爭條件,同時還可用于多進程,避免共享資源混亂。
互斥解決了「多進程/線程」對臨界區使用的問題,但是它沒有解決「多進程/線程」協同工作的問題
我們都知道在多線程里,每個線程一定是順序執行的,它們各自獨立,以不可預知的速度向前推進,但有時候我們希望多個線程能密切合作,以實現一個共同的任務。
所謂同步,就是「多進程/線程間」在一些關鍵點上可能需要互相等待與互通消息,這種相互制約的等待與互通信息稱為「進程/線程」同步。
舉個例,有兩個角色分別是研發、質量管控,質量管控測試功能,需要等研「發完成開發」,研發要修bug也要等質量管控「測試完成提交B U G」,正常流程是研發完成開發,通知質量管控進行測試,質量管控測試完成,通知研發人員修復bug。
互斥:某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的(操作 A 和操作 B 不能在同一時刻執行)
同步:互斥的基礎上,通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥(操作 A 應在操作 B 之前執行,操作 C 必須在操作 A 和操作 B 都完成之后才能執行)
顯然,同步是一種更為復雜的互斥,而互斥是一種特殊的同步。也就是說互斥是兩個線程之間不可以同時運行,他們會相互排斥,必須等待一個線程運行完畢,另一個才能運行,而同步也是不能同時運行,但他是必須要按照某種次序來運行相應的線程(也是一種互斥)!
互斥與同步可以保證「多進程/線程間正確協作」 ,但是互斥與同步僅僅只是概念,操作系統必須要提供對應的實現,針對互斥與同步的實現有下面兩種
鎖:加鎖、解鎖操作(互斥)
信號量:P、V 操作(同步)
這兩個種方式都可以實現「多進程/線程」互斥,信號量比鎖的功能更強一些,它還可以方便地實現「多進程/線程」同步。
顧名思義,給臨界區上一把鎖,任何進入臨界區)的線程,必須先執行加鎖操作,加鎖成功,才能進入臨界區,在離開臨界區時再釋放鎖,達到互斥的效果。
鎖的實現方式又分為「忙等待鎖」和「無忙等待鎖」
檢查并設置(test-and-set-lock,TSL)是一種不可中斷的原子運算,它屬于原子操作指令,可以通過它來實現忙等鎖(自旋鎖)。
test-and-set-lock 指令偽代碼
檢查并設置做了如下幾個步驟
檢查舊值是否相等
相等設置新值,返回原舊值(成功)
不相等,無任何操作,直接返回原舊值(失敗)
上面的步驟,把它看成一步并具備原子性,原子性的意思是指全部執行或都不執行,不會出現執行到一半的中間狀態.
偽代碼testAndSetLock
實現忙等鎖(自旋鎖)
下面兩種場景運行
單線程:假設一個線程訪問臨界區,執行 getLock
方法,檢查舊值 0
通過,更新原舊值 0
為新值 1
,返回原舊值 0
,獲取鎖成功,離開臨界區時,執行 unLock
方法,檢查舊值 1
通過,更新原舊值 1
為新值 0
,釋放鎖成功。
多線程:假設兩個線程,線程A訪問臨界區,執行 getLock
方法,檢查舊值 0
通過,更新原舊值 0
為新值 1
,返回原舊值 0
,獲取鎖成功,此時線程B執行 getLock
方法,舊值檢查失敗,獲取鎖失敗,一直循環直到更新成功為止,當線程A離開臨界區時,執行 unLock
方法,檢查舊值 1
通過,更新原舊值 1
為新值 0
,釋放鎖成功,線程B獲取鎖成功。
當獲取不到鎖時,線程就會一直 wile
循環,不做任何事情,所以就被稱為忙等待鎖,也被稱為自旋鎖。
這是最簡單的鎖,一直自旋,利用 C P U 周期,直到鎖可用。在單處理器上,需要搶占式的調度器(即不斷通過時鐘中斷一個線程,運行其他線程)。否則,自旋鎖在 C P U 上無法使用,因為一個自旋的線程永遠不會放棄 C P U。
顧名思義,無忙等鎖不需要主動自旋,被動等待喚醒即可,在沒有獲取到鎖的時候,就把該線程加入到等待隊列,讓出 C P U 給其他線程,其他線程釋放鎖時,再從等待隊列喚醒該線程。
兩種鎖的實現都是基于檢查并設置(test-and-set-lock,TSL),上面只是簡單的偽代碼,實際上操作系統的實現會更復雜,但是基本思想與大致流程還是與本例一樣。
操作系統中協調「多線程/進程」共同配合工作,就是通過信號量實現的,通常信號量代表「資源數量」,對應一個整型(s e n
)變量,還有兩個原子操作的系統調用函數來控制「資源數量」。
P 操作:將 s e n
減 1
,相減后,如果 s e n
< 0
,則進程/線程進入阻塞等待,否則繼續,P 操作可能會阻塞
V 操作:將 s e n
加 1
,相加后,如果 s e n
<= 0
,喚醒等待中的進程/線程,V 操作不會阻塞
P V操作必須是成對出現,但是沒有順序要求,也就說你可以P V或V P。
舉個例子,最近新冠病毒又出來搗亂了,為了自身安全,大家都去打疫苗,因為醫生只有兩位(相當于2個資源的信號量),所以同時只能為兩個人接種疫苗,過程如下圖
信號量等于 0
時,代表無資源可用
信號量小于 0
時,代表有線程在阻塞
信號量大于 0
時,代表資源可用
使用偽代碼實現P V 信號量
P V操作的函數是由操作系統管理和實現的,所以 P V 函數是具有原子性的。
信號量還是比較有意思的,這里來做幾個實踐,加深大家對信號量的理解,實踐的內容分別是
信號量實現互斥
信號量實現事件同步
信號量實現生產者與消費者
使用信號量實現互斥非常簡單,信號量數量為1
,線程進入臨界區進行 P 操作,離開臨界區進行 V 操作。
以前面說的研發、質量管控線程為例子,實現事件同步的效果,偽代碼如下
首先抽象出兩個信號量,「是否能提測」與「是否能修BUG」,它們默認都是否,也就是 0
,關鍵點就是對兩個信號量進行 P V 操作
質量管控線程詢問開發線程有沒有完成開發,執行 P
操作 p(this.rDSemaphore)
如果沒有完成開發,this.rDSemaphore
減 1
結果為 -1
,質量管控線程阻塞等待喚醒(等后續研發線程進行 V
操作)
如果完成開發,說明研發線程先執行 V
操作 v(this.rDSemaphore)
完成開發,this.rDSemaphore
加 1
結果 1
,此時質量管控線程 P
操作 this.rDSemaphore
減 1
結果 0
,進行后面的提測工作
研發線程詢問質量管控線程能不能修復B U G,執行 P
操作 p(this.qualitySemaphore)
如果不可以修復B U G,this.qualitySemaphore
減 1
結果為 -1
,研發線程阻塞等待喚醒(等后續質量管控線程執行 V
操作)
如果可以修復B U G,說明質量管控線程先執行 V
操作 v(this.qualitySemaphore)
提交BUG, this.qualitySemaphore
加 1
結果為 1
,此時研發線程 P
操作 this.qualitySemaphore
減 1
結果 0
,進行后面的修復 B U G 操作
流程
質量管控線程執行 P
操作 p(this.rDSemaphore)
能不能提測,this.rDSemaphore
減 1
結果是 -1
,不能進行提測,質量管控線程阻塞等待喚醒
研發線程運行,執行 V
操作 v(this.rDSemaphore)
完成研發功能,this.rDSemaphore
加 1
結果是 0
,通知質量管控線程提測
研發線程繼續執行 P
操作 p(this.qualitySemaphore)
能不能修復B U G,this.qualitySemaphor
減 1
結果是 -1
,不能修復B U G,研發線程阻塞等待喚醒
質量管控線程喚醒后進行提測,提測完畢執行 V
操作 v(this.qualitySemaphore)
完成提測與提交相關B U G,this.qualitySemaphore
加 1
結果是 0
,通知研發線程進行B U G修復
生產者與消費者是一個比較經典的線程同步問題,我們先分析下有那些角色
生產者:生產事件放入緩沖區
消費者:從緩沖區消費事件
緩沖區:裝載事件的容器
問題分析可以得出:
任何時刻只能有一個線程操作緩沖區,說明操作緩沖區是臨界代碼,需要互斥
緩沖區空時,消費者必須等待生產者生成數據
緩沖區滿時,生產者必須等待消費者取出數據
通過問題分析我們可以抽象出3個信號量
互斥信號量:互斥訪問緩沖區,初始化 1
消費者資源信號量:緩沖區是否有事件,初始化 0
,無事件
生產者信號量:緩沖區是否有空位裝載事件,初始化 N
(緩沖區大小)
偽代碼如下
關鍵的 P V 操作如下
生產線程,在往緩沖區裝載事件之前,執行 P
操作 p(this.produceSemaphore)
,緩沖區空槽數量減 1
,結果 < 0
說明無空槽,阻塞等待「消費線程」喚醒,否則執行后續邏輯
不論是生產線程還是消費線程在操作緩沖區都要執行 P V
臨界區操作 p(this.mutexSemaphore)
與 v(this.mutexSemaphore)
,這里就不做過多概述了
消費線程,在從緩存區消費事件之前,執行 P
操作 p(this.consumeSemaphore)
,緩沖區事件數量減 1
,結果 < 0
說明緩沖區無事件消費,阻塞等待「生產線程」喚醒,否執行后續邏輯
生產線程與消費線程,執行完「裝載/消費」后,都要喚醒對應的「生產/消費線程」,執行 V
操作「緩沖區空槽加 1
/緩沖區事件加 1
」
看完上述內容,你們對線程安全是什么有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。