标签 翻译 下的文章

【翻译】极简主义工程师宣言

🌐 中文翻译

Manifesto for Minimalist Software Engineers

  • 为帕累托法则奋斗

    • Fight for Pareto's law, look for the 20% of effort that will give you the 80% of results.
  • 优先级

    • Prioritize, minimalism isn't about not doing things but about focusing first in the important.
  • 完美主意是好东西的敌人

    • Perfect is enemy of good, first do it, then do it right, then do it better.
  • 扼杀在摇篮中

    • Kill the baby, don't be afraid of starting all over again. Fail soon, learn fast.
  • 赋能

    • Add value. Think constantly how you can help your team and position yourself in that field/skill.
  • 基础优先

    • Basics, first. Follow always a top-down thinking starting by the best-practises of CS.
  • 非同凡想

    • Think different. Simple is harder than complex, which means you'll need to use your creativity.
  • 综合

    • Synthesis is the key of communication. We have to write code for humans not machines.
  • 保持空白

    • Keep it plain. Try to keep your designs with few layers of indirection.
  • 清除 kipple

    • Clean kipple and redundancy. Minimalism is all about removing distractions.

Ⅰ为帕累托法则奋斗

帕累托原则说明你的应用所取得的 80% 的结果是由 20% 的特性决定的。在计划下一个任务时,要把这个观点牢记在脑中。

最好的工程师能估算需求所需的付出,他们有足够的信息拒绝在最佳范围内之外的 80% 的工作。

践行这个法则!团队不是军队,公司付钱让你做出决定和承担责任,所以如果你觉得需求是多余的应该大声提出来。

你写过的最糟糕的代码就是你不再使用的代码。这样的代码被称为 YAGNI (You Are Not Gonna Need It).

你写过的最好的代码就是你没有写过的代码,因为它是唯一永不失败的代码。

Ⅱ 优先级

有一些 bug 的修复方法我称为沉淀(通过积累其他的 bug 来修复当前 bug),还有一些称为侵蚀(客户改变了他的主意)。这些都是利用优先级和等待,来把你从无价值的工作中拯救出来的例子。

极简主义并不是说不做事情,而是聚焦于最重要的事情。

Ⅲ 完美主义是好东西的敌人

可以寻找完美,但不是现在。迭代才是那位会及时给予你好的建议的朋友。

首先去做,然后做好,接着做得更好。

工程师们喜欢想象世界可以被设计,然而大自然却正相反。任何一个足够复杂的系统都不能由设计构建出来,它必须不断地进化

想象一些都在有条不紊地进行,然后别因为它工作地不好就惊慌。记住 Mosher 定律:程序有问题时不要担心。如果所有东西都没问题,你就失业了。

Ⅳ 扼杀在摇篮中

扼杀掉它,越快越好。不要因为把你努力了一个月的项目丢进垃圾桶。

当你从头开始,以往的经验会给予你创新的能力。

不断地评估你的工作。这就是精益创业这本书的主要内容,制作你的 MVP(最小化可行原型)然后验证它,如果它不合适,那就扼杀掉,你将会从中学到很多。

失败越快,学到越多。

Ⅴ 赋能

没有人知道所有事情。

尝试让你自己为你的团队赋能。你的团队最需要什么?哪一位是可扩展性方面的专家?哪一位最了解某种编程语言的细节?哪一位最聚焦于产品?哪一位是团队的协调者?

每一个团队都是不同的,所以你必须去接受并且选择,尝试分析出你自己以及团队优势和劣势。

学着爱上一件特别的事情并掌握好它。尝试像一个品牌一样,去定位你自己。你喜欢满足你需求的产品,而不是那些你已经自己做了很多事情的人。

和有同样爱好的人更容易相处是理所当然的事情,但是真正的力量在于那些能形成互补的人,想想 《A字特攻队》你就明白了。

Ⅵ 基础优先

在我们生命的晚期我们才会意识到基础的重要性。通常是一些老人声称他们已经回归基础。不要愚蠢到更不要那么晚才意识到这一点:首先注重基础知识。

在计算机科学中有很多非常有价值的概念,更好的是你可以将它们应用到每一个新的设计中。你应该总是尝试从他们开始自上而下地思考。

借用一句老话,软件开发中重要的三件事情是:架构、架构和架构。

