「码上开学——hencoder」Kotlin笔记(到底什么是「非阻塞式」挂起?协程真的更轻量级吗?)

601 阅读5分钟

在协程系列的前两篇稳文章,我们介绍了:

  • 协程就是个线程框架
  • 协程的挂起本质就是线程切出去再切回来。

什么是「非阻塞式挂起」

非阻塞式是相对阻塞式而言的。

编程语言中的很多概念其实都来源于生活,就像脱口秀的段子一样。

线程阻塞很好理解,现实中的例子就是交通阻塞,它的核心有3点:

  • 前面有障碍物,不过不去(线程卡了)
  • 需要等障碍物清除后才能过去(耗时任务结束)
  • 除非你绕道而行(切到别的线程)

从语义上立即「非阻塞式挂起」,讲的是「非阻塞式」这个是挂起的一个特点,也就是说,协程的挂起,就是非阻塞式,本来就是涉及到多线程。

就像视频里讲的,阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。

所以「非阻塞式挂起」,其实就是在讲协程在挂起的同时切线程这件事情。

为什么要讲非阻塞式挂起

既然第三篇说的「非阻塞式挂起」和第二篇的「挂起到切线程」是同一件事情,那还有讲的必要吗?

是有的。因为它在写法上和单线程的阻塞式是一样的。

协程只是在写法上「看起来阻塞」,其实是「非阻塞」的,因为在协程里面它做了很多工作,其中一个就是帮我们切线程。

第二篇讲挂起,重点是说切线程先切过去,然后再切回来。

第三篇讲非阻塞式,重点是说线程虽然会切,但是写法上和普通的单线程差不多。

让我们来看看下面的例子:

main {
    GlobalScope.launch(Dispatchers.Main) {
        // 耗时操作
        val user = suspendingRequestUser()
        updateView(user)
    }
    
    private suspend fun suspendingRequestUser(): User = withContext(Dispatchers.IO) {
        api.requestUser()
    }
}

从上面的例子可以看到,耗时操作和更新UI的逻辑像写单线程一样放在了一起,只是在外面包了一层协程。

而正是这个协程解决了原来我们单线程写法会卡线程这件事。

阻塞的本质

首先,所有的代码本质上都是阻塞的,而只有比较耗时的代码才会导致人类可感知的等待,比如正在主线程上做一个耗时50ms的操作会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「阻塞」。

举个例子,当你开发的app在性能好的手机上很流畅,在性能差的的老手机上会卡顿,就是再说一行代码执行的时间不一样。

视频中讲了一个网络IO的例子,IO阻塞更多是反映在「等」这件事情上,它的性能瓶颈和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。

协程与线程

协程我们讲了3期,Kotlin协程与线程是无法脱离开讲的。

别的语言我不说,在Kotlin里,协程就是基于线程来实现的一种更上层的工具API,类似于Java中自带的Executor系列API或者Android的Handler系列API。

只不过呢,协程它不仅提供了方便的API,在设计思想上是一个基于线程的上层框架,你可以理解为创造了一些概念来帮助你更高地是使用这些API,仅此而已。

就像ReactiveX一样,为了让你更好地使用各种操作符API,新造了Observable等概念。

说到这里,Kotlin协程的散发疑问:协程是什么、挂起是什么、挂起的非阻塞式是怎么回事,就已经全部讲完了。非常简单:

  • 协程就是切线程
  • 挂起就是可以自动切回来的切线程
  • 挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,就这么简单。

当然了,这几句是总结,它们背后的原理你是一定要掌握住的。

Kotlin协程并没有脱离Kotlin或者JVM创造新的东西,它只是将多线程的开发变得更简单了,可以说是因为Kotlin的诞生而顺其自然出现的东西,从语法上看它很神奇,但从原理上讲,它并不是魔术。

练习题 使用协程实现一个网络请求:

  • 等待时显示 Loading;
  • 请求成功或者出错让 Loading 消失;
  • 请求失败需要提示用户请求失败了;
  • 让你的协程写法上看上去像单线程。
fun main() {
    runBlocking {
        launch {
            try {
                showLoading()
                val result = doNektworRequest()
                handleSuccess(result)
            } catch (e: Exception) {
                handleFailure(e)
            } finally {
                hideLoading()
            }
        }
    }
}

suspend fun showLoading() {
    //在此处显示加载状态
    println("Loading......")
}

suspend fun doNetworkRequest(): String {
    // 模拟网络请求
    delay(2000)
    // 这里可以使用实际的网络请求逻辑
    return "Request Result"
}

suspend fun handleSuccess(result: String) {
    //在此处请求成功的逻辑
    println("Request succeeded. Result: $result")
}

suspend fun handleFailure(exception: Exception) {
    println("Request failed. Exception: ${exception.message}")
}

suspend fun hideLoading() {
    // 此处隐藏加载状态
    pringln("Loading finished.")
}

在上述代码中,runBlocking创建了一个协程作用域,launch启动了一个新的协程来执行网络请求。在协程中,我们通过的调用相应的挂起函数来模拟请求的不同阶段。showLoading()函数用于显示加载状态,doNetworkRequest()函数模拟网络请求并返回结果。 handleSuccess()handleFailure()函数分别处理请求成功和失败逻辑,hideLoading()函数用于隐藏加载状态。

这个示例中的协程代码外观看起来像单线程代码,使用了顺序结构,同时满足了要求:在请求间显示加载状态,请求成功或失败后隐藏加载状态,并根据执行结果相应的处理逻辑。

请注意,这只是一个简单的例子。

版权声明

本文首发于:到底什么是「非阻塞式」挂起?协程真的更轻量级吗?

微信公众号:扔物线