Kotlin Coroutines(协程) 完全解析(五),协程的并发

Kotlin Coroutines(协程) 完全解析系列:

Kotlin Coroutines(协程) 完全解析(一),协程简介

Kotlin Coroutines(协程) 完全解析(二),深入理解协程的挂起、恢复与调度

Kotlin Coroutines(协程) 完全解析(三),封装异步回调、协程间关系及协程的取消

Kotlin Coroutines(协程) 完全解析(四),协程的异常处理

Kotlin Coroutines(协程) 完全解析(五),协程的并发

本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1

通过前面几篇文章可以明白协程就是可以挂起和恢复执行的运算逻辑,挂起函数用状态机的方式用挂起点将协程的运算逻辑拆分为不同的片段,每次运行协程执行的不同的逻辑片段。所以协程在运行时只是线程中的一块代码,线程的并发处理方式都可以用在协程上。不过协程还提供两种特有的方式,一是不阻塞线程的互斥锁Mutex,一是通过 ThreadLocal 实现的协程局部数据。

1. Mutex

线程中锁都是阻塞式,在没有获取锁时无法执行其他逻辑,而协程可以通过挂起函数解决这个,没有获取锁就挂起协程,获取后再恢复协程,协程挂起时线程并没有阻塞可以执行其他逻辑。这种互斥锁就是Mutex,它与synchronized关键字有些类似,还提供了withLock扩展函数,替代常用的mutex.lock; try {...} finally { mutex.unlock() }:

fun main(args: Array<String>) = runBlocking<Unit> {
    val mutex = Mutex()
    var counter = 0
    repeat(10000) {
        GlobalScope.launch {
            mutex.withLock {
                counter ++
            }
        }
    }
    println("The final count is $counter")
}

Mutex的使用比较简单,不过需要注意的是多个协程竞争的应该是同一个Mutex互斥锁。

2. 协程局部数据

线程中可以使用ThreadLocal作为线程局部数据,每个线程中的数据都是独立的。协程中可以通过ThreadLocal.asContextElement()扩展函数实现协程局部数据,每次协程切换会恢复之前的值。先看下面的示例:

fun main(args: Array<String>) = runBlocking<Unit> {
    val threadLocal = ThreadLocal<String>().apply { set("Init") }
    printlnValue(threadLocal)
    val job = GlobalScope.launch(threadLocal.asContextElement("launch")) {
        printlnValue(threadLocal)
        threadLocal.set("launch changed")
        printlnValue(threadLocal)
        yield()
        printlnValue(threadLocal)
    }
    job.join()
    printlnValue(threadLocal)
}

private fun printlnValue(threadLocal: ThreadLocal<String>) {
    println("${Thread.currentThread()} thread local value: ${threadLocal.get()}")
}

输出如下:

Thread[main,5,main] thread local value: Init
Thread[DefaultDispatcher-worker-1,5,main] thread local value: launch
Thread[DefaultDispatcher-worker-1,5,main] thread local value: launch changed
Thread[DefaultDispatcher-worker-2,5,main] thread local value: launch
Thread[main,5,main] thread local value: Init

上面的输出有个疑问的地方,为什么执行yield()挂起函数后 threadLocal 的值不是launch changed而变回了launch?

下面直接分析源码:

// 注意这里 value 的默认值是 ThreadLocal 当前值
public fun <T> ThreadLocal<T>.asContextElement(value: T = get()): ThreadContextElement<T> =
    ThreadLocalElement(value, this)

internal class ThreadLocalElement<T>(
    private val value: T,
    private val threadLocal: ThreadLocal<T>
) : ThreadContextElement<T> {
    override val key: CoroutineContext.Key<*> = ThreadLocalKey(threadLocal)

    override fun updateThreadContext(context: CoroutineContext): T {
        val oldState = threadLocal.get()
// 设置 threadLocal 的值为 value 前先保存了之前的值
        threadLocal.set(value)
        return oldState
    }

    override fun restoreThreadContext(context: CoroutineContext, oldState: T) {
// 将 threadLocal 修改为之前保存的值
        threadLocal.set(oldState)
    }
    ...
}

