【问题标题】:Does Kotlin suspend funtion runs on a separate thread?Kotlin 暂停功能是否在单独的线程上运行?
【发布时间】:2018-11-30 07:12:52
【问题描述】:

挂起函数在单​​独的线程上运行? 如果不是,那么性能优势是什么?

suspend fun requestToken():Token {..}  // takes 2 sec to complete
suspend fun createPost (token:Token){..} // takes 3 sec to complete

 suspend fun postItem() {
    val token = requestToken()
    val post =createPost(token)
    processPost(post)
  }

所以,当我们到达 processPost(post) 并且如果挂起函数没有在单独的线程上运行,那么我们必须等待 requestToken()createPost(token) 方法 完成(即 2+3= 5 秒)。按照作者的说法,挂起是异步的,但如果我们不产生任何新线程,那么我们如何实现异步行为?

【问题讨论】:

标签: kotlin coroutine kotlinx.coroutines


【解决方案1】:

挂起是异步的

suspend funs 与其调用者同步执行。您实际上要说的是“非阻塞”,这是一个完全不同的故事。

但如果我们不产生任何新线程,那么我们如何实现异步行为?

您默认所有 I/O 必须在某个级别阻塞。这是错误的。非阻塞 I/O 通过将数据推送到发送缓冲区并在接收缓冲区中有数据时接收通知来工作。 suspend fun 通过在将数据推送到发送缓冲区后挂起自身并安装一个回调,该回调将在接收缓冲区中的响应数据准备好时恢复它,从而与此机制挂钩。

【讨论】:

  • 如果我想在 requestToken() 中对 UI 线程进行网络调用,那么我必须创建一个单独的工作线程还是这个挂起函数会自动执行它在某些后台线程和控制将移动到下一行** createPost(token)** 只有在获得结果/令牌后。谢谢
  • 我只能重复我在答案中所说的:requestToken(),作为一个可挂起的函数,不会阻塞它被调用的线程,也不会阻塞任何其他后台线程。您可以简单地在 UI 线程上调用它。但是,由于requestToken() 只是您在示例代码中看到的内容,因此您应该关注的关键问题是如何实现自己的挂起函数。为此,您可以阅读官方文档的this section
【解决方案2】:

暂停点只能在协程上下文中使用,例如:

fun main() {
    delay(1000)
}

不会工作,因为delay 是一个挂起函数,编译器不知道如何在没有协程的情况下处理它。当您确实有协程时,它可以使用称为调度程序的东西来控制线程所有权。挂起意味着线程不再用于执行程序的该部分,而是执行其他操作或空闲。它的工作方式是您可以让多个协程同时工作,而每个协程都没有线程,然后该线程可以执行每个协程的一部分,直到暂停点。通常的想法是,您可以将挂起函数视为具有产生结果的阶段的“生成器”。

suspend fun hello() {
    println("Hello")
    delay(1000) // suspend here, let someone else use the thread while we wait
    println("World")
    delay(1000) // suspend again, we can still use the thread while waiting
    println("done")
}

每次挂起时,它都会使用线程处理另一个函数,直到该挂起并且延迟到期,此时此函数将最终恢复执行直到下一个挂起点或完全完成执行.

这与阻塞代码不同,因为它不会通过将线程置于 wait 状态来浪费线程,而是将其借给另一个函数。这样就不需要创建其他线程来实现并发,因为您仍然可以在没有真正并行性的情况下处理多个函数,而是使用部分函数的并发执行。

协程不一定能保护你免受阻塞,如果你调用Thread.sleep(1000),它仍然会是一个阻塞调用。程序员有责任对阻塞函数使用挂起等效项,以最大限度地提高这一概念的有效性。

如需了解更多详情,请阅读documentation,因为它非常详细且很有帮助。

【讨论】:

  • 大多数情况下协程调度器不做任何调度。它只确保协程被分派到其关联的执行模块(例如,GUI 事件循环或 ExecutorService)。然后执行模块决定如何调度它。
  • 调度意味着计划任务的执行,这正是所谓的调度员的工作。它计划在何时何地执行任务,而 ExecutorService 只是用于执行任务的资源。调度器是决定如何使用资源的那个,就像调度器一样。
  • 当然,您可以随心所欲地查看它,但它实际上被称为 dispatcher,并且名称适合的原因如上所述。在大多数情况下,协程调度器只是调度而不是调度,这是因为协程主要用于与现有的事件循环或其他类型的任务调度器集成。
【解决方案3】:

一个线程池的后台单线程或多后台线程可以显式声明然后使用,例如作为参数传递,我们称这个参数为“调度器”。关于它的真正酷的事情是,最初从主线程开始,它会自动切换到调度程序线程以在其上执行特定任务,虚拟机在这个地方暂停或中断,而主线程变得更酷畅通无阻,可以在后台有任务时执行其他操作。

一旦任务完成,虚拟机就会回到之前暂停或中断的点,然后从该点恢复执行,但现在也从后台线程返回结果调度器,下面是sn-p的代码:

private val backgroundThread = ThreadPoolExecutor(1, 1, 15L, TimeUnit.SECONDS, LinkedBlockingQueue())

GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT) {
    postItem(backgroundThread))
}

suspend fun CoroutineScope.postItem(scheduler: ThreadPoolExecutor): Boolean {
    val token = requestToken(scheduler)
    val post = createPost(token, scheduler)
    return processPost(post, scheduler)
}

private suspend fun CoroutineScope.requestToken(scheduler: ThreadPoolExecutor): String {
    val def: Deferred<String?> = async(scheduler.asCoroutineDispatcher(), CoroutineStart.DEFAULT) {
        val token = networkApi.requestToken()
    }

    return def.await() ?: ""
}

private suspend fun CoroutineScope.createPost(token: String, scheduler: ThreadPoolExecutor): String {
    val def: Deferred<String?> = async(scheduler.asCoroutineDispatcher(), CoroutineStart.DEFAULT) {
        val post = networkApi.createPost(token)
    }

    return def.await() ?: ""
}

private suspend fun CoroutineScope.processPost(post: String, scheduler: ThreadPoolExecutor): Boolean {
    val def: Deferred<Boolean?> = async(scheduler.asCoroutineDispatcher(), CoroutineStart.DEFAULT) {
        val result = networkApi.processPost(post)
    }

    return def.await() ?: false
}

【讨论】:

  • virtual machine kind of suspended or interrupted at this place--- 没有这种魔法,它只是简单的字节码。 suspend fun 编译成一个在协程挂起时返回的方法。它返回一个特殊值,调用代码可以识别并知道它不是真正的返回值。
  • 您确实提到了“虚拟机”,但它与虚拟机级别无关,它只是简单的Java字节码。
  • 现在您注意到您的代码,我发现它既错误又具有误导性: 1. 在没有并发的情况下使用async-await 的反成语。 2. 将已经是suspend fun 的内容包装到一个毫无意义的附加层中,只得到另一个suspend fun。 3. 每次调用时创建一个Dispatcher。调度程序应该是*属性而不是原始执行程序。 4. 甚至不编译。
  • 我猜你不懂协程已经很清楚了。当网络操作正在进行时,协程没有在任何线程上执行。操作完成后,协程在调度程序确定的线程上恢复。在您的情况下,它将在 backgroundThread 上恢复,除了执行更多线程协调代码以将该结果传播回调用协程,然后它将在原始线程上恢复。
  • 花时间学习不是浪费时间。现在,既然这个网站都是关于教别人的,请你修正一下你的答案,以免误导读者吗?
最近更新 更多