我先講解法,再講為甚麼volatile沒用
那我會搭配文檔講,但其實不管情境怎麼會概念還是一樣的,這裡範例選用和文檔一樣的從1加到10萬
前一篇我們講過了AtomicReference,說明了它會保證不同thread對它的操作是一致的
val counter = AtomicInteger()
在簡單的情境下,這是最快的解法,但它並不適合更複雜的情況,在開發的擴充性也不那麼友善
既然多線程會有問題,要不選擇單線程?
withContext(Dispatchers.Default) {
massiveRun {
// 将每次自增限制在单线程上下文中
withContext(counterContext) {
counter++
}
}
}
在單線程裡,一次就是執行一個任務,所以他保證了每次都只會有一個任務去加1,且每次操作都會從DEFAULT切換到counterContext,缺點也很明顯,速度慢
每次看中文翻譯,反而不知道是甚麼意思
// 将一切都限制在单线程上下文中
withContext(counterContext) {
massiveRun {
counter++
}
}
基本和上面概念一樣,但不用一直切換Dispatcher
前一篇我們也提到了mutex,而概念也很簡單,透過鎖讓任務排隊,只能一個一個執行
withContext(Dispatchers.Default) {
massiveRun {
// 用锁保护每次自增
mutex.withLock {
counter++
}
}
}
連結
連結
英文連結
實現併發通常有兩種解法,共享數據和消息傳遞,而共享數據的方式會面對數據競爭,也就需要上面的mutex鎖
而另一種方法就是透過消息傳遞,比較有名的用法就是golang的channel和erlang的actor
那actor的解說,只看kotlin文檔通常是看不懂,這裡介紹一下基本概念
actor會封裝自己的狀態,且透過message寄到其他actor的mailbox與其通訊,而不是直接通訊,那好處是什麼
透過封裝,只有actor可以修改自己的變數,無法從外部修改,而內部使用單線程執行任務,那相對於共享數據的方式,actor不用關心mutex鎖或是atomic原子性
那actor是如何實現異步處理的?
actor的mailbox大概長這樣,透過queue方式排序任務,而所有actor實例彼此獨立,這樣的設計完成解偶和隔離
//轉自文檔
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要解決甚麼問題
首先常見的硬體記憶體大致上分記憶體(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
資料來源
volatile 輕度了解看這篇
volatile 想深入了解的話看這篇