【发布时间】:2020-09-15 17:37:15
【问题描述】:
我很习惯使用 RX 来处理并发,但是,在我目前的工作中,我们混合了 AsyncTask、Executors + Handlers、线程和一些 LiveData。现在我们正在考虑转向使用 Kotlin Coroutines (并且实际上已经开始在代码库的某些地方使用它)。
因此,我需要开始研究协程,最好利用我现有的并发工具知识来加快进程。
我已经尝试为他们遵循 Google 代码实验室,虽然它让我有点理解,但它也提出了许多未解决的问题,所以我尝试通过编写一些代码、调试和查看日志输出来弄脏自己的手。
据我了解,协程由 2 个主要构建块组成;挂起函数是你工作的地方,协程上下文是你执行挂起函数的地方,这样你就可以掌握协程将在哪些调度程序上运行。
我在下面有一些代码,其行为与我预期的一样。我已经使用 Dispatchers.Main 设置了协程上下文。因此,正如预期的那样,当我启动协程getResources 时,由于Thread.sleep(5000),它最终阻塞了 UI 线程 5 秒:
private const val TAG = "Coroutines"
class MainActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
log("onCreate", "launching coroutine")
launch {
val resource = getResource()
log("onCreate", "resource fetched: $resource")
findViewById<TextView>(R.id.textView).text = resource.toString()
}
log("onCreate", "coroutine launched")
}
private suspend fun getResource() : Int {
log("getResource", "about to sleep for 5000ms")
Thread.sleep(5000)
log("getResource", "finished fetching resource")
return 1
}
private fun log(methodName: String, toLog: String) {
Log.d(TAG,"$methodName: $toLog: ${Thread.currentThread().name}")
}
}
当我运行此代码时,我会看到以下日志:
2020-05-28 11:42:44.364 9819-9819/? D/Coroutines: onCreate: launching coroutine: main
2020-05-28 11:42:44.376 9819-9819/? D/Coroutines: onCreate: coroutine launched: main
2020-05-28 11:42:44.469 9819-9819/? D/Coroutines: getResource: about to sleep for 5000ms: main
2020-05-28 11:42:49.471 9819-9819/com.example.coroutines D/Coroutines: getResource: finished fetching resource: main
2020-05-28 11:42:49.472 9819-9819/com.example.coroutines D/Coroutines: onCreate: resource fetched: 1: main
如您所见,所有日志均源自主线程,Thread.sleep(5000) 前后的日志之间有 5 秒的间隔。在这 5 秒的间隙中,UI 线程被阻塞了,我可以通过查看模拟器来确认这一点;它不会呈现任何 UI,因为 onCreate 已被阻止。
现在,如果我更新getResources 函数以使用暂停乐趣delay(5000) 而不是像这样使用Thread.sleep(5000):
private suspend fun getResource() : Int {
log("getResource", "about to sleep for 5000ms")
delay(5000)
log("getResource", "finished fetching resource")
return 1
}
然后我最终看到的东西让我感到困惑。我知道delay 与Thread.sleep 不同,但是因为我在由Dispatchers.Main 支持的协程上下文中运行它,所以我希望看到与使用Thread.sleep 相同的结果。
相反,我看到的是 UI 线程在 5 秒延迟发生时没有被阻塞,并且日志看起来像:
2020-05-28 11:54:19.099 10038-10038/com.example.coroutines D/Coroutines: onCreate: launching coroutine: main
2020-05-28 11:54:19.111 10038-10038/com.example.coroutines D/Coroutines: onCreate: coroutine launched: main
2020-05-28 11:54:19.152 10038-10038/com.example.coroutines D/Coroutines: getResource: about to sleep for 5000ms: main
2020-05-28 11:54:24.167 10038-10038/com.example.coroutines D/Coroutines: getResource: finished fetching resource: main
2020-05-28 11:54:24.168 10038-10038/com.example.coroutines D/Coroutines: onCreate: resource fetched: 1: main
我可以看到在这种情况下 UI 线程没有被阻塞,因为 UI 在延迟发生时呈现,然后文本视图在 5 秒后更新。
所以,我的问题是,在这种情况下,延迟如何不阻塞 UI 线程(即使我的挂起函数中的日志仍然表明该函数正在主线程上运行......)
【问题讨论】:
-
你可以把协程想象成语法糖,通过嵌套回调编写一系列事件。使用延迟调用挂起函数就像告诉执行程序在后台线程上执行 Thread.sleep,然后在延迟调用后发送到 Main 处理程序的回调中运行延迟调用下面的所有代码。
-
是的,我开始了解@Tenfour04 的那个位,但延迟本身就是一个挂起函数,在这种情况下,它在由 Dispatchers.Main 支持的协程上下文中调用,因此实际上应该挂起在主线程右边(即应该阻塞主线程)?
-
或者
delay是否真的打开了另一个由Dispatchers.Default支持的协程上下文? -
每当你调用一个挂起函数时,如果不查看它的源代码,你就无法知道它是委托给不同的调度程序还是使用其他机制在后台执行某些操作,然后再恢复协程的“续”。正确编写的挂起函数绝不能阻塞调用它的调度程序的线程,因为它可能是主调度程序。
-
如果它被标记为
suspend,你知道它不会阻塞你调用它的主线程,只要那个suspend函数的作者没有出错。对于编写挂起函数时可能犯的某些错误,有一些编译器警告。