說一說Kotlin協程中的同步鎖——Mutex

前言

在多線程并發的情況下會很容易出現同步問題,這時候就需要使用各種鎖來避免這些問題,在java開發中,最常用的就是使用synchronized。kotlin的協程也會遇到這樣的問題,因為在協程線程池中會同時存在多個運行的Worker,每一個Worker都是一個線程,這樣也會有并發問題。

雖然kotlin中也可以使用synchronized,但是有很大的問題。因為synchronized當獲取不到鎖的時候,會阻塞線程,這樣這個線程一段時間內就無法處理其他任務,這不符合協程的思想。為此,kotlin提供了一個協程中可以使用的同步鎖——Mutex

Mutex

Mutex使用起來也非常簡單,只有幾個函數lock、unlock、tryLock,一看名字就知道是什么。還有一個holdsLock,就是返回當前鎖的狀態。

這里要注意,lock和unlock必須成對出現,tryLock返回true的之后也必須在使用完執行unlock。這樣使用的時候就比較麻煩,所以kotlin還提供了一個擴展函數withLock,它與synchronized類似,會在代碼執行完成或異常的時候自動釋放鎖,這樣就避免了忘記釋放鎖導致程序出錯的情況。

withLock

withLock的代碼如下:

public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {
    contract { 
        callsInPlace(action, InvocationKind.EXACTLY_ONCE)
    }

    lock(owner)
    try {
        return action()
    } finally {
        unlock(owner)
    }
}

代碼非常簡單,就是先lock一下,然后執行代碼,最終在finally中釋放鎖,這樣就保證了鎖一定會被釋放。

lock

這樣一看mutex好像跟synchronized或其他java的鎖差不多,那么為什么它是如何解決線程阻塞的問題呢。

這就要從lock和unlock的流程中來看,先來看看lock:

public override suspend fun lock(owner: Any?) {
    // fast-path -- try lock
    if (tryLock(owner)) return
    // slow-path -- suspend
    return lockSuspend(owner)
}

先是通過tryLock來獲取鎖,如果獲取到了就直接返回執行代碼。重點來看獲取不到是如何處理的,獲取不到的時候會執行lockSuspend,它的代碼如下:

private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable<Unit> sc@ { cont ->
    var waiter = LockCont(owner, cont)  //1
    _state.loop { state ->
        when (state) {
            is Empty -> {
                if (state.locked !== UNLOCKED) {  //2
                    _state.compareAndSet(state, LockedQueue(state.locked)) //3
                } else {
                    // try lock
                    val update = if (owner == null) EMPTY_LOCKED else Empty(owner)
                    if (_state.compareAndSet(state, update)) { // locked
                        cont.resume(Unit) { unlock(owner) } //4
                        return@sc
                    }
                }
            }
            is LockedQueue -> {
                val curOwner = state.owner
                check(curOwner !== owner) { "Already locked by $owner" }

                state.addLast(waiter)  //5

                if (_state.value === state || !waiter.take()) {  //6
                    // added to waiter list
                    cont.removeOnCancellation(waiter)
                    return@sc
                }

                waiter = LockCont(owner, cont)
                return@loop
            }
            is OpDescriptor -> state.perform(this) // help
            else -> error("Illegal state $state")
        }
    }
}

可以看到這個函數是被suspend修飾的,所以這個是可掛起的函數,當執行到這里的時候線程就被掛起了,如果沒有立刻恢復,而且有其他任務,那么線程就可以先執行其他任務,這樣就不會阻塞住了。那么是如何恢復的。

函數一開始創建了一個LockCont對象waiter,這個是后面的關鍵,不過現在還用不到。

Empty

繼續看根據不同的狀態執行不同的代碼,先看看Empty(等待列表為空)狀態,再判斷一下當前是否加鎖(代碼2),如果不是非加鎖則將狀態設置為LockedQueue狀態(代碼3);如果當前是非加鎖,則獲取鎖,獲取到之后執行resume來喚醒線程來執行后續代碼(代碼4),這種情況基本就是立刻獲取到鎖,所以不在這里細說了。

上面說了如果等待列表為空并且無法立刻獲取鎖,就會切換到LockedQueue狀態(代碼3),所以只要當前無法獲取鎖,最終都會進行LockedQueue狀態,那么來看看這個狀態怎么處理的。

LockedQueue

這個狀態會就將函數一開始創建的waiter添加到state中(代碼5),然后還是再判斷一次當前狀態,因為這時候可能鎖的狀態已經改變了,如果沒有變則直接就返回了。

注意看到每個狀態里,都會反復的校驗當前鎖的狀態。

可以看到在LockedQueue這個流程結束后并沒有恢復線程,線程則一直是掛起狀態,所以在恢復之前線程是可以處理其他事務的。

那么線程何時恢復?

unlock

來看看unlock代碼:

override fun unlock(owner: Any?) {
    _state.loop { state ->
        when (state) {
            is Empty -> {
                ...
            }
            is OpDescriptor -> state.perform(this)
            is LockedQueue -> {
                if (owner != null)
                    check(state.owner === owner) { "Mutex is locked by ${state.owner} but expected $owner" }
                val waiter = state.removeFirstOrNull()  //1
                if (waiter == null) {
                    ...
                } else {
                    if ((waiter as LockWaiter).tryResumeLockWaiter()) { //2
                        state.owner = waiter.owner ?: LOCKED
                        waiter.completeResumeLockWaiter() //3
                        return
                    }
                }
            }
            else -> error("Illegal state $state")
        }
    }
}

上面我們將waiter放入了等待隊列中,這時候狀態是LockedQueue,所以在unlock函數中我們直接看這個狀態的代碼。

代碼1處從state中取出第一個元素,即waiter。前一個釋放鎖之后,就會把鎖分配給這個waiter。然后在代碼2處執行了它的tryResumeLockWaiter函數,如果返回false,還會執行它的completeResumeLockWaiter函數。

LockCont

上面知道waiter是一個LockCont對象,我們來看看它的源碼:

private inner class LockCont(
    owner: Any?,
    private val cont: CancellableContinuation<Unit>
) : LockWaiter(owner) {

    override fun tryResumeLockWaiter(): Boolean {
        if (!take()) return false
        return cont.tryResume(Unit, idempotent = null) {
            unlock(owner)
        } != null
    }

    override fun completeResumeLockWaiter() = cont.completeResume(RESUME_TOKEN)
    ...
}

可以看到在tryResumeLockWaiter函數中會執行cont的tryResume來嘗試喚醒它對應的線程來執行代碼。

如果這個動作沒有成功,最后會在completeResumeLockWaiter函數中執行cont的completeResume來喚醒線程。

總結

Mutex的內部邏輯其實并不復雜,如果獲取不到鎖則會掛起線程并加入到等待隊列中,等獲取到鎖的時候在喚醒線程來執行代碼。而這段時間內線程,或者說Worker可以執行其他任務,這樣不會阻塞線程,最大的利用了線程的資源,這就很kotlin。

所以大家在處理協程的同步問題的時候,盡量使用Mutex這種Kotlin專門為協程開發的工具,這樣才能更好的發揮協程的能力。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容