⑤ 启动模式
在launch & async那里截了launch和async的源码,关注第二个参数 CoroutineStart,点进源码:
public enum class CoroutineStart { // 默认,创建后立即开始调度,调度前被取消,直接进入取消响应状态。 DEFAULT, // 懒加载,不会立即开始调度,需要手动调用start、join或await才会 // 开始调度,如果调度前就被取消,协程将直接进入异常结束状态。 LAZY, // 和Default类似,立即开始调度,在执行到一个挂起函数前不响应取消。 // 涉及到cancle才有意义 @ExperimentalCoroutinesApi ATOMIC, // 直接在当前线程执行协程体,直到遇到第一个挂起函数,才会调度到 // 指定调度器所在的线程上执行 @ExperimentalCoroutinesApi UNDISPATCHED; }
0x8、调度器 → CoroutineDispatcher
① 四类调度器
Kotlin协程预置4种调度器,如下表所示:
种类 | 描述 |
Default | 默认,线程池,适合处理后台计算,CPU密集型任务调度器 |
IO | IO调度器,适合执行IO相关操作,IO密集型任务调度器 |
Main | UI调度器,根据平台不同会初始化为对应UI线程的调度器,如Android的主线程 |
Unconfined | 不指定线程,如果子协程切换线程,接下来的代码也在该线程继续执行 |
另外,调度器还提供了一个属性**immediate
**,如果当前处于该调度器中,不执行调度器切换直接执行。对比示例如下:
CoroutineScope(Job() + Dispatchers.Main.immediate).launch { // 第一个执行 } // 第二个执行 CoroutineScope(Job() + Dispatchers.Main).launch { // 调度器切换,导致慢一些所以第四个执行 } // 第三个执行
② withContext
和launch、async及runBlocking不同,withContext不会创建新的协程,常用于 切换代码执行所运行的线程。 它也是一个挂起方法,直到结束返回结果。多个withContext是串行执行的, 所以很适合那种一个任务依赖上一个任务返回结果的情况,比如:
就很舒服,使用async+await可以是想同样的效果,只是需要创建了三个协程,有些多余:
0x9、拦截器 → ContinuationInterceptor
见名知意,就是用来拦截协程做一些 附加操作 的,比如上面的调度器,就用拦截器实现的。 写个拦截打日志的Demo试试水,这里的 Continuation 是用来保存协程挂起状态与局部变量的对象。
运行结果如下:
发生了4次拦截,依次是:协程启动时(前两个),挂起时,返回结果时。 我们可以在写个简易版的线程调度器,让协程在启动时完成线程切换,示例如下:
运行结果如下:
可以看到,协程切换到自定义的线程池运行,配合withContext,运行完后又切回来了。
0x10、Deferred
看下Deferred的源码:
在继承Job的基础上,指定了<out T> 输出泛型,await()挂起协程并返回最后的执行结果。
0x11、Channel → 通道
与Java中用于解决多线程数据传递的BlockingQueue类似,Kotlin协程提供了 Channel, 用于解决多协程间的数据传递。元素从一端被加入,从另一端被消费,除了堵塞操作外, Channel还提供了非堵塞的send及receive操作。一个简单的使用代码示例如下:
另外,Receiver端支持用for迭代来接收消息,比如:for(c in channel) 也是可以的。
注:用完Channel记得调用close()关闭通道,否则从Channel读取数据的协程都会无限挂起,在那里等数据传过来!!!
① 不同的Channel类型
打开Channel的源码:
继承了SendChannel 和 ReceiveChannel,然后定义了几个代表Channel类型的常量:
RENDEZVOUS → 默认,0缓存,创建了一个RendezvousChannel,send就挂起,直到被receive; UNLIMITED → 创建了一个LinkedListChannel,无限容量,send不会挂起; BUFFERED → 指定大小,创建了一个ArrayChannel,满了send会挂起; CONFLATED → 创建了一个ConflatedChannel,新send的会覆盖之前send的,receiver只会得到最新的,send不会挂起; // 创建不同类型Channel示例: val rendezvousChannel = Channel<String>() val unlimitedChannel = Channel<String>(UNLIMITED) val bufferedChannel = Channel<String>(10) val conflatedChannel = Channel<String>(CONFLATED)
② SendChannel & ReceiveChannel
SendChannel定义了往通道发送数据的接口,ReceiveChannel定义了从通道接收数据的接口,简单过下API:
/* === SendChannel === */ isClosedForSend: Boolean // 判断通道是否关闭,关闭了再发送数据会抛异常 isFull: Boolean // 通道是否已满 close(cause: Throwable? = null): Boolean // 关闭通道 send(element: E) // 挂起函数,通道满该函数会先暂停执行,如果通道关闭会抛异常 offer(element: E): Boolean // 同步发送一个数据,通道满或关闭无法添加成功,返回false或抛异常 invokeOnClose(handler: (cause: Throwable?) -> Unit) // 通道关闭执行回调 onSend: SelectClause2<E, SendChannel<E>> //立即发送数据(如果允许), 在select表达式中使用 /* === ReceiveChannel === */ isClosedForReceive: Boolean // 判断通道是否关闭,关闭后还有缓存则会接收完再返回false isEmpty: Boolean // 通道是否为空 cancel(cause: CancellationException? = null) // 关闭通道 receive(): E // 挂起函数,接收数据,如果通道关闭会抛异常 receiveOrClosed(): ValueOrClosed<E> // 同上,只是通道关闭了不会抛异常,而是返回null poll(): E // 从通道获取并删除一个数据,如果为空返回null,如果出错则抛出异常 onReceive: SelectClause1<E> // 用于select表达式
Channel、Flow以及实际应用后续补上,待续...
参考文献:
- 『译』揭秘协程上下文
- 理解 Kotlin 的协程
- 协程中的取消和异常 | 取消操作详解
- 协程中的取消和异常 | 异常处理详解
- Kotlin协程生命的尽头---协程取消
- 调度中出现了问题该如何处理-协程异常
- 《深入理解Kotlin协程》 霍丙乾著