在进行业务开发时,我们通常会基于官方的协程框架(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. Gin+Xterm.js实现远程Kubernetes Pod(一)

    Xterm.js简介 xterm.js (https://xtermjs.org/)是一个开源的 JavaScript 库,它模拟了一个终端接口,可以在网页中嵌入一个完全功能的终端.这个库非常灵活,并 ...

  2. 【pandas小技巧】--反转行列顺序

    反转pandas DataFrame的行列顺序是一种非常实用的操作.在实际应用中,当我们需要对数据进行排列或者排序时,通常会使用到Pandas的行列反转功能.这个过程可以帮助我们更好地理解数据集,发现 ...

  3. openlayers动态添加自定义div图层 具有筛选功能 和浮窗

    https://blog.csdn.net/weixin_43863505/article/details/119493664

  4. python命令行解析模块argparse

    argparse是Python标准库中推荐的命令行解析模块 code01: tmp.py import argparse parser = argparse.ArgumentParser(descri ...

  5. CSS基础(4)

    目录 1 定位 1.1 为什么需要定位 1.2 定位组成 1.2.1 边偏移(方位名词) 1.2.2 定位模式 (position) 1.3 定位模式介绍 1.3.1 静态定位(static) - 了 ...

  6. 如何使用Python进行投资收益和风险分析

    如何投资是现代企业.个人投资者所面临的实际问题,投资的目标是收益尽可能大,但是投资往往伴随着风险,如果在保证收益最大化的情况下,风险最小:或是风险相同的情况下,如何实现收益的最大化:通过本实训,可以使 ...

  7. C# MySqlHelp类 "DbModel.MySql"数据库操作类

    以前做易语言/PHP的. 最近刚入门C#, 就简单的封装了一个类库, 边学边玩才容易学到东西嘛, 比起sqlserver, 我还是觉得mysql更加有亲切感; 于是模仿ThinkPHP编写了一个&qu ...

  8. 图解 LeetCode 算法汇总——链表

    本文首发公众号:小码A梦 一般数据主要存储的形式主要有两种,一种是数组,一种是链表.数组是用来存储固定大小的同类型元素,存储在内存中是一片连续的空间.而链表就不同于数组.链表中的元素不是存储在内存中可 ...

  9. Node.js vs. Spring Boot:Hello World 性能对决,谁更快一点?

    前言: Spring Boot 在 Java 生态中备受欢迎,它是一款基于 Java 构建的轻量级服务端框架,主要用于 Web 服务.Spring Boot 的应用使得创建各类基于 Spring 的企 ...

  10. # 简明快速配置 Rust 工具链

    以下内容为本人的学习笔记,如需要转载,请声明原文链接微信公众号「ENG八戒」https://mp.weixin.qq.com/s/dBzL9WZ8P1L1X9j_XkmNQg 你可能会为不同版本的工具 ...