日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

面试官:讲讲互斥锁、自旋锁吧

發(fā)布時間:2024/1/8 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面试官:讲讲互斥锁、自旋锁吧 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

在介紹悲觀鎖和樂觀鎖之前,我們先看一下什么是鎖。

生活中:鎖在我們身邊無處不在,比如我出門玩去了需要把門鎖上,比如我需要把錢放到保險柜里面,必須上鎖以保證我財產(chǎn)的安全。

如何用好鎖是程序員的基本素養(yǎng)之一。多線程訪問共享資源的時候,避免不了資源競爭而導(dǎo)致數(shù)據(jù)錯亂的問題,通常為了解決這一問題,都會在訪問共享資源之前加鎖。最常用的就是互斥鎖,當(dāng)然還有很多種不同的鎖,比如自旋鎖、讀寫鎖、樂觀鎖等,不同種類的鎖自然適用于不同的場景。

如果選對了合適的鎖,則會大大提高系統(tǒng)的性能

如果選擇了錯誤的鎖,在一些高并發(fā)的場景下,可能會降低系統(tǒng)的性能,影響用戶體驗。為了選擇合適的鎖,不僅需要清楚知道加鎖的成本開銷有多大,還需要分析業(yè)務(wù)場景中訪問的共享資源的方式,再來還要考慮并發(fā)訪問共享資源時的沖突概率。對癥下藥,才能減少鎖對高并發(fā)性能的影響。

加鎖的目的就是保證共享資源在任意時間里,只有一個線程訪問,這樣就可以避免多線程導(dǎo)致共享數(shù)據(jù)錯亂的問題。

互斥鎖

互斥鎖(Mutex,全稱 mutual exclusion)是為了來保護一個資源不會因為并發(fā)操作而引起沖突,比如多個線程去訪問資源,線程 A 加鎖成功,此時互斥鎖已經(jīng)被線程 A 獨占了,此時線程 B 加鎖會失敗,因為線程 A 并沒有釋放掉鎖,于是釋放 CPU 給其他線程,而線程 B 加鎖的代碼就會被阻塞。

對此Go語言提供了很是簡單易用的Mutex,Mutex為一結(jié)構(gòu)體類型,對外暴露兩個方法Lock()和Unlock()分別用于加鎖和解鎖

src/sync/mutex.go:Mutex定義了互斥鎖的數(shù)據(jù)結(jié)構(gòu):

Mutex結(jié)構(gòu)體

type Mutex struct {state int32sema uint32 }
  • state:表示互斥鎖的狀態(tài),好比是否被鎖定等
  • sema:表示信號量,協(xié)程阻塞等待該信號量,解鎖的協(xié)程釋放信號量從而喚醒等待信號量的協(xié)程

Mutex.state結(jié)構(gòu)圖:

上面定義4個的含義:

  • Waiter: 表示阻塞等待鎖的線程個數(shù),線程解鎖時根據(jù)此值來判斷是否須要釋放信號量
  • Starving:饑餓狀態(tài), 0:表示正常狀態(tài),1:表示饑餓狀態(tài),說明有線程阻塞了超過1ms
  • Woken: 喚醒狀態(tài),0:表示未喚醒 1:表示已喚醒,正在加鎖過程當(dāng)中
  • Locked: 加鎖狀態(tài),0:表示為加鎖1:表示已加鎖

Mutex方法

  • Lock() : 加鎖方法
  • Unlock(): 解鎖方法

Lock() 代碼詳解