这有一些基础的例子: Separation of concerns, Systems engineering theory, GOF design patterns, GRASP responsability patterns), SOLID principles), Cleancode smells & heuristics, Agile guidelines, Algorithmia, Data structures, HTTP specification...

在你开始挖掘如何使用特定的测试框架 Jasmine, JUint, Mocha 这些细节前,先开始学习如何编写可测试的代码(例如隔离应用程序的无状态代码并减少组件之间的耦合)。

Ⅶ 非同凡想

简单比复杂更困难,极简主义需要大量的创造力,才能找到用更少的钱去做更多的事情的新方法。

科技技能可以征服复杂,但是创造力才是简单的主人。

创造力是充满乐趣的智慧!所以享受这样的机会。

所以不要聚焦于工作得更多,而是如何聪明地降低工作量。多多考虑你的工作!关闭”自动驾驶“,离开你的舒适区并控制好。

以及,不要害羞。改变现状需要勇气。但是记住,你所生存地世界是由那些相信可能性的人创造的,而不是比你聪明的人创造的。

当然,有时候人们会觉得你是一个怪人,因为你在尝试不同的事情。但是如果你不是一个改革者,你就无法称为领导者。

举个例子,我非常确定我们很多人都觉得在公司的会议上浪费了太多的时间。因此,亚马逊的创始人兼首席执行官杰夫贝索斯(Jeff Bezos)在他的公司举办会议时,有一种特别的,显然是奇怪的方式。 当所有服务员到达会议室时,他们有30分钟的时间在严谨的沉默中阅读会议报告。 之后会议才可以开始。

Ⅷ 综合

我们必须为人类写代码而不是机器,所以综合就是沟通的关键。

一个很好的例子就是帕斯卡的名言:如果我有更多时间,我就会写一封更短的信。

尽量保持方法简单、类单一原则、更少的参数、避免多余的评论等待。当你没有东西可以删除时,你的工作就结束了。

“Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.” –Antoine de Saint-Exupery

译者注

Ⅸ 保持空白

在软件编程中,您可以通过添加另一层间接层来解决任何问题,除了间接太多的问题。这是事实,这意味着我们必须在设计的复杂性中找到平衡点。

抽象是很好的特性,它允许我们创建一个新的语义级别,在这个级别上可以绝对精确。然而,在某些情况下,比如重构,我们必须对全局有所理解,抽象就让这件事变得困难。

这就是为什么我们必须寻找一个可以进行划分的自然点,然后尝试理解不同的、可能的权衡之法。

一些例子:领域驱动设计中的根-叶范例, 控制器/工具范例, 高内聚/低耦合原理, 简答的 RESTful APIs...

Ⅹ清理 kipple

有一个名为 kipple 的概念来表示那些我们留存但是从未用过的东西。我们的房子里充满了这样的东西,我们的代码库也是如此。例如,那些我们害怕删除的遗留的方法、以防万一我们会再使用的库、过时的评论...

人类的本性是把事情复杂化,所以我们必须强化自己来简化。这是一个必须定期遵循的过程,识别哪些是 kipple 并销毁它。

极简主义者:这一切是为了避免分心,以便我们可以专注于重要的事情。

结语

由于我英语能力不及格(还有专业能力不够格...),所以很多翻译细节都是待商榷的。包括一些口语化的翻译。如果您对本文翻译有任何意见或建议,非常欢迎可以直接在 GitHub 上发起 PR 和 Issues。

以及有可能是我个人阅读能力差的问题,实际上有几条我觉得说得并不是十分严谨。我个人也持保留意见。但是集思广益,见贤思齐才是最好的学习态度,欢迎探讨。

原文载于:http://minifesto.org/,由于没有找到原文的许可,所以如果此文章有侵权嫌疑麻烦立刻通知我,我将会马上处理。

原文链接:https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md#basic-ui-coroutines

原文开源协议:https://github.com/Kotlin/kotlinx.coroutines/blob/master/LICENSE.txt

本指南假设您已经对协程这个概念有了基础的理解,如果您不了解,可以看看 Guide to kotlin.coroutines,它会给出一些协程在 UI 编程中应用的示例。

