iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 29

day29 大量操作怎麼辦? 連volatile都救不了我QQ

我先講解法,再講為甚麼volatile沒用

那我會搭配文檔講,但其實不管情境怎麼會概念還是一樣的,這裡範例選用和文檔一樣的從1加到10萬

atomic原子性

前一篇我們講過了AtomicReference,說明了它會保證不同thread對它的操作是一致的

val counter = AtomicInteger()

在簡單的情境下,這是最快的解法,但它並不適合更複雜的情況,在開發的擴充性也不那麼友善

單線程執行

既然多線程會有問題,要不選擇單線程?

fine-grained(細粒度控制)

withContext(Dispatchers.Default) {
    massiveRun {
        // 将每次自增限制在单线程上下文中
        withContext(counterContext) {
            counter++
        }
    }
}

在單線程裡,一次就是執行一個任務,所以他保證了每次都只會有一個任務去加1,且每次操作都會從DEFAULT切換到counterContext,缺點也很明顯,速度慢

每次看中文翻譯,反而不知道是甚麼意思

coarse-grained(粗粒度控制)

// 将一切都限制在单线程上下文中
withContext(counterContext) {
    massiveRun {
        counter++
    }
}

基本和上面概念一樣,但不用一直切換Dispatcher

mutex鎖

前一篇我們也提到了mutex,而概念也很簡單,透過鎖讓任務排隊,只能一個一個執行

withContext(Dispatchers.Default) {
    massiveRun {
        // 用锁保护每次自增
        mutex.withLock {
            counter++
        }
    }
}

actor

連結
連結
英文連結
實現併發通常有兩種解法,共享數據和消息傳遞,而共享數據的方式會面對數據競爭,也就需要上面的mutex鎖

而另一種方法就是透過消息傳遞,比較有名的用法就是golang的channel和erlang的actor

那actor的解說,只看kotlin文檔通常是看不懂,這裡介紹一下基本概念
actor

actor會封裝自己的狀態,且透過message寄到其他actor的mailbox與其通訊,而不是直接通訊,那好處是什麼

透過封裝,只有actor可以修改自己的變數,無法從外部修改,而內部使用單線程執行任務,那相對於共享數據的方式,actor不用關心mutex鎖或是atomic原子性

那actor是如何實現異步處理的?
mailbox
actor的mailbox大概長這樣,透過queue方式排序任務,而所有actor實例彼此獨立,這樣的設計完成解偶和隔離

在kotlin如何實現

別人的寫法

//轉自文檔
sealed class Counter
object IncCounter : Counter() // one-way message to increment counter
class GetCounter(val response: CompletableDeferred<Int>) : Counter()
fun CoroutineScope.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)
        }
    }
}
fun main() = runBlocking<Unit> {
    val counter = counterActor() // create the actor
    withContext(Dispatchers.Default) {
        massiveRun {
            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
}

恩,這邊有趣的是,如果直接看文檔,通常會看不懂,但先了解actor後,再看文檔,就變成,對呀就這樣子,還是解釋一下,在massiveRun裡面的counter.send(),就是將訊息送到actor的mailbox,真正執行的在第二段,這邊是不是看到一個很熟悉的東西,for (msg in channel),沒錯,他是以< T>ReceiveChannel< T>實現的,所以也能夠改成consumeEach

for (msg in channel) { // iterate over incoming messages
    when (msg) {
        is IncCounter -> counter++
        is GetCounter -> msg.response.complete(counter)
    }
}

可以看這裡了解channel和actor的差異,儘管他們是討論golang和Erlang

為什麼volatile沒用

讓我先簡單講一下volatile要解決甚麼問題
首先常見的硬體記憶體大致上分記憶體(ram)和硬碟(ssd, hhd),這是我們一般購買電子產品時會考量的因素
但對cpu來講,他們的讀寫都太慢了,像我在day1提到的,program載入到ram會變成process,這時資料是存在記憶體裡面的,但執行程式這個動作是由cpu來完成,而cpu在執行程式時,三不五時就需要資料,他就會去記憶體讀取這個資料,獲取資料後接著執行

但光cpu快沒用,io操作的耗時和cpu的執行速度還是有落差,於是cpu廠商加入了CPU快取,記憶體裡面的資料,快取裡也有一份,這樣直接拿快取的資料即可,當程式要修改資料時,一樣先在快取裡面操作,等運算結束後再移併寫回記憶體即可

這樣的作法在單核的情況下是沒問題的,因為一個運算單元一次只能處理一件事,但現在多數的裝置已經發展到多核,就是一個cpu裡面有多個運算單元,可以同時處理多個任務

那問題來了,多執行續和cpu快取的問題在於,他們的局部变量可能并不指向同一个值,day6講過,這是因為thread1和thread2的cpu快取中各自有一個值,比如說從0加到100000,最後印出來可能9萬多

那該怎麼辦呢? android給了一個關鍵字,volatile,直譯是可揮發的,而他解決了兩個問題
第一個就是,可見性問題,加了這個關鍵字的變數,任何一個執行緒對他的修改,都會讓其他cpu快取記憶體的值過期,這樣就必須重新去記憶體拿最新的值
另一個問題就是指令重排,cpu在執行程式時不會嚴格按照開發者編寫的順序執行,在考慮到效能的情況之下,他會對無關簡要的代碼重新排列,ex.聲明2個變數

好了,先打斷,再講就離題了,下面有更詳細的連結

這個方案聽起來完美的解決了多執行緒的問題呀,為什麼再coroutine行不通呢

文檔解釋
因为 volatile 变量保证可线性化(这是“原子”的技术术语)读取和写入变量,但在大量动作(在我们的示例中即“递增”操作)发生时并不提供原子性。
because volatile variables guarantee linearizable (this is a technical term for "atomic") reads and writes to the corresponding variable, but do not provide atomicity of larger actions (increment in our case)

借一下別人的字節碼

private static void increase();
    descriptor: ()V
    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #7                  // Field value:I
         3: iconst_1
         4: iadd
         5: putstatic     #7                  // Field value:I
         8: return
      LineNumberTable:
        line 12: 0
        line 13: 8

可以看到value++是由四条指令构成的,分别是getstatic、iconst_1、iadd和putstatic,而volatile只能保證getstatic
資料來源

輕度了解看這篇
想深入了解的話看這篇

連結

必看

文檔
英文文檔

選看

actor
actor
actor

volatile 輕度了解看這篇
volatile 想深入了解的話看這篇


上一篇
day28 等一下啦,會壞掉的/// Coroutine併發操作的重複請求
下一篇
day30 Kotlin coroutine 結賽統整
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30

尚未有邦友留言

立即登入留言