【翻譯】kotlin協(xié)程核心庫文檔(六)—— 共享的可變狀態(tài)和并發(fā)

github原文地址

原創(chuàng)翻譯意敛,轉(zhuǎn)載請保留或注明出處:http://www.reibang.com/p/01d26fbc9b80

共享的可變狀態(tài)和并發(fā)


協(xié)程可用多線程調(diào)度器(比如默認(rèn)的 CommonPool )并發(fā)執(zhí)行妈经。這樣就可以提出所有常見的并發(fā)問題凿跳。主要的問題是同步訪問共享的可變狀態(tài)秒裕。協(xié)程領(lǐng)域?qū)@個問題的一些解決方案類似于多線程領(lǐng)域中的解決方案烈评,但其他解決方案則是獨一無二的凉袱。

問題

我們啟動一千個協(xié)程逗柴,它們都做一千次相同的動作(總計100萬次執(zhí)行)。我們同時會測量它們的完成時間院塞,以便進(jìn)一步的比較:

suspend fun massiveRun(context: CoroutineContext, action: suspend () -> Unit) {
    val n = 1000 // number of coroutines to launch
    val k = 1000 // times an action is repeated by each coroutine
    val time = measureTimeMillis {
        val jobs = List(n) {
            launch(context) {
                repeat(k) { action() }
            }
        }
        jobs.forEach { it.join() }
    }
    println("Completed ${n * k} actions in $time ms")
}

我們從一個非常簡單的動作開始:在多線程 CommonPool 上下文遞增一個共享的可變變量遮晚。

var counter = 0

fun main(args: Array<String>) = runBlocking<Unit> {
    massiveRun(CommonPool) {
        counter++
    }
    println("Counter = $counter")
}

獲取完整代碼 here

這段代碼最后打印出什么結(jié)果?它不太可能打印出“Counter = 1000000”拦止,因為一千個協(xié)程從多個線程同時遞增計數(shù)器而且沒有做同步并發(fā)處理县遣。

注意:如果你的運行機(jī)器使用兩個或者更少的cpu,那么你總是會看到1000000汹族,因為CommonPool在這種情況下只會在一個線程中運行萧求。要重現(xiàn)這個問題,可以做如下的變動:

val mtContext = newFixedThreadPoolContext(2, "mtPool") // explicitly define context with two threads
var counter = 0

fun main(args: Array<String>) = runBlocking<Unit> {
    massiveRun(mtContext) { // use it instead of CommonPool in this sample and below
        counter++
    }
    println("Counter = $counter")
}

獲取完整代碼 here

沒有發(fā)揮作用的volatile

有一種常見的誤解:volatile 可以解決并發(fā)問題顶瞒。讓我們嘗試一下:

@Volatile // in Kotlin `volatile` is an annotation 
var counter = 0

fun main(args: Array<String>) = runBlocking<Unit> {
    massiveRun(CommonPool) {
        counter++
    }
    println("Counter = $counter")
}

獲取完整代碼 here

這段代碼運行速度更慢了夸政,但我們?nèi)匀粵]有得到 “Counter = 1000000”,因為 volatile 變量保證可線性化(這是“原子”的技術(shù)術(shù)語)讀取和寫入變量榴徐,但在大量動作(在我們的示例中即“遞增”操作)發(fā)生時并不提供原子性守问。

線程安全的數(shù)據(jù)結(jié)構(gòu)

一種對線程、協(xié)程都有效的常規(guī)解決方法坑资,就是使用線程安全(也稱為同步的耗帕、可線性化、原子)的數(shù)據(jù)結(jié)構(gòu)袱贮,它為需要在共享狀態(tài)上執(zhí)行的相應(yīng)操作提供所有必需的同步處理仿便。在簡單的計數(shù)器場景中,我們可以使用具有 incrementAndGet 原子操作的AtomicInteger 類:

var counter = AtomicInteger()

fun main(args: Array<String>) = runBlocking<Unit> {
    massiveRun(CommonPool) {
        counter.incrementAndGet()
    }
    println("Counter = ${counter.get()}")
}