所有 UI 应用程序库都有一个普遍的问题:他们的 UI 均受限于一个主线程中,所有的 UI 更新操作都必须发生在这个特定的线程中。对于此类应用使用协程,这意味您必须有一个合适的协程调度器,将协程的执行操作限制在那个特定的 UI 线程中。

对于此,kotlin.coroutine有三个模块,他们为不同的 UI 应用程序库提供协程上下文。

kotlin-coroutines-core库里的Dispatcher.Main提供了可用的 UI 分发器实现,而ServiceLoader API 会自动发现并加载正确的实现(Android,JavaFx 或 Swing)。举个例子,如果您正在编写 JavaFx 应用程序,您可以使用Dispatcher.MainDispatcher.JavaFx扩展,他们是同一个对象。

本指南同时涵盖了所有的 UI 库,因为每个模块只包含一个长度为几页的对象定义。您可以使用其中任何一个作为示例,为您喜欢的 UI 库编写相应的上下文对象,即便它未被本文写出来。

目录

  • 设置

  • 基础 UI 协程

    • 启动 UI 协程
    • 取消 UI 协程
  • 在 UI Context 中使用 actors

    • 扩展协程
    • 最多仅有一个协程 job
    • 事件合并
  • 阻塞操作

    • UI 卡顿问题
    • 结构化并发、生命周期和协程亲子继承
    • 阻塞操作
  • 进阶提示

    • 不使用分发器在 UI 事件控制器中启动协程

设置

本指南中可运行的例子将使用 JavaFx 实现。这么做的好处是,所有的示例可以直接在任何操作需要上运行而不需要安装任何模拟器或类似的东西,并且他们是完全独立的。

JavaFx

这个基础的 JavaFx 示例程序由一个名为hello并使用Hello World!进行初始化的文本标签以及一个名为fab的桃红色的位于右下角的原型按钮组成。

ui-example-javafx

JavaFx 的 start函数将会调用setup函数,并将hellofab这两个节点的引用作为参数传递给 setup 函数。setup 函数是本指南中存放各种代码的地方:

fun setup(hello:Text, fab: Circle) {
    // 占个位
}

点击此处查看完整代码

您可以从 GitHub clone kotlinx.coroutines 项目到您本地,然后用 IDEA 打开。本指南的所有例子都在 ui/kotlinx-coroutines-javafx 模块的 test文件夹中。这样您便可以运行并观察每一个例子的运行情况以及修改项目来进行实验。

Android

跟着 Getting Started With Android and Kotlin 这份指南,在 Android Studio 中创建 Kotlin 项目。我们也推荐您使用 Kotlin Android Extensions 中的扩展特性。

在 Android Studio 2.3 中,您会得到下面的类似的应用程序界面:

ui-example-android

context_main.xml文件中,为您的TextView分配hello的资源 ID,然后使用Hello World!来初始化它。

那个桃红色的浮动按钮资源 ID 是fab

MainActivity.kt中,移除掉fab.setOnclickListener{...},接着在onCreate()方法的最后一行添加一行setup(hello, fab)来调用它。

然后在MainActivity.kt文件的尾部,给出setup()函数的实现:

fun setup(text: TextView, fab: FloatingActionButton){
    // 占位
}

在您app/build.gradledependecies{...}块中添加依赖:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0"

Android 的示例存放在 ui/kotlinx-coroutines-android/example-app ,您可以clone下来运行。

基础 UI 协程

这个小节将展示协程在 UI 应用程序中的基础使用。

启动 UI 协程

kotlinx-coroutines-javafx 模块包含了Dispatchers.JavaFx 分发器,该分发器分配协程操作给 JavaFx 应用线程。

我们将之导入并用Main作为其别名,以便所有示例都可以轻松地移植到 Android 上:

import kotlinx.coroutines.javafx.JavaFx as Main

主 UI 线程的协程可以在 UI 线程上执行任何更新 UI 的操作,并且可以不阻塞主线程地挂起(suspend)操作。举个例子,我们可以编写命令式代码(imperative style)来执行动画。下面的代码将使用 launch 协程构造器,从 10 到 1 进行倒数,每隔 2 秒倒数一次并更新文本。

