type
status
date
slug
summary
tags
category
icon
password

概念

协程运行在协程上下文中(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.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
 
Kotlin中的CoroutineScopeLayoutInflater源码分析
姜康
姜康
一个软件工程师
公告
type
status
date
slug
summary
tags
category
icon
password
🎉博客网站重新制作了🎉
👏欢迎更新体验👏