Kotlin

跳到内容

 

登录  注册

Kotlin / kotlin-coroutines

 代码问题26提取请求1项目1见解  

解雇

立即加入GitHub

GitHub是超过2800万开发人员共同主持和审查代码,管理项目以及共同构建软件的所在地。

注册

科: 硕士 

查找文件复制路径

kotlin-coroutines/kotlin-coroutines-informal.md

4b38193 on 26 Jan

@MarcinMoskala MarcinMoskala 让榜样更富有表现力

18位 贡献者@elizarov@abreslav@MarcinMoskala@SabagRonen@JakeWharton@dzharkov@nd@cbeust@Egorand@ y0za@VladimirReshetnikov@ DSteve595@rocketraman@oshai@LouisCAD@ s1monw1@FireZenk@ azak0

原始责备历史

   

1838行(1481 sloc)  83.5 KB

Kotlin的协程(修订版3.2)

  • 类型:非正式说明
  • 作者:安德烈布雷斯拉夫
  • 撰稿人:Vladimir Reshetnikov,Stanislav Erokhin,Ilya Ryzhenkov,Denis Zharkov,Roman Elizarov
  • 状态:在Kotlin 1.1.0中实施

抽象

这是对Kotlin协同程序的描述。这个概念也称为或部分涵盖

  • 发电机/产量
  • 异步/ AWAIT
  • 可组合/分隔的连续性

目标:

  • 不依赖于Futures或其他此类丰富库的特定实现;
  • 同样覆盖“异步/等待”用例和“发电机组”;
  • 可以将Kotlin协同程序用作不同现有异步API(例如Java NIO,Futures的不同实现等)的包装器。

目录

用例

协程可以被认为是可暂停计算的实例,即可以在某些点暂停并稍后可能在另一个线程上恢复执行的协程。协同程序相互呼叫(以及来回传递数据)可以形成协作式多任务处理的机制。

异步计算