fun setup(hello: Text, fab: Circle) {
    GlobalScope.launch(Dispatchers.Main) { // launch coroutine in the main thread
        for (i in 10 downTo 1) { // countdown from 10 to 1 
            hello.text = "Countdown $i ..." // update text
            delay(500) // wait half a second
        }
        hello.text = "Done!"
    }
}

您可以在此获取完整的代码

那么,上面究竟发生了什么呢?因为我们在 UI 线程启动(launching)了协程,所以我们可以在该协程内自由地更新 UI 的同时还可以调用挂起函数(suspend functions),比如 delay 。当 delay 在等待时(waits),UI 并不会卡住(frozen),因为 delay 并不会阻塞 UI 线程 —— 这就是协程的挂起。

相应的 Android 应用代码是一样的。您只需要复制setup函数内的代码到 Android 项目中的对应函数中即可

取消 UI 协程

当我们想要停止一个协程的时候,我们可以持有一个由 launch函数返回的 Job 对象并利用它来取消(cancel)。

让我们通过点击桃红色的按钮来停止协程:

fun setup(hello: Text, fab: Circle) {
    val job = GlobalScope.launch(Dispatchers.Main) { // launch coroutine in the main thread
        for (i in 10 downTo 1) { // countdown from 10 to 1 
            hello.text = "Countdown $i ..." // update text
            delay(500) // wait half a second
        }
        hello.text = "Done!"
    }
    fab.onMouseClicked = EventHandler { job.cancel() } // cancel coroutine on click
}

您可以在这里获取完整代码

现在实现的效果是:当倒数正在进行时,点击圆形按钮将会停止倒数。请注意,Job.cancel 方法线程安全并且非阻塞。它只是给协程发送取消信号,而不会等待协程真正终止。

Job.cancel 该方法可以在任何地方调用,而如果在已经取消或者完成的协程上,该方法不做什么事情。

相应的 Android 代码示例如下

fab.setOnClickListener{job.cancel()}

在 UI Context 中使用 actors

在一节中,我们将会展示 UI 应用程序是如何在其 UI 上下文(Context)中使用 actors ,以确保启动的协程数量不会无限增长。

协程扩展

我们的目标是编写一个名为onClick的扩展协程构建器函数,这样每当圆形按钮被点击的时候,都会执行一个倒数动画:

fun setup(hello: Text, fab: Circle) {
    fab.onClick { // start coroutine when the circle is clicked
        for (i in 10 downTo 1) { // countdown from 10 to 1 
            hello.text = "Countdown $i ..." // update text
            delay(500) // wait half a second
        }
        hello.text = "Done!"
    }
}

我们的第一个onClick版本:在每一个鼠标事件上启动一个新的协程,并将之对应的鼠标事件传递给动作使用者:

fun Node.onClick(action: suspend (MouseEvent) -> Unit) {
    onMouseClicked = EventHandler { event ->
        GlobalScope.launch(Dispatchers.Main) { 
            action(event)
        }
    }
}

您可以在此获取完整的代码

请注意,每当圆形按钮被点击,它便会启动一个新的协程,这些新协程会竞争地更新文本。这看起来并不好,我们会在后面解决这个问题。

在 Android 中,可以为 View 类编写对应的扩展函数代码,所以上面 setup 函数中的代码可以不需要另作更改就直接使用。Android 中没有 MouseEvent,所以此处略过

fun View.onClick(action: suspend () -> Unit) {
    setOnClickListener { 
        GlobalScope.launch(Dispatchers.Main) {
            action()
        }
    }
}

最多只有一个协程 Job

我们可以在开启一个新的协程之前,取消掉一个正在运行(active)的 Job,以此来确保最多只有一个协程在执行倒计时工作。然而,通常来说这并不是一个最好的解决方法。cancel 函数仅仅发送一个取消信号去中断一个协程。取消的操作是合作性的,在现在的版本中,当协程在做一件不可取消的或类似的事件时,它是可以忽略取消信号的。

一个更好的解决方法是使用一个 actor 来确保协程不会同时进行。让我们修改onClick扩展实现:

fun Node.onClick(action: suspend (MouseEvent) -> Unit) {
    // 启动一个 actor 来接管这个节点中的所有事件
    val eventActor = GlobalScope.actor<MouseEvent>(Dispatchers.Main) {
        for (event in channel) action(event) //传递事件给 action
    }
    // install a listener to offer events to this actor
    onMouseClicked = EventHandler { event ->
        eventActor.offer(event)
    }
}