// Lock mutex 的鎖方法。 func (m *Mutex) Lock() {// 快速上鎖.if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// 快速上鎖失敗,將進行操作較多的上鎖動作。m.lockSlow() }func (m *Mutex) lockSlow() {var waitStartTime int64 // 記錄當(dāng)前 goroutine 的等待時間starving := false // 是否饑餓awoke := false // 是否被喚醒iter := 0 // 自旋次數(shù)old := m.state // 當(dāng)前 mutex 的狀態(tài)for {// 當(dāng)前 mutex 的狀態(tài)已上鎖,并且非饑餓模式,并且符合自旋條件if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// 當(dāng)前還沒設(shè)置過喚醒標(biāo)識if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}runtime_doSpin()iter++old = m.statecontinue}new := old// 如果不是饑餓狀態(tài),則嘗試上鎖// 如果是饑餓狀態(tài),則不會上鎖,因為當(dāng)前的 goroutine 將會被阻塞并添加到等待喚起隊列的隊尾if old&mutexStarving == 0 {new |= mutexLocked}// 等待隊列數(shù)量 + 1if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// 如果 goroutine 之前是饑餓模式,則此次也設(shè)置為饑餓模式if starving && old&mutexLocked != 0 {new |= mutexStarving}//if awoke {// 如果狀態(tài)不符合預(yù)期,則報錯if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}// 新狀態(tài)值需要清除喚醒標(biāo)識,因為當(dāng)前 goroutine 將會上鎖或者再次 sleepnew &^= mutexWoken}// CAS 嘗試性修改狀態(tài),修改成功則表示獲取到鎖資源if atomic.CompareAndSwapInt32(&m.state, old, new) {// 非饑餓模式,并且未獲取過鎖,則說明此次的獲取鎖是 ok 的,直接 returnif old&(mutexLocked|mutexStarving) == 0 {break}// 根據(jù)等待時間計算 queueLifoqueueLifo := waitStartTime != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}// 到這里,表示未能上鎖成功// queueLife = true, 將會把 goroutine 放到等待隊列隊頭// queueLife = false, 將會把 goroutine 放到等待隊列隊尾runtime_SemacquireMutex(&m.sema, queueLifo, 1)// 計算是否符合饑餓模式,即等待時間是否超過一定的時間starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.state// 上一次是饑餓模式if old&mutexStarving != 0 {if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}delta := int32(mutexLocked - 1<<mutexWaiterShift)// 此次不是饑餓模式又或者下次沒有要喚起等待隊列的 goroutine 了if !starving || old>>mutexWaiterShift == 1 {delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}// 此處已不再是饑餓模式了,清除自旋次數(shù),重新到 for 循環(huán)競爭鎖。awoke = trueiter = 0} else {old = m.state}} ?if race.Enabled {race.Acquire(unsafe.Pointer(m))} }

大致的流程:

  • 首先,如果 mutex 的 state = 0,即沒有誰在占有資源,也沒有阻塞等待喚起的 goroutine。則會調(diào)用 CAS 方法去嘗試性占有鎖,不做其他動作
  • 如果不符合 m.state = 0,則進一步判斷是否需要自旋
  • 當(dāng)不需要自旋又或者自旋后還是得不到資源時,此時會調(diào)用 runtime_SemacquireMutex 信號量函數(shù),將當(dāng)前的 goroutine 阻塞并加入等待喚起隊列里
  • 當(dāng)有鎖資源釋放,mutex 在喚起了隊頭的 goroutine 后,隊頭 goroutine 會嘗試性的占有鎖資源,而此時也有可能會和新到來的 goroutine 一起競爭
  • 當(dāng)隊頭 goroutine 一直得不到資源時,則會進入饑餓模式,直接將鎖資源交給隊頭 goroutine,讓新來的 goroutine 阻塞并加入到等待隊列的隊尾里
  • 對于饑餓模式將會持續(xù)到?jīng)]有阻塞等待喚起的 goroutine 隊列時,才會解除

Unlock() 代碼詳解

// Unlock 對 mutex 解鎖. // 如果沒有上過鎖,缺調(diào)用此方法解鎖,將會拋出運行時錯誤。 // 它將允許在不同的 Goroutine 上進行上鎖解鎖 func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}// 快速嘗試解鎖new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {// 快速解鎖失敗,將進行操作較多的解鎖動作。m.unlockSlow(new)} }func (m *Mutex) unlockSlow(new int32) {// 非上鎖狀態(tài),直接拋出異常if (new+mutexLocked)&mutexLocked == 0 {throw("sync: unlock of unlocked mutex")}// 正常模式if new&mutexStarving == 0 {old := newfor {// 沒有需要喚起的等待隊列if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// 喚起等待隊列并數(shù)量-1new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {//饑餓模式,將鎖直接給等待隊列的隊頭 goroutineruntime_Semrelease(&m.sema, true, 1)} }

相對于lock()方法, Unlock() 則比較簡單,會先進行快速的解鎖,即沒有等待喚起的 goroutine,則不需要繼續(xù)做其他動作。

如果當(dāng)前是正常模式,則簡單的喚起隊頭 Goroutine。如果是饑餓模式,則會直接將鎖交給隊頭 Goroutine,然后喚起隊頭 Goroutine,讓它繼續(xù)運行。

下面我們分析一下加鎖和解鎖的過程,加鎖分紅功和失敗兩種狀況,成功的話直接獲取鎖,失敗后當(dāng)前線程被阻塞,一樣,解鎖時跟據(jù)是否有阻塞線程也有兩種處理。

加解鎖過程

加鎖(Lock)

第一種情況:當(dāng)前只有一個線程在加鎖,沒有其余線程操作


在加鎖過程先去判斷 Locked 標(biāo)志位是否為 0,如果為 0 就把 Locked 置為 1,表示已經(jīng)加鎖成功。從上圖可見,加鎖成功后,只是 Locked 位置為 1,其余狀態(tài)位沒發(fā)生變化。

第二種情況:假設(shè)線程 B 想要加鎖,但是鎖已經(jīng)被其他線程獨占了