协同程序的第一类激励用例是异步计算(由C#和其他语言中的async / await处理)。让我们来看看如何通过回调完成这样的计算。作为灵感,让我们采用异步I / O(以下API简化):

//异步读入`buf`,完成后运行lambda 
inChannelread(buf){
     //这个lambda在读取完成时执行 
    bytesRead - > 
    ... 
    ... 
    process(buf,bytesRead) //从`buf`异步写入,完成后运行lambda 
    outChannel写(BUF){
         //写入完成后执行该拉姆达... ... 
        不过outFileclose()          
    }
}
    
    
        
        

请注意,我们在回调buf函数中有一个回调函数,虽然它从很多样板文件中省去了我们(例如,没有必要将参数显式传递给回调函数,但它们只是将它视为闭包的一部分),缩进级别是每次都在增长,人们可以很容易地预测嵌套级别大于一的问题(谷歌的“回调地狱”,看看人们在JavaScript中有多少受此影响)。

这个相同的计算可以直接表示为协程(假设有一个库可以使I / O API适应协程要求):

launch(CommonPool){
     //在异步读取
    val  bytesRead  = inChannel时挂起aRead(buf) 
     //当读取完成时我们只到达这一行
    ...... 
    ... 
    进程(buf,bytesRead)//在异步写入    
    outChannel时挂起aWrite(buf)
     //写完成后我们才到达这一行   ... ... 
    outFile close()
}
    
    
    

aRead()aWrite()这里特殊的悬浮功能 -他们可以暂停执行(这并不意味着阻塞线程已经在运行),并继续在呼叫已完成。如果我们眯着眼睛看到后面的所有代码aRead()都被包裹在lambda中并aRead()作为回调传递,并且已经完成了相同的操作aWrite(),我们可以看到这段代码与上面相同,只是更具可读性。

这是我们的明确目标,以支持协同程序在一个非常通用的方式,所以在这个例子中, launch{}.aRead(),和.aWrite()只是库函数面向与协同程序的工作:launch协同程序生成器 -它建立在一些情况下启动协同程序(一个CommonPool上下文中使用在示例中)while aReadaWrite是特殊的 挂起函数,它隐式地接收 continuation(continuation只是通用的回调)。

库代码launch{}显示在coroutine构建器部分中,库代码.aRead()显示在包装回调部分中。

注意,显式传递的回调在循环中间有一个异步调用可能很棘手,但在协程中,它是一个非常正常的事情:

launch(CommonPool){
     whiletrue){
         //在异步读取
        val  bytesRead  = inFile时挂起aRead(buf)
         //在读取完成后继续
        if(bytesRead ==  - 1break 
        ... 
        process(buf,bytesRead)//在异步写入 
        outFile时挂起aWrite(buf) 
         //写完后继续...... 
    } 
}
        
        

可以想象,在协程中处理异常也更方便一些。

期货

还有另一种表达异步计算的方式:通过期货(及其近亲 - 承诺)。我们将在此处使用虚构的API,将叠加层应用于图像:

val  future  = runAfterBoth(
    asyncLoadImage( ... original ... ),//创建一个Future 
    asyncLoadImage( ... overlay ... //创建一个Future 
){ 
    original,overlay - > 
    ... 
    applyOverlay(original,overlay)
}

使用协同程序,可以将其重写为

val  future  = future {
     val  original  = asyncLoadImage( ... original ... //创建一个Future 
    val  overlay  = asyncLoadImage( ... overlay ... //创建一个Future 
    ... 
    //暂停在等待图片加载
    //然后运行`applyOverlay(...)`时,他们都装 
    applyOverlay(原件等待(),覆盖等待())
}

库代码future{}显示在建筑期货部分,库代码.await()显示在暂停功能部分。

再次,少压痕,更自然组成的逻辑(和异常处理,这里没有显示),并没有什么特殊的关键字(如asyncawait在C#中,JS和其他语言)支持期货:future{}.await()只是在库函数。

发电机

协程的另一个典型用例是延迟计算序列(由yieldC#,Python和许多其他语言处理)。这样的序列可以通过看似顺序的代码生成,但在运行时只计算所请求的元素:

//推断出的类型是序列<诠释> 
VAL  斐波纳契 = buildSequence {
    产率 1 //第一Fibonacci数
    变种 CUR  =  1个
    变种 下一 =  1个
    ){
        产率(下一个) //下Fibonacci数
        VAL  TMP  = CUR + next
        cur = next
        next = tmp
    }
}

这段代码创建了一个懒惰SequenceFibonacci数,这可能是无限的(完全像Haskell的无限列表)。我们可以通过以下方式请求其中一些take()

的println(斐波纳契采取(10 joinToString())

这将打印1, 1, 2, 3, 5, 8, 13, 21, 34, 55 您可以在此处尝试此代码

发电机的强度是在支持任意的控制流程,例如while(来自上面的例子), iftrycatchfinally和其他一切:

VAL  SEQ  = buildSequence {
     收益率(与firstItem)//悬挂点

    (项目输入){
         如果项目 isValid()的)打破 //不产生任何更多的项目
        VAL  FOO  =toFoo()
         如果 FOO  isGood())继续
        (富)//悬挂点         
    } 尝试 {
         产量(lastItem())//
    
    暂停点 
    } 终于 {
         //一些终结代码 
    } 
}
    

为库代码buildSequence{}yield()示于 限制悬浮液部分。

请注意,此方法还允许表示yieldAll(sequence)为库函数(以及buildSequence{}yield()),这简化了连接延迟序列并允许有效实现。

异步UI

典型的UI应用程序具有单个事件调度线程,其中所有UI操作都发生。通常不允许从其他线程修改UI状态。所有UI库都提供某种原语来将执行移回UI线程。例如,Swing有 SwingUtilities.invokeLater,JavaFX Platform.runLater,Android有Activity.runOnUiThread,等等。这是来自典型Swing应用程序的代码片段,它执行一些异步操作,然后在UI中显示其结果:

makeAsyncRequest {
     //当异步请求完成 
    结果时执行此lambda,异常- > 
    
    if(exception ==  null){
         //在UI 
        SwingUtilities中显示结果invokeLater { 
            display(result)    
        } 
    } else {
        //进程例外 
    } 
}

这类似于我们在异步计算用例中看到的回调地狱,它也被协同程序优雅地解决了:

launch(Swing){
     try {
         //暂停,同时异步地发出请求
        val  结果 = makeRequest()
         //在UI中显示结果,这里Swing上下文确保我们始终保持在事件调度线程 
        显示(结果)
    } catch(异常 Throwable) {
         //进程例外 
    } 
}

Swing上下文的库代码显示在continuation拦截器部分中。

所有异常处理都使用自然语言结构执行。

更多用例

协同程序可以涵盖更多用例,包括:

  • 基于通道的并发(又名goroutines和channel);
  • 基于Actor的并发;
  • 偶尔需要用户交互的后台进程,例如,显示模态对话框;
  • 通信协议:将每个actor实现为序列而不是状态机;
  • Web应用程序工作流:注册用户,验证电子邮件,登录(暂停的协程可以序列化并存储在数据库中)。

协同程序概述

本节概述了支持编写协同程序的语言机制以及管理其语义的标准库。

协程的实验状态

协同程序在Kotlin 1.1中是实验性的,因为我们期望设计能够改变。Kotlin编译器会对使用与协程相关的功能发出警告。有一个选择加入开关 -Xcoroutines=enable,可以删除警告。

所有与kotlin-stdlib中的协同程序相关的API都在一个名为的包中发布kotlin.coroutines.experimental。当最终设计准备就绪时,它将在下发布kotlin.coroutines,而实验包将保留一段时间,以便旧的二进制文件兼容并继续工作。

在其公共API中使用协同程序的每个库都应该这样做,
因此如果您正在编写一个存在的库并且您关心未来版本的用户,那么您还需要为您的软件包命名org.my.library.experimental。当协同程序的最终设计出现时,experimental从主API中删除后缀,但保留旧包,以便那些可能需要二进制兼容性的用户使用。

更多细节可以在这篇 论坛帖子中找到

术语

  • 一个协同程序 -是一个实例悬浮计算。它在概念上类似于一个线程,在某种意义上它需要运行一段代码并具有相似的生命周期 - 它是创建启动的,但它不受任何特定线程的约束。它可以在一个线程中暂停其执行并在另一个线程中继续执行。此外,像一个未来或承诺,它可以完成一些结果或异常。

  • 一个暂停功能 -标有一个功能suspend修改。它可以通过调用其他挂起函数来暂停执行代码而不阻塞当前执行线程。不能从常规代码调用挂起函数,而只能从其他挂起函数和挂起lambdas调用挂起函数(参见下文)。例如,.await()yield(),如图使用情况,因此将暂停了可以在库中定义的功能。标准库提供原始挂起函数,用于定义所有其他挂起函数。

  • 一个挂起的lambda - 一个必须在协程中运行的代码块。它看起来与普通的lambda表达式完全相同, 但其功能类型用suspend修饰符标记。就像常规lambda表达式是匿名本地函数的简短语法形式一样,挂起lambda是匿名挂起函数的简短语法形式。它可以通过调用挂起函数来暂停执行代码而不阻塞当前执行线程。例如,在大括号的代码块之后launchfuturebuildSequence功能,如图用例,是悬浮的lambda。

    注意:挂起lambdas可以在其代码的所有位置调用挂起函数,其中允许来自此lambda 的 非本地 return语句。也就是说,允许挂起内联lambda中的函数调用,例如apply{},但不允许noinlinecrossinline内部lambda表达式中。一个悬挂被视为一种特殊的非本地控制转移。

  • 悬浮函数类型 -是用于悬挂功能和lambda表达式的函数类型。它就像常规函数类型,但带有suspend修饰符。例如,suspend () -> Int是一种没有返回参数的挂起函数Int。声明的挂起函数suspend fun foo(): Int 符合此函数类型。

  • 一个协程构建器 - 一个将一些挂起的lambda作为参数的函数,创建一个协程,并且可选地以某种形式提供对其结果的访问。例如,launch{}future{},和buildSequence{}如所示的用例,是在一个库中定义的协程助洗剂。标准库提供原始协程构建器,用于定义所有其他协程构建器。

    注意:某些语言具有硬编码支持,用于创建和启动协程的特定方法,这些协程定义了如何表示其执行和结果。例如,generate 关键字可以定义一个返回某种可迭代对象的协程,而async 关键字可以定义一个返回某种承诺或任务的协程。Kotlin没有关键字或修饰符来定义和启动协程。协程构建器只是库中定义的函数。如果协程定义采用另一种语言的方法体的形式,在Kotlin中,这种方法通常是带有表达式主体的常规方法,包括调用某个库定义的协同构建器,其最后一个参数是挂起的lambda :

    有趣的 asyncTask()= async { ... }
  • 悬挂点 -是协程执行期间的点处的协程的执行可能被暂停。从语法上讲,挂起点是挂起函数的调用,但实际 挂起发生在挂起函数调用标准库原语以暂停执行时。

  • 延续 -是在悬挂点悬挂协程的状态。它概念性地表示在暂停点之后执行的其余部分。例如:

    buildSequence {
         for(i in  1 ... 10yield(i * i)
        println( over )
    }  

    这里,每次协程在挂起函数调用时暂停yield(), 其余的执行都表示为一个延续,所以我们有10个延续:首先运行循环i = 2和暂停,第二个运行循环i = 3和暂停等,最后一个打印“结束”并完成协程。创建的协程,但尚未 启动,由其整个执行类型的初始延续表示Continuation<Unit>

如上所述,协同程序的驱动要求之一是灵活性:我们希望能够支持许多现有的异步API和其他用例,并最大限度地减少硬编码到编译器中的部分。因此,编译器仅负责支持挂起函数,挂起lambdas以及相应的挂起函数类型。标准库中有很少的原语,其余的留给应用程序库。

延续界面

以下是标准库接口的定义Continuation,它表示通用回调:

interface  Continuation < in  T > {
    val  context  CoroutineContext
    fun  resumevalue  T)
    fun  resumeWithExceptionexception  Throwable)
}

协程上下文部分详细介绍了上下文,并表示与协程关联的任意用户定义上下文。功能resumeresumeWithException完成 了用于提供或者成功的结果(通过回调resume)或报告(通过故障resumeWithException)的协同程序完成。

暂停功能

一个典型的实现暂停功能.await()这个样子的:

暂停 乐趣 < T > CompletableFuture <T>。await() T = 
    suspendCoroutine <T> {cont  Continuation <T> - > 
        whenComplete {result,exception - > 
            if(exception ==  null//未来已经正常完成resume(result)
             else  //未来已完成, 
                续约为cont resumeWithException(exception)
        } 
    }

你可以在这里获得这个代码。注意:如果未来永远不会完成,这个简单的实现会永久挂起协同程序。kotlinx.coroutines中的实际实现稍微复杂一些,因为它支持取消。

suspend修饰符表明,这是可以暂停协程的执行的功能。此特定函数被定义为类型 上的 扩展函数CompletableFuture<T>以便其用法自然地以从左到右的顺序读取,该顺序对应于实际的执行顺序:

asyncOperation(...等待()

修饰符suspend可用于任何函数:顶级函数,扩展函数,成员函数或运算符函数。

注意,在当前版本的本地函数中,属性getter / setter和构造函数不能包含suspend修饰符。这些限制将在未来取消。

挂起函数可以调用任何常规函数,但要实际挂起执行,它们必须调用其他一些挂起函数。特别是,此await实现调用挂起函数,该函数 suspendCoroutine在标准库中定义为顶级挂起函数,方式如下:

suspend  fun < T > suspendCoroutineblock (Continuation <T>)- >  Unit T

suspendCoroutine协同程序内部调用时(它只能在协程内部调用,因为它是一个挂起函数),它在一个连续实例中捕获一个协同程序的执行状态,并将该continuation传递给指定block的参数。恢复协程的执行中,块可以调用任一continuation.resume()或 continuation.resumeWithException()在此线程或以一些其它线程。协程的实际暂停发生在suspendCoroutine块返回时没有调用它们中的任何一个。如果直接从块内部恢复继续,则不认为协程已被挂起并继续执行。

传递给值continuation.resume()成为返回值suspendCoroutine(),这反过来,成为返回值.await()

不允许多次恢复相同的延续并产生IllegalStateException

注意:这是Kotlin中的协程与Haskell中的Scheme或continuation monad等函数式语言中的第一类分隔连续之间的关键区别。选择仅支持有限的简历 - 一次性延续纯粹是务实的,因为没有一个预期的用例需要一流的延续,我们可以更有效地实现它们的有限版本。但是,通过克隆在continuation中捕获的协同程序的状态,可以将第一类continuation实现为单独的库,以便可以再次恢复其克隆。将来可以由标准库有效地提供该机制。

Coroutine建设者

无法从常规函数调用挂起函数,因此标准库提供了从常规非挂起作用域启动协程执行的函数。这是一个简单的 launch{} 协程构建器的实现

有趣的 启动上下文 CoroutineContext, suspend()- >  Unit=startCoroutine(StandaloneCoroutine(context))私有StandaloneCoroutine覆盖val 上下文 CoroutineContext)Continuation < Unit > {
     覆盖有趣的简历单位){} 覆盖有趣的resumeWithException异常)

        

       Throwable){
         val  currentThread  = Thread currentThread()
        currentThread uncaughtExceptionHandler uncaughtException(currentThread,exception)
    } 
}

你可以在这里获得这个代码。

此实现定义了一个StandaloneCoroutine表示此协程的简单类,并实现Continuation接口以捕获其完成。协程的完成调用了它的完成继续。 当协程完成相应的结果或异常时,调用它resumeresumeWithException函数。因为“即发即忘”协程,它被定义用于挂起具有返回类型的函数,并且实际上在其函数中忽略了该结果。如果协程执行完成了异常,则使用当前线程的未捕获异常处理程序来报告它。launchUnitresume

注意:这个简单的实现返回Unit并且根本不提供对协同程序状态的访问。kotlinx.coroutines中的实际实现更复杂,因为它返回一个Job表示协程的接口实例,可以取消。

协程上下文部分详细介绍了上下文。这里可以说,context在库定义的协程构建器中包含参数以便与可能定义有用上下文元素的其他库更好地组合是一种好的方式。

startCoroutine在标准库中定义为挂起函数类型的扩展。它的签名是:

fun < T >(suspend   () - > T)。startCoroutine(completion  Continuation <T>)

startCoroutine创建协同程序,并立即开始执行它,在当前线程(见下文说明),直到第一个悬挂点,然后返回。暂停点是协程体中一些暂停函数的调用,由相应的暂停函数的代码决定协程执行何时以及如何恢复。

注意:稍后介绍的continuation拦截器(来自上下文)可以将协程的执行(包括其初始延续)分派到另一个线程中。

协同上下文

协程上下文是一组持久的用户定义对象,可以附加到协程。它可能包括负责协程线程策略的对象,协程执行的日志记录,安全性和事务方面,协程标识和名称等。这是协同程序及其上下文的简单心智模型。将coroutine视为轻量级线程。在这种情况下,协程上下文就像一个线程局部变量的集合。不同之处在于线程局部变量是可变的,而协程上下文是不可变的,这对协同程序来说不是一个严重的限制,因为它们非常轻,以至于当需要更改某些内容时很容易启动新的协同程序在上下文中。

标准库不包含上下文元素的任何具体实现,但是具有接口和抽象类,以便可以以可组合的方式在库中定义所有这些方面,以便来自不同库的方面可以作为相同上下文的元素和平共存。

从概念上讲,协同上下文是一组索引元素,其中每个元素都有一个唯一的键。它是集合和地图之间的混合。它的元素具有像地图中的键,但它的键直接与元素相关联,更像是一组。标准库定义了最小接口CoroutineContext

interface  CoroutineContext {
     operator  fun < E   Element> getkey  Key <E>) E?
    fun < R > fold初始 R,操作(R,元素)- > R) R
     运算符 fun  pluscontext  CoroutineContext) CoroutineContext
     fun  minusKeykey  Key < *>) CoroutineContext 接口元素CoroutineContext {
         val key  Key < * > 
    } interface Key < E  Element> 
}

        

      

