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 想深入了解的话看这篇


<<:  Day 29 Rails soft delete - paranoia

>>:  Day-30 跳页传值

Day 16:AWS是什麽?30天从动漫/影视作品看AWS服务应用 -《云端情人》part 3

Samantha和Theodore在一起的时候,时常提及自己在写钢琴曲,灵感来自於和Theodore...

Day08. 後疫情的新常态,运用Blue Prism「超前布署」好运自创-BP从Excel新建一个工作表

这两天台湾疫情又告急,基於同岛一命的概念防疫的习惯确实不宜松懈, 戴了一天的口罩,回到家里通常懒虫上...

图的深度广度检查 - DAY 25

深度优先检查 像是走迷宫一样,摸着同一面(左或右)墙,一路走,遇到已经标住过的节点,就选择其他节点走...

结语

结语 目前完成进度 登入 登出 朋友的邀请与拒绝和同意 新增聊天室 新增讯息 尚未完成 讯息显示 未...

【Day 24】- 用方便的 Postman 储存或测试 API

前情提要 昨天带各位用 Selenium 写了自动发留言的 Discord 机器人,可以在指定的文字...