您可以在此获取完整代码

整合 actor 协程和常规事件控制(event handler)的关键点,在于 SendChannel 中有一个不中断(no wait)的 offer 函数。如果发送消息这个行为可行的话,offer 函数会立即发送一个元素给 actor ,否则该元素将会被丢弃。offer 函数会返回一个 Boolean 作为结果,不过在此该结果被我们忽略了。

试着重复点击这个版本的代码中的圆形按钮。当倒数都动画正在执行时,该点击操作会被忽略掉。这是因为 actor 正忙于动画而没有从 channel 接受消息。默认情况下,一个 actor 的消息信箱(mailbox)是由 RendezvousChannel实现的,后者的 offer操作仅在 receive 活跃时有效。

在 Android 中,View 被传递给 OnClickListener,所以我们把 view 当作信号(signal)传递给 actor 。对应的 View 类扩展如下:

fun View.onClick(action: suspend (View) -> Unit) {
    // launch one actor
    val eventActor = GlobalScope.actor<View>(Dispatchers.Main) {
        for (event in channel) action(event)
    }
    // install a listener to activate this actor
    setOnClickListener { 
        eventActor.offer(it)
    }
}

事件合并

有时候处理最新的事件比忽略掉它更合适。 actor 协程构建器接受一个可选的 capacity 参数来控制用于消息信箱(mailbox)的 channel 的实现。所有有效的选项均在 Channel() 工厂函数中有所阐述。

让我们修改代码,传递 Channel.CONFLATED 这个 capacity 参数来使用 ConflatedChannel 。只需要更改创建 actor 的那行代码即可:

fun Node.onClick(action: suspend (MouseEvent) -> Unit) {
    // launch one actor to handle all events on this node
    val eventActor = GlobalScope.actor<MouseEvent>(Dispatchers.Main, capacity = Channel.CONFLATED) { // <--- Changed here
        for (event in channel) action(event) // pass event to action
    }
    // install a listener to offer events to this actor
    onMouseClicked = EventHandler { event ->
        eventActor.offer(event)
    }
}

您可以在此获取完整的 JavaFx 代码。在 Android 上,您需要修改之前示例中的 val eventActor = ... 这一行。

现在,如果动画正在进行时圆形按钮被点击了,动画将会在结束之后重新启动。仅会重启一次。当动画进行时,重复的点击操作将会被合并,而仅有最新的事件会被处理。

这对于那些需要接收高频率事件流,并基于最新事件更新 UI 的 UI 应用程序而言,也是一种合乎需求的行为( a desired behaviour )。使用 ConflatedChannel 的协程可以避免由事件缓冲(buffering of events)带来的延迟。

您可以试验不同的 capacity 参数来看看上面代码的效果和行为。设置 capacity = Channel.UNLIMITED 将创建一个 LinkedListChannel 实现的信箱,这会缓冲所有事件。在这种情况下,动画的执行次数和圆形按钮点击次数保持一致。

阻塞操作

这一小节将解释如何在 UI 协程中完成线程阻塞操作(thread-blocking operations)。

UI 卡顿问题

The problem of UI freezes

如果所有 API 接口函数均以挂起函数(suspending functions)来实现那是最好不过的事情了,这样那些函数将永远不会阻塞调用它们的线程。然而,事实往往并非如此。比如,有时候您必须做一些消耗 CPU 的计算操作,或者只是需要调用第三方的 API 来访问网络,这些行为都会阻塞调用函数的线程。您无法在 UI 线程或是 UI 线程启动的协程直接做上述操作,因为那会直接阻塞 UI 线程从而导致 UI 界面卡顿。

下面的例子将会展示这个问题。我们将使用 onClick 扩展和上一节中的 UI 限制性事件合并 actor 来处理 UI 线程的最后一次点击。

举个例子,我们将进行 斐波那契数列 的简单演算:

fun fib(x: Int): Int = 
    if (x <= 1) x else fib(x - 1) + fib(x - 2)

每当圆形按钮被点击,我们都会进行更大的斐波那契数的计算。为了让 UI 卡顿变得明显可见,将会有一个持续执行的快速的计数器动画,并在 UI 分发器(dispatcher)更新文本:

fun setup(hello: Text, fab: Circle) {
    var result = "none" // the last result
    // counting animation 
    GlobalScope.launch(Dispatchers.Main) {
        var counter = 0
        while (true) {
            hello.text = "${++counter}: $result"
            delay(100) // update the text every 100ms
        }
    }
    // compute the next fibonacci number of each click
    var x = 1
    fab.onClick {
        result = "fib($x) = ${fib(x)}"
        x++
    }
}

您可以在这里获取完整的 JavaFx 代码。您只需要复制 fib 函数及 setup 函数体内代码到您的 Android 项目即可

试着点击例子中的圆形按钮。大概第在 30~40 次点击后,我们的计算将会变得缓慢,接着您会立刻看到 UI 卡顿,因为倒数动画在 UI 卡顿的时候停止了。

结构化并发、生命周期和协程亲子继承

一个典型的 UI 应用程序拥有许多生命周期元素。Windows、UI 控制、activities,views,fragments 以及其他可视化元素将会被创建和销毁。一个长时间运行的协程,在后台执行着诸如 IO 或计算操作,如果它持有 UI 元素的引用,那么可能导致 UI 元素生命周期过长,继而阻止那些已经销毁并且不再显示的 UI 树被 GC 收集和回收。

一个自然的解决方法是将一个 Job 对象关联到 UI 对象,后者拥有生命周期并在其上下文(Context)中创建协程。但是传递已关联的 Job 对象给所有线程构造器容易出错,而且这个操作容易被遗忘。故此,CoroutineScope 接口可以被 UI 所有者所实现,然后每一个在 CoroutineScope 上定义为扩展的协程构造器都将继承 UI 的 Job,而无需显式声明。为了简单起见,可以使用 MainScope() 工厂方法。它会自动提供 Dispatchers.Main 及其父级 job 。

举个例子,在 Android 应用程序中,一个 Activitycreated 中被初始化,而当其不再被需要或者其内存必须被释放时,该对象被销毁destroyed)。一个自然的解决方法是为一个 Activity 实例对象附加一个 Job 实例对象:

abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onDestroy() {
        super.onDestroy()
        cancel() // CoroutineScope.cancel
    } 
}

现在,继承 ScopedAppActivity 来让一个 activity 和一个 job 关联起来:

class MainActivity : ScopedAppActivity() {

    fun asyncShowData() = launch { // Is invoked in UI context with Activity's job as a parent
        // actual implementation
    }
    
    suspend fun showIOData() {
        val deferred = async(Dispatchers.IO) {
            // impl      
        }
        withContext(Dispatchers.Main) {
          val data = deferred.await()
          // Show data in UI
        }
    }
}

每个从MainActivity中启动(launched)的协程都将拥有它的 job 作为其父亲,当 activity 被销毁时,协程将会被立刻取消(canceled)。

可以使用多种方法,来将 activtiy 的 scope 传递给它的 Views 及 Presenters:

class ActivityWithPresenters: ScopedAppActivity() {
    fun init() {
        val presenter = Presenter()
        val presenter2 = ScopedPresenter(this)
    }
}

class Presenter {
    suspend fun loadData() = coroutineScope {
        // Nested scope of outer activity
    }
    
    suspend fun loadData(uiScope: CoroutineScope) = uiScope.launch {
      // Invoked in the uiScope
    }
}

class ScopedPresenter(scope: CoroutineScope): CoroutineScope by scope {
    fun loadData() = launch { // Extension on ActivityWithPresenters's scope
    }
}

suspend fun CoroutineScope.launchInIO() = launch(Dispatchers.IO) {
   // Launched in the scope of the caller, but with IO dispatcher
}

jobs 间的亲子关系形成了层级结构。一个代表视图在后台执行工作的协程,可以进一步创建子协程。当父级 job 被取消的时候,整个协程树都将被取消。协程指南中的“子协程”用一个例子阐述了这些用法。

阻塞操作

使用协程可以非常简单地解决 UI 线程上的阻塞操作。我们将把我们的“阻塞” fib 函数转换为挂起函数,然后通过使用 withContext 函数来将把后台运算部分的线程的执行上下文(execution context)转换为 Dispatchers.DefaultDispatchers.Default 由一个后台线程池( background pool)实现。请注意,fib函数现在标有 suspend 修饰符。这表示无论它怎么被调用都会不会阻塞协程,而是在后台线程执行计算时,挂起它的操作。