CoroutineContext本身有四个核心操作:

  • 运算符get提供对给定键的元素的类型安全访问。它可以与Kotlin运算符重载中[..]所解释的符号一起使用。
  • 函数的fold作用类似于Collection.fold 标准库中的扩展,并提供迭代上下文中所有元素的方法。
  • 运算符的plus工作方式类似于Set.plus 标准库中的扩展,并返回两个上下文的组合,其中右侧的元素使用左侧的相同键替换元素。
  • 函数minusKey返回不包含指定键的上下文。

一个Element协程上下文是上下文本身。它只是一个带有此元素的单例上下文。这样就可以通过获取协同上下文元素的库定义并将它们连接起来来创建复合上下文+。例如,如果一个库定义了auth具有用户授权信息的元素,而某个其他库定义了CommonPool具有某些执行上下文信息的对象,那么您可以使用带有组合上下文的launch{} 协同构建器来使用 launch(auth + CommonPool) {...}调用。

注意:kotlinx.coroutines提供了几个上下文元素,包括CommonPool将协程的执行分派到后台线程的共享池上的对象。

所有库定义的上下文元素都应扩展AbstractCoroutineContextElement标准库提供的类。对于库定义的上下文元素,建议使用以下样式。下面的示例显示了一个存储当前用户名的假设授权上下文元素:

class  AuthUserval  name  String AbstractCoroutineContextElement(AuthUser){
     companion object  Key   CoroutineContext < AuthUser > 
}

将context定义为Key相应元素类的伴随对象,可以流畅地访问上下文的相应元素。这是一个假设的挂起函数实现,需要检查当前用户的名称:

suspend  fun  secureAwait() Unit  = suspendCoroutine {cont - > 
    val  currentUser  = cont context [AuthUser] name
     //做一些特定于用户的事情 
}

继续拦截器

让我们回顾一下异步UI用例。异步UI应用程序必须确保协程体本身始终在UI线程中执行,尽管各种挂起函数在任意线程中恢复协程执行。这是使用延续拦截器完成的。首先,我们需要完全理解协程的生命周期。考虑使用launch{}协程构建器的代码片段:

launch(CommonPool){ 
    initialCode()//执行初始代码 
    f1 await()//暂停点#1 
    block1()//执行#1 
    f2 await()//暂停点#2 
    block2()//执行#2 
}

Coroutine从执行initialCode到第一个暂停点开始。在暂停点它 暂停,并在一段时间后,由相应的暂停功能定义,它恢复执行block1,然后它再次暂停并恢复执行block2,之后它完成

延续拦截器有一个选项来拦截和包裹对应于执行的延续initialCodeblock1block2从它们恢复到随后的悬挂点。协程的初始代码被视为其初始延续的恢复。标准库提供以下界面:

interface  ContinuationInterceptor   CoroutineContext元素 {
     companion object  Key   CoroutineContext < ContinuationInterceptor >
     fun < T > interceptContinuationcontinuation  Continuation <T>) Continuation <T> 
}

interceptContinuation函数包含了协同程序的延续。只要coroutine被挂起,协程框架就会使用以下代码行来包装实际continuation的后续恢复:

val  facade  =延续context [ContinuationInterceptor] interceptContinuation(续)延续

Coroutine框架为每个实际的continuation实例缓存生成的Facade。有关详细信息,请参阅 实现详细信息