// 协程启动和恢复都会用此函数包装,在 Dispatched.run()、DisptchedContinuation.resumeWith() 、
// DisptchedContinuation.resumeUndispatched() 等协程启动和恢复的地方都可以发现此函数的踪影
internal actual inline fun <T> withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T {
// updateThreadContext() 函数会调用到 ThreadContextElement.updateThreadContext(context)
// oldValue 是 threadLocal 之前的值
    val oldValue = updateThreadContext(context, countOrElement)
    try {
        return block()
    } finally {
// restoreThreadContext() 函数会调用到 ThreadContextElement.restoreThreadContext(context, oldValue)
        restoreThreadContext(context, oldValue)
    }
}

根据上面的源码和断点调试,可以发现协程的启动和恢复都会执行一次ThreadContextElement.updateThreadContext(context)ThreadContextElement.restoreThreadContext(context, oldValue),现在再分析一次上面的代码运行:

fun main(args: Array<String>) = runBlocking<Unit> {
    val threadLocal = ThreadLocal<String>().apply { set("Init") }
// 此时在 main 线程,threadLocal 的值为 Init
    printlnValue(threadLocal)
    val job = GlobalScope.launch(threadLocal.asContextElement("launch")) {
// 启动协程后,切换到 DefaultDispatcher-worker-1 线程,threadLocal 在该线程的值为 null
        // 调用 updateThreadContext() 设置 threadLocal 的值为 launch,保存之前的为 null
        printlnValue(threadLocal)
        // 在 DefaultDispatcher-worker-1 线程,修改 threadLocal 的值为 launch changed
        threadLocal.set("launch changed")
        printlnValue(threadLocal)
        // yield() 挂起函数会挂起当前协程,并将协程分发到 Dispatcher.Default 的队列中等待恢复
        // 挂起协程后调用 restoreThreadContext() 修改 threadLocal 为 null
        yield()
// 恢复协程后,此时在 DefaultDispatcher-worker-2 线程,threadLocal 的值为 null
        // 再次调用 updateThreadContext() 设置 threadLocal 的值为 launch,保存之前的为 null
        printlnValue(threadLocal)
        // 结束协程后,restoreThreadContext() 修改 threadLocal 为 null
    }
    job.join()
    // 此时已经从 DefaultDispatcher-worker-2 线程切换回 main 线程,main 线程中的 threadlocal 没有修改过,还是为 Init
    printlnValue(threadLocal)
}

private fun printlnValue(threadLocal: ThreadLocal<String>) {
    println("${Thread.currentThread()} thread local value: ${threadLocal.get()}")
}

所以 ThreadContextElement 并不能跟踪所有ThreadLocal对象的访问,而且每次挂起时更新的值将丢失。最重要的牢记它的原理:启动和恢复时保存ThreadLocal在当前线程的值,并修改为 value,挂起和结束时修改当前线程ThreadLocal的值为之前保存的值。

3. 已有线程同步方式

协程中的并发与线程的并发大部分是相同的,所以本篇文章应该是目前为止该系列文章中最容易理解的一篇,本系列Kotlin Coroutines(协程) 完全解析暂时就到这里,后面待 select 表达式、Channel、Actor 等实验性内容正式发布后继续解析,还有在 Android 项目中协程的实际运用,敬请期待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 【每日视检】 日期:2018/11/3 306/365 20/90 【作息】早睡:23:10早起:6:00 【健康...
    岩下松子阅读 183评论 0 0
  • 思念总是若有若无 相思又总是非常痛苦 漫长的黑夜一个人数着星星 被拉长的影子 那么的孤独 爱了心才有所牵挂 分开了...
    琢玉书生阅读 186评论 2 4
  • 最近一段日子,有些忙乱,也因为沉迷手绘把参加的语音练习有所懈怠。很长的时间只是把群里发出的语音练习草草练习一两遍,...
    一谙其优阅读 355评论 0 1