在进行业务开发时,我们通常会基于官方的协程框架(kotlinx.coroutines)来运用Kotlin协程优化异步逻辑,不过这个框架过于庞大和复杂,如果直接接触它容易被劝退。所以,为了我们在后续的学习中游刃有余,在使用官方给出的复合协程时能够胸有成竹,我们暂且抛开它,按照它的思路实现一个轻量版的协程框架。

1.开胃小菜:实现一个delay函数

  在实现delay函数前,我们先来思考delay函数的作用。在使用线程开发时,如果我们想让一段代码延迟一段时间再执行,我们一般会用Thread.sleep函数,但是这个函数的缺点是它会阻塞当前线程。在协程当中,我们同样可以这样做,只是这么做不好,明知道协程可以挂起,却要它阻塞线程,岂不是在浪费cpu资源?

  我们的目的是让代码延迟一段时间后再执行,只要做到这点就好了。因此,delay函数的实现可以确定以下两点:

  • 不需要阻塞线程
  • 是个挂起函数,指定时间后,能够恢复执行即可

  这里,直接给出delay函数的实现,然后再作出解释:

suspend fun delay(time:Long,unit: TimeUnit =TimeUnit.MILLISECONDS){
val executor=Executors.newScheduledThreadPool(1){r->
Thread(r,"Scheduler").apply{
isDaemon=true
}
}
if(time<=0){
return
}
suspendCoroutine<Unit> {continuation ->
executor.schedule({continuation.resume(Unit)},time,unit)
}
}

  当time不大于0时,表示无延迟,直接返回就好;接下来需要考虑挂起,我们可以使用suspendCoroutine,不难想到,只要再指定time之后,恢复协程的执行就好,所以只要能够给我们提供一个这样的定时回调机制就可以轻松实现这个功能。

  在jvm上,我们很自然的就可以想到使用ScheduledExecutorService,问题就这样迎刃而解了。

2.协程的描述

  客观的讲,startCoroutine和createCoroutine这两个api并不适合直接做业务开发。因此,对于协程的创建,在框架中也要根据不同的目的提供不同的构建器(例如launch,async),其背后对于封装出来的复合协程的类型描述,就是至关重要的一环。

  协程的描述类,官方给出的名字是Job,和线程的描述类Thread相比,Job同样有join函数,调用时会挂起协程,直到它的完成,它的cancel函数可以对应Thread的interrupt函数,用于取消协程,isActive可以类比Thread的isActive(),用于查询协程是否还在运行。此外,Job还有取消回调函数invokeOnCancel,完成回调函数invokeOnComplete,用于移除回调的remove函数。

3.协程的创建

  我们已经给出了协程的描述,知道了协程应该具有哪些能力,接下来就需要如何封装协程的创建了。

  3.1无返回值的launch函数

    如果一个协程的返回值时Unit,我们可以称它为无返回值的,对于这样的协程,我们只需要启动它即可,下面我们给出launch函数的定义:

fun launch(context: CoroutineContext=EmptyCoroutineContext,block:suspend ()->Unit):Job{
val completion=StandaloneCoroutine(context)
block.startCoroutine(completion)
return completion
}
class StandaloneCoroutine(context:CoroutineContext):AbstractCoroutine<Unit>(context){}
abstract class AbstractCoroutine<T>(context:CoroutineContext):Job,Continuation<T>{
protected val state=AtomicReference<CoroutineState>()
override val context:CoroutineContext
init{
state.set(CoroutineState.Incomplete())
this.context=context+this
}
val isCompleted
get()=state.get() is CoroutineState.Complete<*>
override val isActive:Boolean
get()=when(state.get()){
is CoroutineState.Complete<*>->false
is CoroutineState.Cancelling->false
else->true
}
......
}

    其中,StandaloneCoroutine是AbstractCoroutine的子类,目前只有一个空实现。

  3.2实现join函数

    join函数是一个挂起函数,他需要等待协程的执行,此时会有两种情况:被等待的协程已经执行完成,join函数就不会挂起,而是立马返回;被等待的协程尚未完成,此时join将协程挂起,直到协程完成。

    下面给出join函数的伪代码实现:

sealed class CoroutineState {
class Incomplete():CoroutineState()
class Cancelling():CoroutineState()
class Complete():CoroutineState()
} suspend fun join(){
when(state.get()){
is CoroutineState.Incomplete->return joinSuspend()
is CoroutineState.Cancelling->return joinSuspend()
is CoroutineState.Complete->return
}
} suspend fun joinSuspend()= suspendCoroutine<Unit> {continuation ->
doOnCompleted{result->
continuation.resume(Unit)
}
}

  3.3有返回值的async函数

    现在,我们已经知道如何启动协程和等待协程完成了,不过很多时候,我们想拿到协程的返回值,因此我们再基于Job接口再定义一个Deferred接口,如下所示:

