我不确定我的suspend
心理模型是否正确。从我收集的信息来看,这似乎意味着一个(长时间运行的)suspend
函数可以被挂起,如果它内部的另一个函数被标记为suspend
(这会为父函数生成一个挂起点)。
为了保持简单,让我们假设一个没有异步编程的单线程环境。
launch { //<--- creates a coroutine in which we can use suspend functions
fetchUserData("Jon")
}
// and following functions:
suspend fun fetchUserData(userName: String) {
makeLongRunningNetworkCall(userName) //<---- suspends fetchUserData()
}
suspend fun makeLongRunningNetworkCall(userName: String) {...}
字符串
我的理解是,makeLongRunningNetworkCall()
将fetchUserData()
“从”线程中“取出”,这样它在等待makeLongRunningNetworkCall()
的结果时不会阻塞其他计算。
但是如果没有任何东西挂起makeLongRunningNetworkCall()
,线程不是仍然被makeLongRunningNetworkCall()
阻塞吗?我的意思是“等待”网络结果必须在某个地方完成,否则结果可能会被错过?
所以对我来说,如果fetchUserData()
和makeLongRunningNetworkCall()
在不同的线程上运行,那么makeLongRunningNetworkCall()
告诉它的父函数回家并释放它的线程,直到它收到结果,那么在这种情况下suspend
就有意义了。!
我的理解正确吗?或者suspend
意味着整个协程都从线程中删除了?但话又说回来,谁能保证网络呼叫的响应被捕获?
3条答案
按热度按时间zz2j4svz1#
一个suspend函数只有在它调用的suspend函数也suspend时才真正挂起,依此类推。
这就是为什么按照约定,你绝不能在协程或挂起函数中直接调用阻塞函数的原因之一。标准库中的所有suspend函数都遵循这个约定。(另一个重要原因是简单。我们不必担心挂起函数是否也是一个阻塞函数,并占用了一个不应该占用的线程。)
处理阻塞调用的常见模式是将其 Package 在
withContext
中。withContext
是一个suspend函数,当它使用可能能够处理阻塞调用的CoroutineContext运行其代码时会挂起。您可以根据需要将其与Dispatchers.Default
或Dispatchers.IO
一起使用,以允许在withContext
lambda中调用阻塞函数。如果你使用的是流行的库,比如Retrofit、Jetpack Room、Google Firebase等,它们会公开一些公共的
suspend
函数,你可以相信这些函数不会被阻塞,这是约定所要求的。由于大多数库同时支持Java和Kotlin,它们实际上在后台使用自己的线程池,而不是Kotlin Coroutines Dispatcher池。他们通过使用低级别的suspendCancellableCoroutine
suspend函数来实现这一点,该函数允许更精确地控制协程的挂起和恢复方式。另一个值得一提的方面是,尽管你没有问,暂停功能是否支持取消。通常情况下,如果可能的话,我们希望支持取消。所有的标准库挂起函数都是这样。如果您在
withContext
中 Package 了一个长阻塞计算,这不足以支持取消。你还必须在阻塞代码中穿插一些挂起的调用或if (isActive)
检查,如果你想让它在完成之前被停止,就给予它中断和取消的机会。0s0u357o2#
我的理解正确吗?还是说挂起意味着整个协程被从线程中移除?
而不是第二个,但为了看到差异,您需要在
fetchUserData
中添加一些代码。例如,考虑:字符串
如果
makeLongRunningNetworkCall
挂起,那么fetchUserData
* 在执行其剩余代码(return
)之前 * 需要等待它恢复。同样,fetchUserData
的调用者也需要等待等。这就是为什么suspend
函数很容易推理的原因--它们实际上是按顺序运行的。因此,考虑到这一点,整个协程都被挂起(整个堆栈直到初始协程构建器
launch
),因为执行堆栈中的任何东西都不会继续,直到makeLongRunningNetworkCall
恢复。但话又说回来,谁能保证网络呼叫的响应被捕获?
这是个好问题。以上所有内容实际上都是从假设
makeLongRunningNetworkCall
* 确实挂起 * 开始的。这不是一个抽象的概念。它的具体含义是,函数将返回(实际上是返回)一个名为COROUTINE_SUSPENDED
的特殊令牌,因此整个挂起机制发生,线程开始执行其他内容。这意味着这个函数必须是协作的,并且在可能的情况下挂起。如果函数实际上在网络调用时阻塞,那么它实际上并没有挂起,而是如您所猜测的那样阻塞了线程。当像这样的函数实际挂起时,通常意味着它们将阻塞工作卸载到另一个线程,或者它们是真正的非阻塞(例如基于回调)-但有时这只是意味着卸载发生得更深。
为了理解和揭开这是如何工作的,我建议阅读这篇关于
kotlinx.coroutines
是如何构建在几个编译器内置程序上的文章:https://blog.kotlin-academy.com/kotlin-coroutines-animated-part-1-coroutine-hello-world-51797d8b9cd4hm2xizp93#
让我们假设一个没有异步编程的单线程环境
异步编程甚至可以在单线程环境中实现,例如:Python中的
asyncio
和node.js
。协程只是在单个线程上轮流 *(以微/毫秒 * 为单位)并给予并发的概念,但它们并不是真正的并发。这对于IO绑定的任务仍然非常有用,因为它们会触发一些工作并等待结果。对于CPU密集型任务,单线程不足以/不可能实现并发。但是如果没有任何东西挂起makeLongRunningNetworkCall(),线程是否仍然被makeLongRunningNetworkCall()阻塞?还是说挂起意味着整个协程被从线程中移除?
当协程被挂起时,它完全脱离线程,并释放它。稍后当协程恢复时,它可能在不同的线程上恢复。
但话又说回来,谁能保证网络呼叫的响应被捕获?我的意思是“等待”网络结果必须在某个地方完成,否则结果可能会被错过?
No thread is required等待结果,否则我们将只是将工作卸载到每个库(网络IO,数据库,文件IO......)的专用线程池,并创建另一个抽象层。那么如果每个库都有单独的线程 (每个读/写请求) 等待结果,就不会有真实的的好处。
如何在不占用线程的情况下等待结果?
简单的解释是IO库调用 (直接/间接) OS的低级函数,这些函数在某个时候调用设备驱动程序。设备驱动程序立即返回操作系统,请求现在正在进行中并异步执行。
Regardless of the type的I/O请求,代表应用程序向驱动程序发出的内部I/O操作异步执行**;也就是说,一旦发起了I/O请求,设备驱动程序就返回到I/O系统。I/O系统是否立即返回到调用方取决于句柄是为同步还是异步I/O打开的。
操作系统返回到库,库以某种形式返回到调用者 (callback/Future/Observable等),并且调用者不会被阻止。没有线程在等待结果。
Some time after请求开始,设备完成处理请求。它通过中断通知CPU...设备驱动程序的中断服务例程(ISR)响应中断...那么延迟过程调用(DPC)被排队…DPC获取表示初始请求的IRP并将其标记为“完成”。然而,“完成”状态仅存在于OS级别。OS将异步过程调用(APC)排队到拥有设备的底层HANDLE的线程…因为Library/BCL已经向I/O完成端口(IOCP)注册了句柄,IOCP是线程池的一部分。因此,需要短暂借用一个I/O线程池线程来执行APC,APC会通知任务完成。
请求在飞行中时没有线程。当请求完成时,各种线程被“借用”,或者有工作暂时排队等待它们。这项工作通常在毫秒左右(例如,APC在线程池上运行)到微秒左右(例如,ISR)的量级上。但是没有被阻塞的线程,只是在等待该请求完成。