獲取完整代碼 here

這是針對此類特定問題的最快解決方案。它適用于普通計數(shù)器嗽仪、集合荒勇、隊列和其他標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu)以及它們的基本操作。然而钦幔,它并不容易擴(kuò)展為應(yīng)對復(fù)雜狀態(tài)枕屉、或復(fù)雜操作沒有現(xiàn)成的線程安全實現(xiàn)的情況。

以細(xì)粒度限制線程

限制線程是解決共享可變狀態(tài)問題的一種方案鲤氢,其中對特定共享狀態(tài)的所有訪問權(quán)都限制在單個線程中搀擂。它通常應(yīng)用于UI程序中:所有UI狀態(tài)都局限于單個事件分發(fā)線程或應(yīng)用主線程中。這在協(xié)程中很容易實現(xiàn)卷玉,通過使用一個單線程上下文:

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main(args: Array<String>) = runBlocking<Unit> {
    massiveRun(CommonPool) { // run each coroutine in CommonPool
        withContext(counterContext) { // but confine each increment to the single-threaded context
            counter++
        }
    }
    println("Counter = $counter")
}

獲取完整代碼 here

這段代碼運行非常緩慢哨颂,因為它進(jìn)行了細(xì)粒度的線程限制。每個增量操作都得使用 withContext 塊從多線程 CommonPool 上下文切換到單線程上下文相种。

以粗粒度限制線程

在實踐中威恼,線程限制是在大段代碼中執(zhí)行的,例如:狀態(tài)更新類業(yè)務(wù)邏輯中大部分都是限于單線程中寝并。下面的示例演示了這種情況箫措,在單線程上下文中運行每個協(xié)程。

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main(args: Array<String>) = runBlocking<Unit> {
    massiveRun(counterContext) { // run each coroutine in the single-threaded context
        counter++
    }
    println("Counter = $counter")
}

獲取完整代碼 here

這段代碼運行更快而且打印出了正確的結(jié)果衬潦。

互斥

該問題的互斥解決方案是使用永遠(yuǎn)不會同時執(zhí)行的關(guān)鍵代碼塊來保護(hù)共享狀態(tài)的所有修改斤蔓。在阻塞的世界中,你通常會使用 synchronized 或者 ReentrantLock 镀岛。在協(xié)程中的替代品叫做 Mutex 弦牡。它具有 lockunlock 方法,可以隔離關(guān)鍵的部分漂羊。關(guān)鍵的區(qū)別在于 Mutex.lock() 是一個掛起函數(shù)驾锰,它不會阻塞線程。

還有 withLock 擴(kuò)展函數(shù)走越,可以方便的替代常用的 mutex.lock(); try { ... } finally { mutex.unlock() } 模式:

val mutex = Mutex()
var counter = 0

fun main(args: Array<String>) = runBlocking<Unit> {
    massiveRun(CommonPool) {
        mutex.withLock {
            counter++
        }
    }
    println("Counter = $counter")
}

獲取完整代碼 here

此示例中鎖是細(xì)粒度的椭豫,因此會付出一些代價。但是對于某些必須定期修改共享狀態(tài)的場景旨指,它是一個不錯的選擇赏酥,但是沒有自然線程可以限制此狀態(tài)。

Actors

一個 actor 是由若干元素組成的一個實體:一個協(xié)程淤毛、它的狀態(tài)受限封裝在此協(xié)程中、以及一個與其他協(xié)程通信的 channel 算柳。一個簡單的 actor 可以簡單的寫成一個函數(shù)低淡,但是一個擁有復(fù)雜狀態(tài)的 actor 更適合由類來表示。

有一個 actor 協(xié)程構(gòu)建器,它可以方便地將 actor 的郵箱 channel 組合到其作用域中(用來接收消息)蔗蹋、組合發(fā)送 channel 與結(jié)果集對象何荚,這樣對 actor 的單個引用就可以作為其句柄持有。

