Kotlin 协程 - 延迟,它是如何工作的?

Tho*_*ook 7 kotlin kotlin-coroutines

我很习惯使用 RX 来处理并发,但是,在我目前的工作中,我们混合使用了 AsyncTask、Executors + Handlers、Threads 和一些 LiveData。现在我们正在考虑转向使用 Kotlin Coroutines(事实上已经开始在代码库的某些地方使用它)。

因此,我需要开始关注协程,最好是利用我现有的并发工具知识来加快进程。

我曾尝试为他们关注 Google 代码实验室,虽然它让我有所了解,但它也提出了许多悬而未决的问题,因此我尝试通过编写一些代码、调试和查看日志输出来弄脏我的手。

据我了解,协程由 2 个主要构建块组成;挂起函数是你工作的地方,协程上下文是你执行挂起函数的地方,这样你就可以处理协同程序将在哪些调度程序上运行。

下面我有一些代码,它的行为符合我的预期。我已经使用 Dispatchers.Main 设置了一个协程上下文。因此,正如预期的那样,当我启动协程时,由于以下原因,getResources它最终会阻塞 UI 线程 5 秒Thread.sleep(5000)

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}")
    }
}
Run Code Online (Sandbox Code Playgroud)

当我运行此代码时,我看到以下日志:

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
Run Code Online (Sandbox Code Playgroud)

可以看到,所有的日志都来自主线程,在Thread.sleep(5000). 在这 5 秒的间隙中,UI 线程被阻塞,我可以通过查看模拟器来确认这一点;它不会呈现任何 UI,因为onCreate它被阻止了。

现在,如果我更新getResources函数以使用 suspend fundelay(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
}
Run Code Online (Sandbox Code Playgroud)

然后我最终看到的东西让我感到困惑。我理解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
Run Code Online (Sandbox Code Playgroud)

我可以看到在这种情况下 UI 线程没有被阻塞,因为 UI 在延迟发生时呈现,然后文本视图在 5 秒后更新。

所以,我的问题是,在这种情况下,延迟如何不阻塞 UI 线程(即使我的挂起函数中的日志仍然表明该函数正在主线程上运行......)

Ten*_*r04 11

将挂起函数视为使用接受回调的函数的一种方式,但不需要您将该回调传递给它。相反,回调代码是挂起函数调用下的一切。

这段代码:

lifecycleScope.launch {
    myTextView.text = "Starting"
    delay(1000L)
    myTextView.text = "Processing"
    delay(2000L)
    myTextView.text = "Done"
}
Run Code Online (Sandbox Code Playgroud)

有点像:

myTextView.text = "Starting"
handler.postDelayed(1000L) {
    myTextView.text = "Processing"
    handler.postDelayed(2000L) {
        myTextView.text = "Done"
    }
}
Run Code Online (Sandbox Code Playgroud)

永远不应期望挂起函数会阻塞。如果他们这样做了,他们的组成是不正确的。挂起函数中的任何阻塞代码都应该包含在它的背景中,例如withContextor suspendCancellableCoroutine(这是较低级别的,因为它直接与协程延续一起工作)。

如果您尝试编写这样的挂起函数:

suspend fun myDelay(length: Long) {
    Thread.sleep(length)
}
Run Code Online (Sandbox Code Playgroud)

您将收到“不适当的阻塞方法调用”的编译器警告。如果将其推送到后台调度程序,则不会收到警告:

suspend fun myDelay(length: Long) = withContext(Dispatchers.IO) {
    Thread.sleep(length)
}
Run Code Online (Sandbox Code Playgroud)

如果您尝试将其发送到Dispatchers.Main,您将再次收到警告,因为编译器认为主线程上的任何阻塞代码都是不正确的。

这应该让您了解挂起函数应该如何操作,但请记住,编译器无法始终将方法调用识别为阻塞。