Kotlin - Go语言中文社区

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视为轻量级线程。在这种情况下,协程上下文就像一个线程局部变量的集合。不同之处在于线程局部变量是可变的,而协程上

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/m0_38126177/article/details/81557831
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2019-08-27 14:09:29
  • 阅读 ( 1557 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