如何理解 Kotlin 协程?

9

我曾尝试阅读有关Kotlin协程的各种教程和页面,虽然我有点理解,但我仍然感觉没有真正掌握,也不觉得自己已经准备好使用协程编写异步非阻塞代码。我认为我需要的是一个图表或图片,来显示协程代码执行时确切发生的事情以及顺序。这段代码在线程级别上是如何运行的?

    launch {
        delay(1000)
        println("World (${currentThread().name})")
    }
    println("Hello (${currentThread().name})")
    sleep(1500)

我的理解是这样的。如果有更好的例子可以纠正我或者继续加深我的理解。

第0行:代码在主线程上启动

第1行:在一个新的线程上启动了一个新的协程(我想是从 forkjoin 池中)

第2行:挂起函数,协程暂停并将线程返回给线程池(因此是非阻塞状态)

第5行:在主线程上打印

第6行:阻塞主线程1.5秒

第3行:协程在某个线程上恢复执行(不确定是之前的挂起线程还是可能是另一个线程)。协程在该线程上打印并完成,然后再次将线程返回到线程池。

另一个问题是,如果我将整个代码用 runBlocking { ... } 包装起来,底层执行会如何改变?


为了更好地理解背后的内容,我建议观看这个InfoQ演示文稿。它介绍了Oracle正在进行的工作,旨在在Java中提供continuations/fibers。从概念上讲,它与Kotlin协程非常相似。 - yegodm
将“启动”视为Java中传统的线程创建,也就是说,它可以独立于创建它的主线程工作。 “runBlocking”表示协程及其创建的所有协程必须先结束。 应用于主程序时,这意味着程序不会结束,除非所有生成的协程都已完成(而不像守护进程那样)。 - Neil
@Neil runBlocking 干的不仅仅是这些,它为协程运行创建了一个环境。一旦你已经在协程环境中,它甚至不应该被使用。它不会在其协程完成之前结束只是这个函数的副作用。 - Marko Topolnik
@MarkoTopolnik 我同意,但在一个单独的评论中解释所有这些内容有点过多。我想要区分Java类行为和Kotlin中的新协程,因为他可能来自Java背景。 - Neil
@Neil 我明白你的意思。我仍然要指出,launch通常不能独立于创建它的主线程工作(在Android上的基本惯用法是让它在同一线程上工作),这也几乎正是使协程与线程如此不同的原因。你必须跳过这种认知上的障碍,接受现在在单个线程上存在并发。这是真正理解协程的关键。 - Marko Topolnik
建议学习Kotlin基础知识,其中详细解释了此内容。 - Pragnesh Ghoda シ
2个回答

6

你的代码实际上没有展示协程特殊性质的任何行为。它只是像在普通Java中一样让两个线程同时执行各自的任务。

当你在已有线程(例如主线程)上启动一个协程时,才会变得更加有趣。这可以通过使用 runBlocking 块来实现之一:

runBlocking {
    launch {
        delay(1000)
        println("Hello from the launched coroutine. My thread is "
                + Thread.currentThread().name)
    }
    println("Hello from the top-level coroutine. My thread is "
            + Thread.currentThread().name)
}

这将会打印出来

Hello from the top-level coroutine. My thread is main
Hello from the launched coroutine. My thread is main

runBlocking 在调用线程上运行事件循环,并将对其的引用传播到您在其中启动的所有协程。例如,delay(1000) 将向此事件循环发布一个带有一秒延迟指示的事件,然后它将挂起协程。这将允许主线程运行下面 launch 的其余代码。当时间过去时,事件循环将运行事件处理程序,它将反过来恢复协程。


更为教育性的例子是启动没有任何 Dispatcher 的协程。这消除了协程看起来像线程的幻觉,揭示了它们真正的魔力:通过调用 continuation.resume(),您可以使当前线程跳转到挂起的协程代码块的中间位置,所有这些都使用普通的 Java 方法调用。建议详细研究这个答案,其中详细解释了这一点。如果您对纯Java方法如何完成此技巧的更深入说明感兴趣,建议观看Roman Elizarov在YouTube上的解释视频


1

这里有另一个例子:https://kotlinlang.org/docs/reference/coroutines/basics.html#your-first-coroutine

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

以上代码输出:

Hello,
World!

正如您所看到的,虽然代码看起来是同步的,但执行是异步的。这就是 Coroutine 的整个理念。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接