为什么可以在主线程上运行Kotlin协程?

10

我不太理解 为什么 这段代码 可以正常工作:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    launch(Dispatchers.Main) {
        log("A")
    }

    log("B")
}

该代码应首先输出B,然后是A

这是否有效,因为主线程已经由协程控制?还是协程API在某种魔法般地将代码注入到主线程中?


我一直以为Dispatchers是其他东西,但是通过阅读文档(通过IDE),它明确表示它将在主线程上运行,其中UI操作可用,这非常有趣。 - cutiko
3个回答

16
在Android(以及其他UI框架)中,UI /主线程运行所谓的event loop。这意味着它等待任务被安排到队列中,并按顺序执行它们。例如,当您单击按钮时,内部将调度onClick操作在主线程上运行。但是用户也可以手动安排自己的任务,例如使用runOnUiThread()getMainLooper()Dispatchers.Main只是在主线程上安排某些内容的另一种方式。这并不意味着协程完全控制主线程或以某种方式神奇地注入任何内容。主线程是合作的,它允许安排任务,而协程只是利用了这个功能。
此外,您在评论中询问了如何在同一线程上并行运行两个日志语句的可能性。它们不是并行运行的。onCreate()只安排将log("A")稍后执行的任务添加到队列中。然后调用log("B"),只有当onCreate()完成时,主线程才能开始执行log("A")块。因此,这实际上是按顺序进行的,但不是从上到下的顺序。

所以这是一个框架的问题。如果我要编写一个简单的JVM可执行文件,例如 fun main() { /* stuff */ },那么在“stuff”内部,如果不使用 runBlocking 阻塞整个线程,就没有办法在主线程上运行协程了? - Snackoverflow
1
我不确定我是否正确理解了你的问题,但是是的,我相信runBlocking()是使用协程现有线程的唯一方法。我们不能“强制”任何线程做我们需要的事情,我们必须通过运行事件循环来完全占用它。在内部,runBlocking()执行我所描述的操作。它创建一个任务队列,并循环遍历该队列。从外部观察者的角度来看,线程完全被阻塞,从内部观察者的角度来看,它允许在其上安排任务并运行它们。 - broot
1
此外,我认为有一个普遍的误解,即什么是“主”线程。这不是一种神奇的线程,只能在应用程序内运行一个线程。主线程只是 UI 框架的主线程,它旋转其主事件循环的线程(它可能有更多)。如果由于任何原因您在单个应用程序中使用了 2 个 UI 框架,则每个框架都将拥有自己的主线程。而且,它也不必是启动 main() 函数的相同主线程。 - broot
我相信runBlocking()是使用现有线程进行协程的唯一方法,或者如果该线程已经运行某种事件循环或允许在其中安排任务,则可以使用此功能并在其中安排协程。例如,我们可以在Java Executor或Android主循环程序上安排协程。但是,我们不能在任何线程上安排协程,它必须合作。 - broot

9

协程是 Kotlin 的一个概念。你并不会将任务发送到“主线程”,而是发送到了“Main 协程调度程序”(虽然在幕后,你确实是从主线程调用该代码)。

Kotlin 只是一种编程语言,它稍后会被编译成不同的其他语言(Java 和 Android 的 JVM 字节码,本机目标的 LLVM 以及浏览器或 NodeJS 目标的 JS)。

无论目标是什么,概念都保持不变:Kotlin 协程。

例如,在 JavaScript 世界中,我们没有线程:我们只有同步和异步任务堆栈。如果编译到像 Arduino 或 ESP32 这样的嵌入式设备上,情况也是如此。

对于 Android 协程实现,它使用了 ExecutorsHandlersLooper API。它并不是“神奇地注入到了主线程”。但你所提供的代码最终会执行以下操作:

Handler(Looper.getMainLooper()).post({/*your lambda here*/})

因此,在 Android 上,Main 协程调度程序最终会将任务(您的 lambda)分派(传递)到 Android 上的 Main Looper

如果该代码在浏览器(JavaScript)上运行,低级实现将完全不同。 我想它可能是这样的(但我们必须在 GitHub 上检查源代码):

new Promise((res) => res(yourLambda()))

但协程的概念保持不变:我们将任务传递给Main协程调度器。

最后,回答你的问题:

为什么可以在主线程上运行Kotlin协程?

有些代码(或任务)是可以安全地运行(或必须在)主线程上的,例如操作View的属性。为了实现这一点,我们将这些任务委派给Main协程调度器,该调度器最终会在主线程上运行您的代码。

这段代码可以在主线程上运行;然而,它被标记为挂起代码。同样,在记录日志时也会发生类似的情况。它们可以在挂起上下文中或在其外部运行。

suspend fun makeGone(view: View) {
  view.visibility = View.GONE
}

suspend 标记函数并不是真正的关键,它只要求你在 CoroutineContextCoroutineScope 内运行它。

不过需要注意的是:如果你在主线程中运行 IO 代码,Android 将会崩溃:

override fun onCreate() {
  launch(Dispatcher.Main) { somethingThatDoesIO() }
}

即使somethingThatDoesIO未标记为suspend,此代码也会崩溃,因为它在主线程上执行IO操作。

如果我们将somethingThatDoesIO重新实现如下:

suspend fun somethingThatDoesIO() = withContext(Dispatchers.IO) {
  // old code
}

以下是翻译的结果:

将要发生的事情是:

  1. 您向主协程调度程序派发任务
  2. 主协程调度程序将执行lambda表达式
  3. 它会意识到somethingThatDoesIO需要由IO调度程序运行,并“交出一切所需的”(我知道这非常高级)
  4. Dispatchers.IO完成任务后,将控制权交给主调度程序,后者将继续执行lambda中的代码。

我不确定我是否帮助了您或使事情更加混乱。请在评论中让我知道。


谢谢!这确实有帮助,如果你结合下面broot的答案中提到的事实,即Android的“Main”线程实际上是一个事件循环,你可以在其中安排任务。因此,它不完全是传统意义上的主线程(运行可执行文件的线程),而是用于主要任务的任务执行器线程,这些任务可能由实现中隐藏的实际主线程安排。对吗? - Snackoverflow
没错! :) - Some random IT boy

-2
有时候你需要从异步源获取数据。而且你不能没有这些数据继续进行,比如从Room数据库或在线服务器获取已登录用户信息,在这种情况下,通常会阻塞主线程以便获取所需数据,然后再进一步进行。

OP并不是在问为什么需要使用协程,而是在问为什么协程可以在主线程上运行。 - cutiko
@cutiko 这就是我之前回答的内容... - Ghulam Rasool

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