注意,暂停函数await可能会或可能不会实际暂停执行协程。例如,暂停函数部分中await显示的实现在未来已经完成时实际上不会挂起协同程序(在这种情况下,它会立即调用并在没有实际暂停的情况下继续执行)。只有在执行协程期间发生实际暂停时才会截获延续,即当块返回而不调用时 。resumesuspendCoroutineresume

让我们看一下Swing拦截器的具体示例代码,该代码将执行分派到Swing UI事件派发线程。我们从SwingContinuation包装类的定义开始,该类检查当前线程并确保仅在Swing事件调度线程中继续继续。如果执行已经在UI线程中发生,那么Swing只需立即调用适当的cont.resume,否则它将使用延迟执行延迟到Swing UI线程SwingUtilities.invokeLater

私人  SwingContinuation < Ť >(VAL  续<T>)  继续 < Ť > 通过  {
     重写 乐趣 履历 T){
         如果(SwingUtilities类 isEventDispatchThread())CONT resume(value)
         else SwingUtilities invokeLater {续resume(value)} 
    } 覆盖有趣的resumeWithException异常

       Throwable的){
         如果(SwingUtilities类 isEventDispatchThread())CONT resumeWithException(exception)
         else SwingUtilities invokeLater {续resumeWithException(exception)} 
    } 
}

然后定义Swing将作为相应的上下文元素并实现 ContinuationInterceptor接口的对象:

object  Swing   AbstractCoroutineContextElement(ContinuationInterceptor),ContinuationInterceptor {
     override  fun < T > interceptContinuationcontinuation  Continuation <T>) Continuation <T> = 
        SwingContinuation(continuation)
}

你可以在这里获得这个代码。注意:kotlinx.coroutinesSwing对象的实际实现 还支持协程调试工具,该工具在当前运行此协程的线程名称中提供并显示当前运行的协同程序的标识符。

现在,可以使用带参数的launch{} coroutine builderSwing来执行在Swing事件调度线程中完全运行的协同程序:

启动(Swing){
   //此处的代码可以暂停,但在Swing EDT中将一直恢复 
}

限制暂停

需要一种不同类型的协同构建器和悬挂功能来实现buildSequence{}yield() 从发生器用例。这是buildSequence{}coroutine builder 的库代码:

fun < T > buildSequenceblock  suspend SequenceBuilder <T>。()- >  Unit Sequence <T> = Sequence { 
    SequenceCoroutine <T>()apply { 
        nextStep = block createCoroutine(receiver =  this,completion =  this)
    } 
}

它使用所谓的标准库不同的原始人createCoroutine创造协同程序,但启动它。相反,它返回其初始延续作为参考Continuation<Unit>。另一个区别是暂停 block此构建器的lambda是 带SequenceBuilder<T>接收器的 扩展lambda。该SequenceBuilder接口提供了生成器块的范围,并在库中定义为:

接口 SequenceBuilder < in  T > {
     suspend  fun  yieldvalue  T)
}

为了避免创建多个对象,buildSequence{}实现定义SequenceCoroutine<T>了实现SequenceBuilder<T>和实现的类Continuation<Unit>,因此它既可以作为receiver参数,createCoroutine也可以作为其completioncontinuation参数。简单的实现SequenceCoroutine<T>如下所示:

private  class  SequenceCoroutine < T >  AbstractIterator < T >(),SequenceBuilder < T >,Continuation < Unit > { 
    lateinit var  nextStep  Continuation < Unit > // AbstractIterator实现覆盖有趣的computeNext(){nextStep resume(Unit)} //完成继续实现覆盖val 上下文

    
      

    
      CoroutineContext get()= EmptyCoroutineContext
     override  fun  resumevalue  Unit){done()}
     覆盖 fun  resumeWithException异常 Throwable){ throw exception} //生成器实现覆盖suspend fun yield T){ 
        setNext(value)return suspendCoroutine {cont - > nextStep = cont} 
    } 
}

    
       
        

你可以在这里获得这个代码

实行yield用途suspendCoroutine 暂停功能暂停协程,并捕捉到它的延续。存储继续nextStep以便在computeNext调用时恢复 。

然而,buildSequence{}yield(),如上所示,是没有准备好一个任意暂停功能以捕获延续在它们的范围。他们同步工作。他们需要绝对控制如何捕获延续,存储它的位置以及何时恢复。它们形成有限的悬架范围。限制挂起的功能由@RestrictsSuspension放置在作用域类或接口上的注释提供,在上面的示例中,此作用域接口是SequenceBuilder

@RestrictsSuspension 
接口 SequenceBuilder < in  T > {
     suspend  fun  yieldvalue  T)
}

此批注对挂起可在SequenceBuilder{}同步协同生成器或类似同步协同程序构建器范围内使用的函数强制执行某些限制。任何扩展暂停lambda或具有受限制的挂起范围类或接口(标记为@RestrictsSuspension)作为其接收器的函数称为限制挂起函数。受限制的挂起函数只能在其受限制的挂起范围的同一实例上调用成员或扩展挂起函数。特别是,它意味着SequenceBuilder在其范围内没有lambda的扩展可以调用suspendContinuation或其他一般的挂起函数。要暂停执行generate协程,他们必须最终调用 SequenceBuilder.yield。实施yield本身是实现的成员函数,Generator 它没有任何限制(只有扩展挂起lambdas和函数受限制)。

为这样一个受限制的协同构建器支持任意上下文是没有意义的,sequenceBuilder 所以它总是很难编码EmptyCoroutineContext

更多例子

这是一个非规范性的部分,没有介绍任何新的语言结构或库函数,但显示了所有构建块如何构成涵盖各种用例。

包装回调

许多异步API都有回调式接口。suspendCoroutine标准库中的挂起函数提供了一种将任何回调包装到Kotlin挂起函数的简单方法。

有一个简单的模式。假设您具有someLongComputation接收Result此计算的回调函数。

有趣的 someLongComputation参数参数,回调:(结果)- >  单位

您可以使用以下简单代码将其转换为挂起函数:

暂停 乐趣 someLongComputationparams  Params) Result = suspendCoroutine {cont - > 
    someLongComputation(params){cont 简历(it)} 
}

现在这个计算的返回类型是显式的,但它仍然是异步的,并且不会阻塞线程。

对于更复杂的示例,让我们看一下 异步计算用例的aRead()函数。它可以作为Java NIO的挂起扩展函数 及其 回调接口实现,具有以下代码:AsynchronousFileChannelCompletionHandler

暂停 有趣的 AsynchronousFileChannel。的areadBUF  ByteBuffer的) 诠释 = 
    suspendCoroutine {续- > 
        读(BUF,0L单位,对象 CompletionHandler < 诠释单位 > {
             重写 乐趣 完成bytesRead  诠释附件 单位){ 
                CONT 恢复(bytesRead)
            } 覆盖的乐趣失败(例外

             Throwable,附件 单位){ 
                续resumeWithException(exception)
            } 
        })
    }

你可以在这里获得这个代码。注意:kotlinx.coroutines中的实际实现 支持取消以中止长时间运行的IO操作。

如果您正在处理许多共享相同类型回调的函数,那么您可以定义一个通用的包装函数,以便轻松地将它们全部转换为挂起函数。例如, vert.x使用其所有异步函数Handler<AsyncResult<T>>作为回调接收的特定约定 。为了简化协同程序中任意vert.x函数的使用,可以定义以下辅助函数:

直列 暂停 乐趣 < Ť > VXcrossinline  回调(处理程序<AsyncResult <T >>)- >  单元=  
    suspendCoroutine <T> {续- > 
        回调(处理程序{结果 AsyncResult <T >> 
            如果(结果成功()){ 
                续恢复(结果结果())
            } 其他 { 
                续 resumeWithException(结果原因())
            }
        })
    }

使用此辅助函数,async.foo(params, handler) 可以从协程调用任意异步vert.x函数vx { async.foo(params, it) }

建设期货