從上圖可以看到,當(dāng)線程 B 對一個已被占用的鎖再次加鎖時,Waiter 計數(shù)器增長為 1,此時線程 B 將被阻塞,直到Locked 值變?yōu)?0 后才會被喚醒。

解鎖(Unlock)

第一種情況:當(dāng)前只有一個線程在解鎖,沒有其余線程阻塞


因為沒有其余線程阻塞等待加鎖,所以解鎖時只需要將 Locked 置為 0 就可以,不需要釋放信號量。

第二種情況:加鎖姐鎖過程,有多個線程被阻塞了


線程 A 解鎖過程分為兩個步驟,一是把 Locked 置為 0,二是查看到 Waiter > 0,因此釋放一個信號量,喚醒一個阻塞的協(xié)程,被喚醒的線程 B 把 Locked 置為 1,因而線程 B 得到鎖。

上面只是說了 Waiter 和 Locked ,這里也說一下其他兩個 starvation 和 Woken 作用

starvation狀態(tài)

自旋過程當(dāng)中能搶到鎖,必定意味著同一時刻有線程釋放了鎖,釋放鎖時若是發(fā)現(xiàn)有阻塞等待的協(xié)程。還會釋放一個信號量來喚醒一個等待線程,被喚醒的線程獲得 CPU 后開始運行,此時發(fā)現(xiàn)鎖已被搶占了,本身只好再次阻塞,不過阻塞前會判斷自上次阻塞到本次阻塞通過了多長時間,若是超過1ms的話,會將Mutex標(biāo)記為"饑餓"模式,而后再阻塞。

處于饑餓模式下,不會啟動自旋過程,也即一旦有線程釋放了鎖,那么必定會喚醒其他線程,被喚醒的線程將會成功獲取鎖,同時也會把等待計數(shù)減1。

Woken狀態(tài)

Woken 狀態(tài)用于加鎖和解鎖過程的通訊,舉個例子,同一時刻,兩個線程一個在加鎖,一個在解鎖,在加鎖的協(xié)程可能在自旋過程當(dāng)中,此時把 Woken 標(biāo)記為1,用于通知解鎖線程沒必要釋放信號量了。

自旋鎖

自旋過程

加鎖時,若是當(dāng)前 Locked 為 1,說明該鎖當(dāng)前由其余線程獨占了,嘗試加鎖的線程并非立刻轉(zhuǎn)入阻塞,而是會持續(xù)的檢測 Locked 是否變?yōu)?0,這個過程即為自旋過程。

什么是自旋鎖

當(dāng)一個線程嘗試去獲取某一把鎖的時候,如果這個鎖此時已經(jīng)被別的線程占用,那么此線程就無法獲取到這把鎖,該線程將會等待,間隔一段時間后會再次嘗試獲取。這種采用循環(huán)加鎖 -> 等待的機制被稱為自旋鎖(spinlock)。

解決自旋鎖 CPU 占用

自旋鎖的目的是占著 CPU 資源不進行釋放,這種情況一個很好的方式是給自旋鎖設(shè)定一個自旋時間,等時間一到立即釋放自旋鎖,等到獲取鎖立即進行處理。但是如何去選擇自旋時間呢?如果自旋執(zhí)行時間太長,會有大量的線程處于自旋狀態(tài)占用 CPU 資源,進而會影響整體系統(tǒng)的性能。因此自旋的周期選的額外重要!JDK在1.6 引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋時間不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態(tài)來決定,基本認(rèn)為一個線程上下文切換的時間是最佳的一個時間。

自旋鎖的優(yōu)點

自旋鎖盡可能的減少線程的阻塞,這對于鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于線程阻塞掛起再喚醒的操作的消耗,這些操作會導(dǎo)致線程發(fā)生兩次上下文切換!

自旋鎖的缺點

如果鎖的競爭激烈,或者持有鎖的線程需要長時間占用鎖執(zhí)行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用 CPU 做無用功,同時有大量線程在競爭一個鎖,會導(dǎo)致獲取鎖的時間很長,線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要 CPU 的線程又不能獲取到 CPU,造成 CPU 的浪費。所以這種情況下我們要關(guān)閉自旋鎖。

互斥鎖、自旋鎖對于加鎖失敗后的處理方式

  • 互斥鎖加鎖失敗后,線程會釋放 CPU ,給其他線程
  • 自旋鎖加鎖失敗后,線程會忙等待,直到它拿到鎖

自旋鎖與互斥鎖使用上比較相似,但實現(xiàn)上完全不同:當(dāng)加鎖失敗時,互斥鎖用線程切換來應(yīng)對,自旋鎖則用忙等待來應(yīng)對。

總結(jié)

以上是生活随笔為你收集整理的面试官:讲讲互斥锁、自旋锁吧的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。