粗糙的谈一下Kotlin中的协程


概念

协程运行在协程上下文中(CoroutineContext)。

协程上下文包含一个协程调度器(CoroutineDispatcher),它可以将协程限制在一个特定的线程中执行,或者将协程分配到一个线程池中,或者让它不受限制的运行。

CoroutineContext使用以下元素(Element)定义协程的行为:

  • Job: 控制协程的生命周期
  • CoroutineDispatcher: 将工作(协程)分派到适当的线程
  • CoroutineName: 协程的名字,可用于调试
  • CoroutineExceptionHandler: 处理未捕获的异常

CoroutineScope会跟踪它使用launch或者async创建的所有协程,可以随时调用scope.cancel()取消正在运行的协程.但是,已取消的scope是不能再创建协程的,这一点需要注意.

与调度程序不同,CoroutineScope并不运行协程.

Job是协程的句柄,使用launch或者async创建的每个协程都会返回一个job对象,这个对象唯一标识协程并管理协程的生命周期,当然也可以将Job传递给CoroutineScope进一步管理其生命周期.执行job.cancel()不影响CoroutineScope.

协程可以在一个线程上挂起,并在其他线程上恢复.

Dispatcher

主要使用3种Dispatcher:

  • Dispatchers.Main

    运行在主线程

  • Dispatchers.IO

    适合主线程之外的磁盘/网络IO

  • Dispatchers.Default

    适合主线程之外占有大量CPU资源的工作

可以使用withContext(Dispatcher)来指定:

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

withContext在性能上有优势,可以避免频繁的线程切换.

启动协程

两种方式:

  • launch

    不返回结果,从常规函数启动协程一般用launch,因为常规函数无法调用await

  • async

    返回结果 (使用await),在另一个协程中或者在挂起函数中且在执行并行分解时才使用async

两者处理异常的方式不同,async持有异常,并作为结果在await中返回.因此,如果使用await从常规函数启动,则会丢弃异常信息.

在普通方法中启动协程一般使用下面两种方法:

  • launch() (不会阻塞当前线程)
  • runBlocking {} (一般用于测试,将后台认为进行同步处理,防止过早退出)

并行分解

由suspend函数启动的所有协程都必须在函数返回结果之前停止,因此需要保证这些协程在返回结果之前完成.

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
}

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
}

自定义CoroutineScope

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

GlobalScope

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

使用GlobalScope.launch 的时候,会创建一个顶层协程,如果忘记保持对新启动的协程的引用,它还会继续运行,如果挂起了(比如delay了太久),必须手动保持对所有已经启动协程的引用.并调用join()方法.

在GlobalScope中启动的活动协程并不会使进程保活,它们就像守护线程.

阻塞与非阻塞

先看一段代码:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // 开始执行主协程
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主协程在这里会立即执行
}

你觉得会输出什么结果?

结果是:

Hello,

对,GlobalScope.launch中的代码快并没有执行到打印“World”.

这个时候可以再主协程中添加一个delay,但是方法未免太过死板,可以使用JOb控制:

val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
    delay(1000L)
    println("World!")
}
println("Hello,")
job.join() // 等待直到子协程执行结束

但是,如果有很多协程,每个都去获取job并join的话也太容易出错了,因此:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // 在 runBlocking 作用域中启动一个新协程
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

直接在对应的coroutineScope中启动协程,而不是使用GlobalScope.

调用顺序问题

  • 顺序调用

    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
  • async并发

    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
    suspend fun concurrentSum(): Int = coroutineScope {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        one.await() + two.await()
    }
  • Lazy 的 async并发

    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // 执行一些计算
        one.start() // 启动第一个
        two.start() // 启动第二个
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")

    只有结果通过await获取的时候协程才会启动,或者在Job的start函数调用的时候.

参考文档

  1. https://developer.android.com/kotlin/coroutines-adv?hl=zh-cn

文章作者: 姜康
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 姜康 !
评论
 上一篇
Kotlin中的object Kotlin中的object
Kotlin中object有3种使用场景. 对象表达式window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent
2020-10-29
下一篇 
粗糙的谈一下Kotlin中的CoroutineScope 粗糙的谈一下Kotlin中的CoroutineScope
简单说明CoroutineScope其实定义了协程的生命周期,比如在Activity中启动的协程,在Activity销毁的时候应该要取消. 而GlobalScope则是对应整个APP的生命周期,即使Activity已经销毁,Coroutin
2020-10-29
  目录