interface Deferred<T>:Job{
suspend fun await()
}

    这里多了一个泛型参数T,T表示返回值类型,通过它的await函数可以拿到这个返回值,因此await函数的主要作用有:在协程执行完成时,立即拿到协程的结果;如果协程尚未完成,则挂起协程,直到它完成,这一点和join类似。下面,给出await函数的定义:

class DeferredCoroutine<T>(context:CoroutineContext):AbstractCoroutine<T>(context),Deferred<T>{
override suspend fun await():T{
val currentState=state.get()
return when(currentState){
is CoroutineState.Incomplete->awaitSuspend()
is CoroutineState.Cancelling->awaitSuspend()
is CoroutineState.Complete->currentState.value as T
}
}
private suspend fun awaitSuspend()=suspendCoroutine<T>{continuation->
doOnCompleted{result->
continuation.resumeWith(result)
}
}
}

    await函数的实现思路和join函数类似,只是在对结果的处理上有差异。接下来,我们再给出async函数的实现,代码如下:

fun <T> async(context:CoroutineContext=EmptyCoroutineContext,block:suspend ()->T):Deferred<T>{
val completion=DeferredCoroutine<T>(context)
block.startCoroutine(completion)
return completion
}

    这样,我们就可以启动有返回值的协程了,首先定义一个挂起函数,然后用delay(1000)函数来模拟耗时操作,然后我们用async启动协程,并获取协程的返回值,代码如下:

suspend fun getValue():String{
delay(1000)
return "使用async启动协程"
}
suspend fun main(){
val deferred=async{
getValue()
}
val result=deferred.await()
println(result)//将会打印"使用async启动协程"
}

4.协程的调度

  截至目前,我们已经大致用程序勾勒出一个比较完善的复合协程了,不过还有一个问题没有解决,我们的协程是如何实现并发的?我们前面在介绍协程的时候提到过协程的挂起和恢复和线程的不同点在于在哪儿挂起,什么时候恢复是开发者自己决定的,这意味着调度工作不能交给操作系统,而应该在用户态解决。

  协程需要调度的位置就是挂起点的位置,当协程执行到挂起点的位置时,如果产生了异步行为,协程就会在这个挂起点挂起,只有协程在挂起点正真挂起时,我们才有机会实现调度,而实现调度器需要使用协程的拦截器。调度的本质就是解决协程在挂起点恢复后的协程逻辑在哪里运行的问题,由此给出调度器的接口定义:

interface Dispatcher{
fun dispatch(block:()->Unit)
}

  接下来,我们将调度器和拦截器结合起来,拦截器是协程上下文元素的一类实现,下面给出基于调度器的拦截器实现的定义:

open class DispatcherContext(private val dispatcher:Dispatcher):AbstractCoroutineContextElement(ContinuationInterceptor),ContinuationInterceptor{
override fun <T> interceptorContinuation(continuation:Continuation<T>):Continuation<T>=DispatchedContinuation(continuation,dispatcher)
}
private class DispatchedContinuation<T>(val delegate:Continuation<T>,val dispatcher:Dispatcher):Continuation<T>{
override val context=delegate.context
override fun resumeWith(result: Result<T>) {
dispatcher.dispatch{
delegate.resumeWith(result)//通过dispatch将协程的恢复执行调度在指定的调度器上
}
}
}

  调度的具体过程其实就是在delegate的恢复调用之前,通过dispatch将其调度在指定的调度器上。下面我们介绍一下有哪些调度器类型:

  • 默认调度器(Dispatchers.Default):使用共享的线程池,适用于 CPU 密集型的计算任务。默认调度器的线程数量通常与可用的 CPU 核数相等,因此适用于并行计算。但不适合执行可能导致线程阻塞的操作
  • 主线程调度器(Dispatchers.Main):适用于 Android 应用程序中执行 UI 操作的协程。它会将协程的执行切换到主线程,以确保 UI 操作不会在后台线程上执行
  • IO调度器(Dispatchers.IO):专门用于执行涉及阻塞 IO 操作的协程,例如文件读写或网络请求。IO 调度器使用一个专门的线程池,允许执行大量的 IO 操作而不阻塞线程
  • 无限制调度器(Dispatchers.Unconfined):允许协程在调用挂起函数的线程中继续执行,直到第一个挂起点。之后,它可能在其他线程中继续执行,这取决于具体的挂起函数实现。

5.协程的作用域

  通常我们提到域,都是用来表示范围的,域既有约束作用,又能提供额外的能力。官方框架在实现复合协程的过程中也提供了作用域,主要用以明确协程之间的父子关系以及对于取消的传播行为。该作用域包括以下三种:

  • 顶级作用域:没有父协程的协程所在的作用域为顶级作用域
  • 协同作用域:协程中启动新的协程,新协程为所在协程的子协程,这种情况下子协程所在的作用域默认为协同作用域。
  • 主同作用域:与协程作用域在协程的父子关系上一致,区别在于处于该作用域下的协程出现未捕获的异常时不会将异常向上传递给父协程。

  除了这三种作用域中提到的行为外,父子协程之间还存在以下规则:

  • 父协程被取消,则所有的子协程均被取消
  • 父协程需要等待子协程执行完毕后才最终进入完成状态
  • 子协程会继承父协程的协程上下文中的元素,如果自身有相同的key的成员,则覆盖对应的key,覆盖效果仅限自身范围内有效

  下面给出一个协程作用域的通用接口:

interface CoroutineScope{
val coroutineContext:CoroutineContext
}

  从约束的角度来讲,既然有了作用域,我们就不能任意直接使用launch和async函数来创建协程了,加上作用域之后,我们的launch函数的定义如下:

fun CoroutineScope.launch(context:CoroutineContext=EmptyCoroutineContext,block:suspend CoroutineScope.()->Unit):Job{
val completion=StandaloneCoroutine(newContext)
block.startCoroutine(completion,completion)
return completion
}

  async函数的实现类似,请自行尝试。

    

    

Kotlin协程系列(二)的更多相关文章

  1. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  2. Kotlin 协程一 —— 全面了解 Kotlin 协程

    一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...

  3. Kotlin协程第一个示例剖析及Kotlin线程使用技巧

    Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...

  4. 深入理解协程(二):yield from实现异步协程

    原创不易,转载请联系作者 深入理解协程分为三部分进行讲解: 协程的引入 yield from实现异步协程 async/await实现异步协程 本篇为深入理解协程系列文章的第二篇. yield from ...

  5. Retrofit使用Kotlin协程发送请求

    Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...

  6. Kotlin协程基础

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...

  7. Android Kotlin协程入门

    Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...

  8. rxjava回调地狱-kotlin协程来帮忙

    本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...

  9. Kotlin协程通信机制: Channel

    Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...

  10. Kotlin协程作用域与Job详解

    Job详解: 在上一次https://www.cnblogs.com/webor2006/p/11725866.html中抛出了一个问题: 所以咱们将delay去掉,需要改造一下,先把主线程的dela ...

随机推荐

  1. Docker版SS安装

    灰常简单 首先安装docker 使用官方安装脚本自动安装 64位的centos7和8安装命令如下: curl -fsSL https://get.docker.com | bash -s docker ...

  2. 从read 系统调用到 C10M 问题

    一.前言 从上个世纪到现在,工程师们在优化服务器性能的过程中,提出了各种不同的io模型,比如非阻塞io,io复用,信号驱动式io,异步io.具体io模型在不同平台上的实现也不一样,比如io复用在bsd ...

  3. Windows查找监听端口对应的进程及其路径

    前言 假设扫描到1234端口存在可疑进程,需要找到该监听端口对应的进程及其进程文件的全路径,判断是否为可疑程序. 步骤 启动命令行:按win + r键,然后输入"cmd" 查看端口 ...

  4. Javascript执行原理 网页引入javascript的三种方式* javascript核心语法 数据类型 Typeof运算符

    Javascript执行原理: 用户端发送请求到服务器端 将js解析出来的数据(用户身份表示)绑定在请求路径中 服务器端获取到参数后会响应客户端 客户端通过浏览器解析响应的数据并将数据展现在浏览器上 ...

  5. 一文详述流媒体传输网络MediaUni

    一张「多元融合」的网络. 黄海宇|演讲者 大家好,我是阿里云视频云的黄海宇,今天分享主题是MediaUni--面向未来的流媒体传输网络设计与实践. 下面我将会从应用对流媒体传输网络的要求.MediaU ...

  6. P1830题解

    思路: 利用桶存储轰炸区域,双重循环. 在存储轰炸区域时将次数刷新,也就是pos[j][k]=i;. 下面是核心代码: for(int i=1;i<=x;i++) { int x1,x2,y1, ...

  7. Auto-GPT免费尝鲜之初体验-使用攻略和总结

    写在前面的废话 ChatGPT 的交互模式,是和一个 "人" 对话聊天. 如果你想了解更多ChatGPT和AI绘画的相关知识,请参考:ChatGPT注册和变现思路,AI绘画教程汇总 ...

  8. [TSG开发日志](一)软件基础框架

    目录 前言 说明 框架 TSG_Framework 一.底层信号机制 TSG_Caller 二.参数类型声明 TSG_Params 三.设备类声明 TSG_Device 四.设备配置文件控制 TSG_ ...

  9. 拯救Win7,2023该如何正确升级?

    对于现存的Win7系统用户,微软曾多次提醒将在2023年1月停止对Win7与Win8.1的安全更新和技术支持.而转眼已经来到2023,时间已到,对于Win7,微软已经再也不管了,停止为Win7用户提供 ...

  10. Vue Vuex状态管理

    1.1 理解 Vuex 1.1.1 Vuex 是什么 概念:专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组 ...