期货用例的future{}构建器可以为任何future或promise原型定义,类似于coroutine builder部分中解释的构建器launch{}

fun < T > futurecontext  CoroutineContext = CommonPool,block  suspend()- > T) CompletableFuture <T> = 
        CompletableFutureCoroutine <T>(context)也{块startCoroutine(completion = it)}

第一个区别launch{}是它返回一个实现 CompletableFuture,另一个区别是它是用默认CommonPool上下文定义的,因此它的默认执行行为类似于CompletableFuture.supplyAsync 运行其代码的 方法 ForkJoinPool.commonPool。基本实现CompletableFutureCoroutine很简单:

class  CompletableFutureCoroutine < T >(覆盖 val  上下文 CoroutineContext) CompletableFuture < T >(),Continuation < T > {
     覆盖 有趣的 简历 T){complete(value)}
     覆盖 有趣的 resumeWithException异常 Throwable){completeExceptionally(例外)} 
}

你可以在这里获得这个代码。kotlinx.coroutines中的实际实现更为高级,因为它会传播取消生成的未来以取消协程。

该协程的完成调用了complete将来的相应方法来记录该协程的结果。

无阻塞睡眠

协同程序不应该使用Thread.sleep,因为它会阻塞一个线程。但是,delay使用Java 实现挂起非阻塞功能非常简单ScheduledThreadPoolExecutor