使用 actor 的第一步是定一個 actor 要處理的消息類猪杭。Kotlin 的 sealed classes 密封類很適合這種場景餐塘。我們使用 IncCounter 消息(用來遞增計數(shù)器)和 GetCounter 消息(用來獲取值)來定義 CounterMsg 密封類。后者需要發(fā)送回復(fù)皂吮。CompletableDeferred 通信原語表示未來可知(傳達(dá))的單個值戒傻,此處用于此目的。

// Message types for counterActor
sealed class CounterMsg
object IncCounter : CounterMsg() // one-way message to increment counter        
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // a request with reply

接下來我們定義一個函數(shù)蜂筹,使用 actor 協(xié)程構(gòu)建器來啟動一個 actor:

// This function launches a new counter actor
fun counterActor() = actor<CounterMsg> {
    var counter = 0 // actor state
    for (msg in channel) { // iterate over incoming messages
        when (msg) {
            is IncCounter -> counter++
            is GetCounter -> msg.response.complete(counter)
        }
    }
}

主函數(shù)代碼很簡單:

fun main(args: Array<String>) = runBlocking<Unit> {
    val counter = counterActor() // create the actor
    massiveRun(CommonPool) {
        counter.send(IncCounter)
    }
    // send a message to get a counter value from an actor
    val response = CompletableDeferred<Int>()
    counter.send(GetCounter(response))
    println("Counter = ${response.await()}")
    counter.close() // shutdown the actor
}

獲取完整代碼 here

actor 本身執(zhí)行所處上下文的正確性無關(guān)緊要需纳。一個 actor 是一個協(xié)程,而一個協(xié)程是按順序執(zhí)行的艺挪,因此將狀態(tài)限制到特定協(xié)程可以解決共享可變狀態(tài)的問題不翩。實際上,actor 可以修改自己的私有狀態(tài)麻裳,但只能通過消息互相影響(避免任何鎖定)口蝠。

actor 在高負(fù)載下比鎖更有效,因為在這種情況下它總是有工作要做津坑,而且根本不需要切換到不同的上下文妙蔗。

注意, actor 協(xié)程構(gòu)建器是 produce 協(xié)程構(gòu)建器的雙重構(gòu)件国瓮。一個 actor 與它接收消息的 channel 相關(guān)聯(lián)灭必,而一個 producer 與它發(fā)送元素的 channel 相關(guān)聯(lián)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乃摹,一起剝皮案震驚了整個濱河市禁漓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌孵睬,老刑警劉巖播歼,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異掰读,居然都是意外死亡秘狞,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門蹈集,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烁试,“玉大人,你說我怎么就攤上這事拢肆〖跸欤” “怎么了靖诗?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長支示。 經(jīng)常有香客問我刊橘,道長,這世上最難降的妖魔是什么颂鸿? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任促绵,我火速辦了婚禮,結(jié)果婚禮上嘴纺,老公的妹妹穿的比我還像新娘败晴。我一直安慰自己,他們只是感情好颖医,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布位衩。 她就那樣靜靜地躺著,像睡著了一般熔萧。 火紅的嫁衣襯著肌膚如雪糖驴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天佛致,我揣著相機(jī)與錄音贮缕,去河邊找鬼。 笑死俺榆,一個胖子當(dāng)著我的面吹牛感昼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播罐脊,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼定嗓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了萍桌?” 一聲冷哼從身側(cè)響起宵溅,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎上炎,沒想到半個月后恃逻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡藕施,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年寇损,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裳食。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡矛市,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诲祸,到底是詐尸還是另有隱情浊吏,我是刑警寧澤憨愉,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站卿捎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏径密。R本人自食惡果不足惜午阵,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望享扔。 院中可真熱鬧底桂,春花似錦、人聲如沸惧眠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氛魁。三九已至暮顺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秀存,已是汗流浹背捶码。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留或链,地道東北人惫恼。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像澳盐,于是被迫代替她去往敵國和親祈纯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359