suspend fun fib(x: Int): Int = withContext(Dispatchers.Default) {
    if (x <= 1) x else fib(x - 1) + fib(x - 2)
}

您可以在这里 获取完整代码。

您可以运行上述代码然后确认在计算较大的斐波那契数时 UI 不会被卡住。然而,这段 fib计算代码速度稍慢,因为每一次都是通过 withContext 来递归调用的。这在练习中并不是什么大问题,因为 withContext 能够机智地检查该协程是否已经在所需的上下文中,然后避免过度分发(dispatching)协程到不同的线程。尽管如此,这仍是一种开销。它在原生代码(primitive code)上是可见的,并且它除了调用 withContext 之间提供整数以外,不做其他工作。对于一些实际性的代码, withContext 的开销不会很明显。

尽管如此,这个特定实现的可在后台线程工作的 fib 函数也可以变得和没有使用挂起函数时一样快,只需要重命名原来的 fib 函数为 fibBlocking 然后定义一个用 withContext 包装在 fibBlocking 顶部的 fib 函数即可:

suspend fun fib(x: Int): Int = withContext(Dispatchers.Default) {
    fibBlocking(x)
}

fun fibBlocking(x: Int): Int = 
    if (x <= 1) x else fibBlocking(x - 1) + fibBlocking(x - 2)

您可以在这里 获取完整代码。

您现在可以享受全速(full-speed)的原生斐波那契数计算而不会阻塞 UI 线程了。我们仅仅需要 withContext(Dispatchers.Default) 而已。

请记住,因为在我们代码中 fib 函数是被单个 actor 所调用的,故而在任何时间都最多只有一个并行运算。所以这份代码在资源利用上有着天然的限制性。它最多只能占用一个 CPU 核心。

进阶提示

这个小结覆盖了多种进阶提示。

不使用分发器在 UI 事件控制器中启动协程

让我们用下列 setup 函数中的代码来形象展示协程从 UI 中启动的执行步骤:

fun setup(hello: Text, fab: Circle) {
    fab.onMouseClicked = EventHandler {
        println("Before launch")
        GlobalScope.launch(Dispatchers.Main) {
            println("Inside coroutine")
            delay(100)
            println("After delay")
        } 
        println("After launch")
    }
}

您可以在这里获取完整的 JavaFx 代码。

当我们运行代码并点击桃红色的圆形按钮,控制台将会打印出如下信息:

Before launch
After launch
Inside coroutine
After delay

正如您所见,launch 后的操作被立刻执行了,而发布到 UI 线程的协程则在其之后才执行。所有在 kotlinx.coroutines 的分发器都是如此实现的。为什么要这样呢?

基本上,这是在 “JavaScript 风格”异步方法(异步操作总是被延迟给事件分发线程执行)和 “C# 风格”异步方法(异步操作在调用者线程遇到第一个挂起函数时被执行)之间的选择。尽管 C# 风格看起来更有效率,但是它最终建议诸如“如果您需要时请使用 yield ...”的信息。这样是容易出错的。JavaScript 风格的方法更加一致,它也不要求编程人员去思考什么时候该或不该使用 yield

然而,当协程从事件控制器(event handler)启动,并且没有其周围没有其它的代码,这中特殊情况下,此种额外的分派确实会带来额外的开销,并且没有其他的附加价值。在这样的情况下, launchasyncactor 三种协程构造器均可以传递一个可选的 CoroutineStart 参数来优化性能。传递 CoroutineStart.UNDISPATCHED 参数将会实现:遇到首个挂在函数便立刻执行协程的效果。正如下面代码所示:

fun setup(hello: Text, fab: Circle) {
    fab.onMouseClicked = EventHandler {
        println("Before launch")
        GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { // <--- Notice this change
            println("Inside coroutine")
            delay(100)                            // <--- And this is where coroutine suspends      
            println("After delay")
        }
        println("After launch")
    }
}

您可以在此获取到完整的 JavaFx 代码。

当点击时,下面的信息将会被打印出来,可以确认协程中的代码被立刻执行:

Before launch
Inside coroutine
After launch
After delay