在进行业务开发时,我们通常会基于官方的协程框架(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. JavaWeb和MVC三层架构

    JavaWeb 概述 网站发布和部署一定要依托技术语言吗: 不一定,一个网站可以直接发布和部署,因为因为浏览器能够识别网页只需要两样东西,网络和静态页面,还有一个装在他们的容器,比如 nginx. 静 ...

  2. 从javascript代码解析过程理解执行上下文与作用域提升

    javascript代码解析过程 执行上下文和作用域是javascript中非常重要的部分,要弄清楚它们首先就要说到javascript的运行机制,javascript代码被解析经过了以下几个步骤 P ...

  3. 手工搭建并配置apache,php,mysql环境服务器

    1,安装apache2.4: 从apache官网中下载windows版本的apache二进制文件,解压 打开apache目录中的bin目录,在其中打开cmd窗口,使用命令: httpd -k inst ...

  4. [ABC126E] 1 or 2

    2023-01-07 题目 题目传送门 翻译 翻译 难度&重要性(1~10):2 题目来源 AtCoder 题目算法 并查集 解题思路 因为每张卡片上的数字只能是 \(1\) 或者 \(2\) ...

  5. 小白整理了VUEX

    在小白开发的项目中前端使用的是Vue,虽然在日常工作中可以使用Vue进行开发工作.但是没有系统的学习过Vue,对Vue的一些特性和方法使用时常常需要查询资料解决问题,查询资料常常会占用大量时间,尤其对 ...

  6. Hugging News #0904:🤗 登陆 AWS Marketplace

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...

  7. C# Wke使用例子 (KyozyWke)

    概述 wke是国人大牛BlzFans封装的webkit, 基于chrome浏览器源代码的裁剪版本, 大小只有仅仅10M. 无需依赖其他的扩展库就可以在本地使用谷歌内核快速加载网页. wke是2011年 ...

  8. Windows 某些软件显示"口口"解决办法

    和乱码不同,文字变成"口口",一般是语言环境出错了. 解决办法 开始->控制面板->区域 (时钟.语言和区域)->区域:更改设置->管理->非 Uni ...

  9. KRPANO资源分析工具下载四方环视全景图

    提示:目前分析工具中的全景图下载功能将被极速全景图下载大师替代,相比分析工具,极速全景图下载大师支持更多的网站(包括各类KRPano全景网站,和百度街景) 详细可以查看如下的链接: 极速全景图下载大师 ...

  10. 【krpano】多分类缩略图及多分类地图案例

    该案例提供了场景多分类缩略图展示以及多地图展示,效果如下截图:                 下载地址:http://pan.baidu.com/s/1hsA5ta8 感谢群内小伙伴H·T·T的分享 ...