private  val  executor  = Executors newSingleThreadScheduledExecutor { 
    Thread(it, scheduler 申请:{isDaemon =   } 
} 暂停乐趣延迟时间单位 TIMEUNIT = TIMEUNIT MILLISECONDS单位= suspendCoroutine {续- > 
    执行时间表({续简历(单位

     ),时间,单位)
}

你可以在这里获得这个代码。注意:kotlinx.coroutines还提供了delay功能。

注意,这种delay函数恢复在其单个“调度程序”线程中使用它的协同程序。正在使用的协同程序拦截器一样Swing不会留在这个线程来执行,因为他们拦截分派他们到合适的线程。没有拦截器的协同程序将继续在此调度程序线程中执行。因此,此解决方案便于演示,但它不是最有效的解决方案。建议在相应的拦截器中本地实现睡眠。

对于Swing拦截器,非阻塞睡眠的本机实现应使用 专门为此目的设计的Swing Timer

暂停 乐趣摇摆。delaymillis  Int Unit  = suspendCoroutine {cont - > 
    Timer(millis){续简历(单位)} apply { 
        isRepeats =  false 
        start()
    } 
}

你可以在这里获得这个代码。注意:kotlinx.coroutines实现delay了解拦截器特定的睡眠设施,并在适当的情况下自动使用上述方法。

协作单线程多任务处理

编写协作的单线程应用程序非常方便,因为您不必处理并发和共享可变状态。JS,Python和许多其他语言没有线程,但具有协作式多任务原语。

Coroutine拦截器提供了一个简单的工具来确保所有协同程序都被限制在一个线程中。这里的示例代码 定义newSingleThreadContext()了创建单线程执行服务并使其适应协程拦截器要求的函数。

我们将在以下示例future{}中的构建期货部分中定义的coroutine构建器中使用它,它在单个线程中工作,尽管它内部有两个同步的任务,它们都是活动的。

fun  mainargs  Array < String >){ 
    log( Starting MyEventThread val  context  = newSingleThreadContext( MyEventThread val  f  = future(context){ 
        log( Hello,world!val  f1  = future(context ){ 
            log( f1正在睡觉)
            延迟(1000//sleep 1s 
            log( f1 return 1 1 
        } val f2 = future(context){ 
            log( f2 is sleeping )
            delay(1000// sleep 1s 
            log( f2 return 2 2 
        } 
        log( I '等待f1和f2。它应该只需要一秒钟!val sum = f1 await()+
            f2 await()
        log(和总和是$ sum )
    } 
    f get()
    log( Terminated )
}

你可以在这里得到完整的工作示例。注意:kotlinx.coroutines具有现成的实现功能 newSingleThreadContext

如果整个应用程序基于单线程执行,则可以为单线程执行工具定义具有硬编码上下文的自己的帮助程序协同程序构建器。

异步序列

buildSequence{}限制悬架 部分中示出的协程构建器是同步协程的示例。协同程序中的生产者代码在消费者调用时在同一个线程中同步调用Iterator.next()。在buildSequence{}协程块被限制,使用第三方如图悬浮剂等非同步型文件IO函数它不能挂起其执行包装的回调部分。

一个异步序列生成器允许随意暂停和恢复其执行。这意味着当数据尚未生成时,其消费者应准备好处理该案例。这是暂停功能的自然用例。让我们定义SuspendingIterator类似于常规 Iterator 接口的接口,但它next()hasNext()函数是挂起的:

interface  SuspendingIterator < out  T > {
     suspend  operator  fun  hasNext() Boolean 
    suspend  operator  fun  next() T 
}

定义SuspendingSequence类似于标准, Sequence 但它返回SuspendingIterator

interface  SuspendingSequence < out  T > {
     operator  fun  iterator() SuspendingIterator <T> 
}

我们还为其定义了一个范围接口,类似于同步序列构建器的范围,但它的暂停不受限制:

interface  SuspendingSequenceBuilder < in  T > {
     suspend  fun  yieldvalue  T)
}

构建器功能suspendingSequence{}类似于同步generate{}。它们的不同之处在于实现细节SuspendingIteratorCoroutine以及在这种情况下接受可选上下文是有意义的:

有趣 < Ť > suspendingSequence上下文 CoroutineContext = EmptyCoroutineContext,
         。暂停SuspendingSequenceBuilder <T>()- >  单位 SuspendingSequence <T> =对象 SuspendingSequence <T> {
     重写 乐趣 迭代器()  SuspendingIterator <T> = suspendingIterator(context,block)

}

你可以在这里获得完整的代码。注意:kotlinx.coroutines有一个Channel原语实现, 带有相应的produce{}协程构建器,可以更灵活地实现相同的概念。

让我们newSingleThreadContext{}协作单线程多任务部分和非阻塞睡眠部分的非阻塞delay功能中获取上下文 。这样我们就可以编写一个非阻塞序列的实现,它产生1到10的整数,在它们之间休眠500毫秒:

val  seq  = suspendingSequence(context){
     for(i in  1 ... 10){
         yield(i)
        delay(500L)
    } 
}

现在,消费者协程可以按照自己的节奏消耗这个序列,同时还可以暂停其他任意挂起功能。注意,Kotlin for循环 按惯例工作,因此不需要await for在该语言中使用特殊的循环结构。常规for循环可用于迭代我们上面定义的异步序列。只要生产者没有价值,它就会被暂停:

(价值 SEQ){ //暂停,等待生产者
    //做一些与价值在这里,可能会暂停在这里,太 
}

您可以找到一个带有一些日志记录的示例,说明此处的执行情况

通道

Go-style类型安全通道可以在Kotlin中作为库实现。我们可以为具有挂起功能的发送通道定义一个接口send

interface  SendChannel < T > {
     suspend  fun  sendvalue  T)
     fun  close()
}

和具有挂起功能的接收器通道receive以及operator iterator异步序列类似的样式:

interface  ReceiveChannel < T > {
     suspend  fun  receive() T
     suspend  operator  fun  iterator() ReceiveIterator <T> 
}

Channel<T>类实现这两个接口。的send,挂起当信道缓冲器是满的,而receive当所述缓冲器是空的暂停。它允许我们几乎逐字地将Go风格的代码复制到Kotlin中。从Go的第4个并发示例中将斐波fibonaccin契数字发送到通道 的函数 在Kotlin中看起来像这样:

暂停 有趣的 斐波那契n  Intc  SendChannel < Int >){
     var  x  =  0 
    var  y  =  1 
    for(i in  0 .. n -  1){ 
        c send(x)
         val  next  = x + y 
        x = y 
        y = next 
    } 
    c close()
}

我们还可以定义Go-style go {...}块以在某种多线程池中启动新的协同程序,该多线程池将任意数量的轻量级协同程序分派到固定数量的实际重量级线程上。这里的示例实现简单地写在Java的常见之上ForkJoinPool

使用这个go协同程序构建器,相应的Go代码的主要功能如下所示,其中mainBlockingrunBlocking与使用相同的池的快捷方式帮助程序函数go{}

fun  mainargs  Array < String >)= mainBlocking {
     val  c  = Channel < Int >(2)
    go {fibonacci(10,c)}
     for(i in c){ 
        println(i)
    } 
}

你可以在这里查看工作代码

您可以自由地使用通道的缓冲区大小。为简单起见,在示例中仅实现了缓冲通道(最小缓冲区大小为1),因为无缓冲通道在概念上类似于 之前介绍的异步序列

Go-style select控制块暂停,直到其中一个操作在其中一个通道上可用,可以实现为Kotlin DSL,因此 Go之旅的第5个并发示例 在Kotlin中将如下所示:

暂停 有趣的 斐波那契c  SendChannel < Int >,退出 ReceiveChannel < Int >){
     var  x  =  0 
    var  y  =  1 
    whileSelect { 
        c onSend(x){
             val  next  = x + y 
            x = y 
            y = next
             true  // continue while loop 
        } 
        quit onReceive {
            println( quit false  // break while loop 
        } 
    } 
}

你可以在这里查看工作代码

示例具有两者的实现select {...},它返回其中一个案例的结果,如Kotlin when表达式,以及与较少的大括号whileSelect { ... }相同的便利性while(select<Boolean> { ... })

Go之旅的第6个并发示例中的默认选择案例 仅在select {...}DSL中添加了一个案例:

fun  mainargs  Array < String >)= mainBlocking {
     val  tick  =时间tick(100val  boom  =时间在(500)之后
    选择{ 
        tick onReceive { 
            println( tick。true  //继续循环 
        } 
        繁荣onReceive { 
            println(繁荣!false  // break loop 
        } 
        onDefault { 
            println()
            delay(50true  // continue loop 
        } 
    } 
}

你可以在这里查看工作代码

Time.tickTime.after正在实施的平凡 这里有无阻塞delay功能。

这里可以找到其他示例以及注释中相应Go代码的链接。

请注意,此通道的示例实现基于单个锁来管理其内部等待列表。它使人们更容易理解和推理。但是,它永远不会在此锁下运行用户代码,因此它是完全并发的。此锁仅在某种程度上将其可伸缩性限制为非常大量的并发线程。

信道的和实际的实现selectkotlinx.coroutines 是基于无锁不相交的存取并行数据结构。

此通道实现独立于协程上下文中的拦截器。它可以在事件线程拦截器下的UI应用程序中使用,如相应的延续拦截器部分所示,或者与任何其他部件一起使用,或根本没有拦截器(在后一种情况下,执行线程仅由代码确定)协程中使用的其他暂停函数)。通道实现只提供线程安全的非阻塞挂起功能。

互斥

编写可伸缩的异步应用程序是一个遵循的规则,确保一个代码永远不会阻塞,但是暂停(使用挂起函数),而不实际阻塞线程。Java并发原语就像 ReentrantLock 是线程阻塞的,它们不应该用在真正的非阻塞代码中。要控制对共享资源的访问,可以定义Mutex暂停执行协程而不是阻止它的类。相应类的标题如下:

class  Mutex {
     suspend  fun  lock()
     fun  unlock()
}

你可以在这里得到全面的实施。kotlinx.coroutines中的实际实现 还有一些额外的功能。

使用非阻塞互斥体这种实现, Go的游览的第9个并发示例 可以使用Kotlin转换为Kotlin try-finally ,其目的与Go的目的相同defer

class  SafeCounter {
     private  val  v  = mutableMapOf < StringInt >()
     private  val  mux  = Mutex()suspend fun inckey String){ 
        mux lock()
         try {v [key] = v getOrDefault(key,0+ 1 }
         finally {mux unlock()} 
    } suspend fun get关键字 字符串 Int?{ 
        mux lock()
         返回 try {v [key]}
         finally {mux 解锁()} 
    } 
}

你可以在这里查看工作代码

高级主题

本节介绍了一些涉及资源管理,并发和编程风格的高级主题。

资源管理和GC

协同程序不使用任何堆外存储,并且不会自己使用任何本机资源,除非在协程内运行的代码确实打开了文件或其他资源。虽然必须以某种方式关闭在协程中打开的文件,但协同程序本身不需要关闭。当协程被暂停时,其整个状态可通过参考其继续来获得。如果你失去了对暂停的coroutine延续的引用,那么它最终会被垃圾收集器收集。

打开一些可靠资源的协同程序值得特别关注。考虑以下协程,它使用buildSequence{}来自受限制暂停部分的构建器来从文件生成一系列行:

fun  sequenceOfLinesfileName  String= buildSequence < String > { 
    BufferedReader(FileReader(fileName))使用{
         ){
             产量(它的readLine()  休息)
        } 
    } 
}

此函数返回a Sequence<String>,您可以使用此函数以自然方式打印文件中的所有行:

sequenceOfLines( examples / sequence / sequenceOfLines.kt forEach(:: println)

你可以在这里获得完整的代码

只要您完全迭代sequenceOfLines函数返回的序列,它就可以正常工作。但是,如果您只打印此文件中的几行,请执行以下操作:

sequenceOfLines( examples / sequence / sequenceOfLines.kt 拿(3forEach(:: println)

然后协程恢复几次以产生前三行并被废弃。可以放弃协程本身,但不能打开文件。该 use函数 将无法完成其执行并关闭文件。该文件将保持打开状态,直到GC收集,因为Java文件具有finalizer关闭文件的文件。对于小型滑动软件或短期运行的实用程序来说,这不是一个大问题,但对于具有多GB堆的大型后端系统来说,这可能是一个灾难,它可能会耗尽打开的文件句柄而不是耗尽内存触发GC。

这与Java的Files.lines 方法类似 ,产生了懒惰的流线。它返回一个可关闭的Java流,但大多数流操作不会自动调用相应的Stream.close方法,用户需要记住关闭相应流的必要性。可以在Kotlin中定义可关闭的序列生成器,但它们会遇到类似的问题,即语言中的自动机制无法确保它们在使用后关闭。明确地超出了Kotlin协程的范围,为自动化资源管理引入了一种语言机制。

但是,通常这个问题不会影响协同程序的异步使用情况。异步协程永远不会被放弃,但最终会一直运行直到完成,所以如果协程中的代码正确地关闭了它的资源,那么它们最终将被关闭。

并发和线程

每个单独的协程,就像一个线程一样,按顺序执行。这意味着以下类型的代码在协程中非常安全:

launch(CommonPool){ //启动协程
    val  m  = mutableMapOf < StringString >()
     val  v1  = someAsyncTask1()//启动一些异步任务
    val  v2  = someAsyncTask2()//启动一些异步任务 
    m [  k1  ] = v1 await()//映射修改等待等待 
    m [  k2  ] = v2 await()//地图修改等待等待 
}

您可以在特定协程的范围内使用所有常规单线程可变结构。但是,协同程序之间共享可变状态是有潜在危险的。如果您使用一个coroutine构建器来安装一个调度程序来恢复单个事件派发线程中的所有协程JS样式,就像继续拦截器部分中Swing显示的拦截器一样,那么您可以安全地使用通常从此事件修改的所有共享对象-dispatch线程。但是,如果您在多线程环境中工作或以其他方式在运行在不同线程中的协程之间共享可变状态,则必须使用线程安全(并发)数据结构。

协同程序就像线程一样,虽然它们更轻巧。你可以在几个线程上运行数百万个协同程序。运行协程总是在某个线程中执行。但是,挂起的协同程序不会消耗线程,并且它不会以任何方式绑定到线程。恢复此协程的挂起函数通过调用Continuation.resume此线程来确定协程恢复的线程,并且协同程序的拦截器可以覆盖此决策并将协程的执行分派到不同的线程上。

异步编程风格

有不同风格的异步编程。

异步计算部分讨论了回调,并且通常是协同程序旨在替换的最不方便的样式。如示出的任何回调风格的API可以缠绕到相应的暂停功能在这里

让我们回顾一下。例如,假设您从具有以下签名的假设阻塞 sendEmail函数开始:

有趣的 sendEmailemailArgs  EmailArgs) EmailResult

它在运行时可能会长时间阻塞执行线程。

要使其成为非阻塞,您可以使用,例如,error-first node.js回调约定 ,以回调样式表示其非阻塞版本,并具有以下签名:

有趣的 sendEmailemailArgs  EmailArgs,回调:( Throwable?,EmailResult?)- >  单位

但是,协同程序支持其他类型的异步非阻塞编程。其中之一是async / await风格,内置于许多流行语言中。在Kotlin中,这种风格可以通过引入future{}.await()库函数来复制,这些函数作为期货用例部分的一部分显示出来。

这种样式由约定表示从函数返回某种未来对象,而不是将回调作为参数。在这种异步风格中,签名sendEmail将如下所示:

有趣的 sendEmailAsyncemailArgs  EmailArgs)未来的<EmailResult>

作为一种风格问题,最好为Async这些方法名称添加后缀,因为它们的参数与阻塞版本没有区别,并且很容易忘记忘记其操作的异步性质。该函数sendEmailAsync启动并发异步操作,并可能带来并发的所有陷阱。但是,促进这种编程风格的语言通常也有某种await原语,可以根据需要将执行带回序列中。

Kotlin的原生编程风格基于暂停功能。在这种风格中,sendEmail自然的签名 ,没有任何修改其参数或返回类型,但有一个额外的 suspend修饰符:

暂停 有趣的 sendEmailemailArgs  EmailArgs) EmailResult

可以使用我们已经看到的基元轻松地将异步和挂起样式转换为彼此。例如,sendEmailAsync可以通过sendEmail使用 future协程构建器挂起来实现:

有趣的 sendEmailAsyncemailArgs  EmailArgs)未来<EmailResult> = future { 
    sendEmail(emailArgs)
}

同时暂停功能sendEmail可以通过执行sendEmailAsync使用 .await()悬浮功能

暂停 有趣的 sendEmailemailArgs  EmailArgs) EmailResult =  
    sendEmailAsync(emailArgs)等待()

因此,从某种意义上说,这两种风格是相同的,并且在方便性方面都明显优于回调风格。但是,让我们更深入地了解sendEmailAsync和暂停之间的区别sendEmail

让我们先比较一下他们如何构成。挂起函数可以像普通函数一样组成:

暂停 乐趣 largerBusinessProcess(){
     //大量的代码在这里,然后里面的某个地方 
    sendEmail(emailArgs)//别的推移之后 
}
    

相应的异步样式函数以这种方式组成:

有趣的 largeBusinessProcessAsync()= future {
    //这里有很多代码,然后是 
   sendEmailAsync(emailArgs)里面的某个地方await()
    //之后继续发生的事情 
}

请注意,异步样式的函数组合更冗长,更容易出错。如果.await()在async-style示例中省略了调用,代码仍然可以编译并运行,但它现在可以异步或甚至与更大的业务流程的其余部分同时执行电子邮件发送过程,从而可能修改某些共享状态并引入一些非常难以重现的状态错误。相反,默认情况下,挂起功能是顺序的。使用挂起函数,无论何时需要任何并发,都可以在源代码中使用某种future{}或类似的协程构建器调用来明确表达它。

比较这些样式如何使用许多库扩展到一个大项目。暂停功能是Kotlin中的轻量级语言概念。所有暂停功能都可以在任何不受限制的Kotlin协程中使用。异步样式函数依赖于框架。每个promises / futures框架都必须定义async自己的类似函数,它返回自己的promise / future类和它自己的await类函数。

比较他们的表现。挂起函数为每次调用提供最小的开销。您可以查看实施细节部分。除了所有挂起机制之外,异步风格的函数还需要保持相当重的承诺/未来抽象。一些类似未来的对象实例必须始终从异步样式函数调用返回,即使函数非常简短,也无法进行优化。异步风格不适合非常细粒度的分解。

比较它们与JVM / JS代码的互操作性。异步样式函数与使用匹配类型的类似未来抽象的JVM / JS代码更具互操作性。在Java或JS中,它们只是返回相应的类似未来对象的函数。暂停函数看起来很奇怪,任何语言本身都不支持 continual-passing-style。但是,您可以在上面的示例中看到将任何挂起函数转换为任何给定promise / future框架的异步样式函数是多么容易。因此,您只需在Kotlin中编写一次暂停函数,然后使用适当的future{}协程生成器函数,使用一行代码将其与任何样式的承诺/未来进行互操作 。

实施细节

本节提供了协同程序实现细节的一瞥。它们隐藏在协程概述部分中解释的构建块之后,它们的内部类和代码生成策略可以随时更改,只要它们不违反公共API和ABI的合同即可。

延续传球风格

暂停功能通过Continuation-Passing-Style(CPS)实现。每个挂起函数和挂起的lambda都有一个额外的Continuation 参数,在调用它时会隐式传递给它。回想一下,await暂停函数的声明如下所示:

暂停 乐趣 < T > CompletableFuture <T>。await() T

但是,CPS转换后其实际实现具有以下签名:

有趣 < T > CompletableFuture <T>。等待继续<T>) 任何

其结果类型T已在其附加的continuation参数中移入type参数的位置。实现结果类型Any?旨在表示挂起函数的操作。当挂起函数暂停 coroutine时,它返回一个特殊的标记值 COROUTINE_SUSPENDED。当挂起函数不挂起协同程序但继续执行协程时,它会返回其结果或直接抛出异常。这样,实现的Any?返回类型await实际上是一个 COROUTINE_SUSPENDED并且T不能在Kotlin的类型系统中表达的并集。

暂停函数的实际实现不允许直接调用其堆栈帧中的延续,因为这可能导致长时间运行的协同程序上的堆栈溢出。suspendCoroutine标准库中的函数通过跟踪continuation的调用来隐藏应用程序开发人员的这种复杂性,并确保符合挂起函数的实际实现契约,而不管调用continuation的方式和时间。

国家机器

有效地实现协程是至关重要的,即尽可能少地创建类和对象。许多语言通过状态机实现它们,Kotlin也这样做。在Kotlin的情况下,这种方法导致编译器每个挂起的lambda只创建一个类,它的主体中可能有任意数量的挂起点。

主要思想:将挂起函数编译到状态机,其中状态对应于挂起点。示例:让我们采用具有两个暂停点的暂停块:

val  a  = a()
 val  y  = foo(a)await()//暂停点#1 
b()val z = bar(a,y)await()//暂停点#2 
c(z)
  

这段代码有三种状态:

  • 初始(在任何暂停点之前)
  • 在第一个暂停点之后
  • 在第二个暂停点之后

每个州都是这个区块的一个延续的入口点(最初的延续从第一条线开始)。

代码被编译为一个匿名类,该类具有实现状态机的方法,保存状态机当前状态的字段,以及状态之间共享的协同程序的局部变量字段(也可能有关闭字段协程,但在这种情况下,它是空的)。这是上面的块的伪Java代码,它使用连续传递样式来调用挂起函数await

class < anonymous_for_state_machine >扩展了CoroutineImpl <。.. > implements Continuation < Object > {
     //状态机的当前状态
    int label =  0 
    
    //协程的局部变量
    A a =  null 
    Y y =  null 
    
    void resume(Object data){
         if(label ==  0)转到L0 
        如果(标签==  1)转到L1 
        if(label ==  2)goto L2 
        else  throw IllegalStateException()L0 //在此调用中数据应为“null” 
        a = a()
        label = 1 
        data = foo(a)await(this// 'this'作为延续传递if(data == COROUTINE_SUSPENDEDreturn //如果await已暂停执行则返回L1 //
        
      
         
          
      
        外部代码已恢复此协程,传递.await()的结果作为数据 
        y =Y)数据
        b()
        label =  2 
        data = bar(a,y)await(this// 'this'作为延续传递
        if(data ==  COROUTINE_SUSPENDED返回 //返回如果await已暂停执行
      L2 
        //外部代码已恢复此协同程序将.await()的结果作为数据传递
        Z z =Z)data 
        c(z)
        label =  - 1  //不再允许
        返回步骤 
    }           
}    

请注意,有一个goto运算符和标签,因为该示例描述了字节代码中发生的情况,而不是源代码中发生的情况。

现在,当coroutine启动时,我们调用它resume()label0,然后我们跳转到L0,然后我们做一些工作,设置label为下一个状态 - 如果协程的执行被暂停则1调用.await()并返回。当我们想要继续执行时,我们resume()再次调用,现在它 继续执行,执行L1一些工作,将状态设置为2,调用.await()并在暂停时再次返回。下次它继续从L3设置状态到-1意味着“结束,没有更多的工作要做”。

循环内的暂停点只生成一个状态,因为循环也可以通过(条件)goto

var  x  =  0 
(x <  10){ 
    x + = nextNumber()await()
}

生成为

class < anonymous_for_state_machine >扩展了CoroutineImpl <。.. > implements Continuation < Object > {
     //状态机的当前状态
    int label =  0 
    
    // coroutine的局部变量
    int x void resume(对象数据){
         if(label == 0)goto L0 if(label == 1)转到L1 else 抛出 IllegalStateException()
    
     
         
         
        
      L0  
        x =  0 
      LOOP 
        if(x >  10)goto END 
        label =  1 
        data = nextNumber()await(this// 'this'作为延续传递
        if(data ==  COROUTINE_SUSPENDEDreturn  //返回如果await已暂停执行
      L1 
        //外部代码已恢复此协同程序将.await()的结果作为数据传递 
        x + =((整数)数据)intValue()
        label =  - 1 
        goto LOOP 
      END  
        label =  - 1  //不允许更多步骤
        返回  
    }           
}    

编译挂起函数

用于挂起函数的已编译代码取决于它何时以及何时调用其他挂起函数。在最简单的情况下,挂起函数仅在尾部位置调用其他挂起函数, 从而对它们进行尾调用。这是暂停实现低级同步原语或包装回调的函数的典型情况,如挂起函数和 包装回调节所示。这些函数调用一些其他挂起函数,如suspendCoroutine尾部位置。它们的编译方式与常规的非挂起函数一样,唯一的例外是它们从CPS转换中获得的隐式延续参数 传递给尾调用中的下一个挂起函数。

注意:在当前实现中,Unit返回函数必须包含一个显式return语句,其中包含另一个挂起函数的调用,以便将其识别为尾调用。

在暂停调用出现在非尾部位置的情况下,编译器为相应的挂起函数创建 状态机。调用挂起函数时创建的状态机对象的实例,并在完成时被丢弃。

注意:在将来的版本中,可以优化此编译策略,以便仅在第一个挂起点创建状态机的实例。

反过来,该状态机对象用作在非尾部位置调用其他挂起函数的完成继续。当函数对其他挂起函数进行多次调用时,将更新并重用此状态机对象实例。将此与其他异步编程样式进行比较,其中异步处理的每个后续步骤通常使用单独的,新分配的闭包对象来实现。

协同内在论

suspendCoroutine标准库中挂起函数的实际实现是用Kotlin本身编写的,其源代码作为标准库源包的一部分提供。为了提供协同程序的安全和无问题的使用,它将状态机的实际延续包装在协程的每个暂停上的附加对象中。这对于异步计算期货等真正的异步用例来说非常好,因为相应的异步原语的运行时成本远远超过额外分配对象的成本。但是,对于发电机用例,这种额外的成本是令人望而却步的。

kotlin.coroutines.experimental.intrinsics标准库包中包含命名的函数suspendCoroutineOrReturn 具有以下特征:

暂停 乐趣 < T > suspendCoroutineOrReturn:(继续<T>)- >  任何?) T

它提供了对持续传递样式的暂停函数的直接访问以及对继续的未经检查的引用。suspendCoroutineOrReturn承担全部责任的用户 遵循CPS结果惯例,但结果获得稍好的性能。该公约是通常很容易遵循buildSequenceyield般协程,但试图写入异步await式的悬挂在顶部的功能suspendCoroutineOrReturn被 劝阻,因为他们是非常棘手的正确实施,而不的帮助suspendCoroutine 和错误在这些实现的尝试通常heisenbugs 违抗试图通过测试找到并重现它们。

还有一些createCoroutineUnchecked使用以下签名调用的函数:

有趣 < Ť >(暂停() - > T).createCoroutineUnchecked(完成续<T>) 继续< 单位 >
 乐趣 < - [R Ť >(暂停 - [R 。() - > T).createCoroutineUnchecked(接收机 R,完成继续<T>)继续< 单位 >

它们返回未经检查的对初始延续的引用(没有附加的包装器对象)。
优化版本的buildSequencevia createCoroutineUnchecked如下所示:

fun < T > buildSequenceblock  suspend SequenceBuilder <T>。()- >  Unit Sequence <T> = Sequence { 
    SequenceCoroutine <T>()apply { 
        nextStep = block createCoroutineUnchecked(receiver =  this,completion =  this)
    } 
}

yield通道的优化版本suspendCoroutineOrReturn如下所示。注意,因为yield总是挂起,所以相应的块总是返回COROUTINE_SUSPENDED

//生成器实现
覆盖 suspend  fun  yield T){
    setNext(value) return suspendCoroutineOrReturn {cont - > 
        nextStep = cont
         COROUTINE_SUSPENDED 
    }
}
    

你可以在这里获得完整的代码

kotlin.coroutines.experimental.intrinsics在IDEA的Kotlin插件中隐藏包的内容以防止意外使用。您需要手动编写相应的import语句才能访问上述内部函数。

修订记录

本节概述了协程设计的各种修订之间的变化。

修订版3.2的变化

  • 添加了createCoroutineUnchecked内在的描述。

修订版3.1的变化

此修订版在Kotlin 1.1.0版本中实现。

  • kotlin.coroutines包装被替换为kotlin.coroutines.experimental
  • SUSPENDED_MARKER重命名为COROUTINE_SUSPENDED
  • 澄清了协同程序的实验状态。

修订版3的变化

此修订版在Kotlin 1.1-Beta中实施。

  • 挂起函数可以在任意点调用其他挂起函数。
  • 协程调度程序被推广到协程上下文:
    • CoroutineContext 界面介绍。
    • ContinuationDispatcher接口被替换为ContinuationInterceptor
    • createCoroutinestartCoroutine参数dispatcher已删除。
    • Continuation界面包括val context: CoroutineContext
  • CoroutineIntrinsics对象替换为kotlin.coroutines.intrinsics包。

修订版2的变化

此修订在Kotlin 1.1-M04中实施。

  • 所述coroutine关键字由悬浮功能型取代。
  • Continuation 挂起函数隐含在调用站点和声明站点上。
  • suspendContinuation 提供捕获延续是在需要时暂停功能。
  • 持续传递样式转换可以防止非挂起调用的堆栈增长。
  • createCoroutinestartCoroutinecoroutine建设者介绍。
  • 协同控制器的概念被删除:
    • 协程完成结果通过Continuation接口提供。
    • 可以通过协同程序选择协同作用域receiver
    • 可以在没有接收器的顶层定义挂起功能。
  • CoroutineIntrinsics 对于性能比安全性更重要的情况,对象包含低级基元。

 

按h打开包含更多详细信息的悬浮卡。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值