首页 > 其他分享 >Kotlin协程系列(二)

Kotlin协程系列(二)

时间:2023-11-27 17:46:01浏览次数:33  
标签:协程 函数 val 作用域 Kotlin 调度 context 系列

  在进行业务开发时,我们通常会基于官方的协程框架(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函数的实现类似,请自行尝试。

    

    

 

标签:协程,函数,val,作用域,Kotlin,调度,context,系列
From: https://www.cnblogs.com/luqman/p/coroutine2.html

相关文章

  • ruckus zd系列恢复出厂设置的方法
    zd1200竟然不是按reset请注意截至2020年1月,Ruckus共有5款ZoneDirector产品,其中4款为EOL(寿命终止)ZD1000、ZD1100、ZD3000和ZD5000。仅ZD1200仍可供购买。所有型号的出厂重置(出厂默认/硬重置)程序均相同,但每个控制器的物理设计不同。所以这里我们提供所有ZD型号......
  • TS4210D系列多功能激光调阻机
    ·设备可实现对集成电路各项参数的精密修调,如:电阻、电压、电流、周期、频率等;·自主研发的多通道测量系统(最多96通道),精度高、速度快、稳定可靠;·适用于各种厚膜电路;·可匹配不同规格的探针板连接器,兼容各型号标准探针板;另可定制飞针式测量结构.以满足特殊的修调需求;·采用高......
  • Java系列之 String indexOf() 方法
    我|在这里 ......
  • 30. 干货系列从零用Rust编写正反向代理,HTTP的组装之旅(中间件)
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/......
  • 【PADAUK】应广单片机一级代理 PFS122系列芯片
    PADAUK应广单片机一级代理 PFS122系列芯片一、PFS122系列芯片概述PFS122系列芯片是应广单片机一级代理的一款高性能、低成本、高可靠性的8位单片机芯片。该芯片采用精简指令集(RISC)结构,具有高速运算能力和低功耗特性,适用于各种低成本、高可靠性的应用场景。PFS122系列芯片内置多种......
  • 【PADAUK】应广单片机一级代理 PFC151系列
    Padauk是一家专业的单片机一级代理,我们提供全面的单片机解决方案,包括PFC151系列。PFC151系列是一款高性能的单片机,它具有低功耗、高速度、高可靠性等优点。该系列单片机适用于各种应用,如智能家居、智能安防、智能工业等。在智能家居领域,PFC151系列单片机可以用于控制家用电器、照明......
  • 【PADAUK】应广 PMS160系列单片机一级代理
    Padauk PMS160系列单片机芯片是一款功能强大的微控制器芯片,广泛应用于各种嵌入式系统开发中。这款芯片具有高可靠性、高性能、低功耗、易于编程和易于使用等特点,因此深受广大开发人员的喜爱。在Padauk PMS160系列单片机芯片中,最核心的部件是一个16位的中央处理器(CPU),它能够处理大......
  • Kotlin协程系列(一)
    一.协程的定义最近看了一本有关kotlin协程的书籍,对协程又有了不一样的了解,所以准备写一个关于kotlin协程系列的文章。言归正传,我们在学习一个新东西的时候,如果连这个东西"是什么"都回答不了,那么自然很难进入知识获取阶段的"为什么"和"怎么办"这两个后续环节了。因此,我们......
  • 反转链表系列问题
    反转链表系列问题作者:Grey原文地址:博客园:反转链表系列问题CSDN:反转链表系列问题反转单链表题目描述见:LeetCode206.ReverseLinkedList思路如下对于任何一个节点cur来说,记录一个前驱节点pre(第一个节点的前驱节点是null)先用一个临时节点tmp记录cur的下一个......
  • 假期没闲着系列 续作 02
    今天下班早(呸,之后每天都得下班早点),然后有个项目有依赖需要内网下载,不过看了下里面的东西大概都还是用过的。略。项目管理工具,(TaskSaas),我还听朋友圈一位说他们公司自研的叫taskc,这个又进行了更新,加入了在每天0点30分将当天的deadline的task推给assign和attentions,放到了提醒的左侧做......