协程,协程
2023-08-04 19:09:17

水一篇,不保证能看懂,能看懂都是缘分,

关于并发,涉及的关键概念即 “阻塞、多线程、异步、非阻塞、挂起、系统内核、分时复用”,

阻塞

如果只有单线程,那么代码执行到 IO 时,会干等着,直到 IO 结束,才再继续下一步,这种 “干等着” 的现象即阻塞,

如果该线程主要用于 “客户端图形渲染” 或 “服务端接收请求响应”,在该线程中阻塞显然不合适,图形渲染和请求响应会被搁置,乃至给人 “无法响应” 的糟糕体验感受,

多线程

为此引入多线程技术,在需要 IO 时,新开一个工作线程,并在工作线程中阻塞,如此根据 “分时复用”,主线程的任务很快便能再度被分到 “时间片” 运行,乃至不再卡住,

操作系统与分时复用

分时复用是系统内核关于 CPU 资源分配的机制,

操作系统的存在,是为了将有限硬件资源 “合理分配” 以实现效益最大化,

分时复用即是将 CPU 算力以时间为单位进行划分,然后线程之间轮流享用时间片进行计算,

假设 CPU 每秒运行一亿次,此时若有两个线程,则线程 A 分到 1 毫秒,足够 10 万次计算,然后轮到线程 B 分到 1 毫秒运行,再然后线程 A,线程 B … 如此往复,直到任务结束,

连贯性

分时复用实现了资源的公平分配,让任务耗时均摊,并且线程执行到阻塞操作时,控制权移交给系统,该线程被系统控制,不再争抢时间片,进一步减少 CPU 资源的浪费,

不过开工作线程再回调,破坏了代码顺序书写的连贯性,甚至可能出现层层嵌套的回调地狱,

为此引入 “非阻塞” 设计,

非阻塞

所谓非阻塞即,执行到 “原本意味着阻塞的操作,比如 IO 操作” 时,可以立即返回,继续执行当前线程中原有任务(比如 eventLoop 乃至消息队列中的其他任务),直到 IO 操作结束,系统会发消息到消息队列,稍后线程执行到该消息,读到结果并执行与此相关的下一步,

也即对于 “非阻塞” 操作,可以直接在主线程中执行,执行时不会将主线程阻塞,而是当即返回和继续下一步,直到操作结束时自动返回结果,并根据结果继续相关下一步,

其中 “直到操作结束时自动返回结果,并根据结果继续相关下一步” 又被称作 “异步”,也即上述举例是一种异步非阻塞,

常见非阻塞

js 是一种单线程语言,为了能在主线程中执行 IO,js 引入非阻塞机制,

遇到 IO 操作时,交给系统内核,这期间 js 不会干等,仍旧基于 eventLoop 处理图形渲染或请求响应,然后内核会在结束时,发消息到事件循环模型的队列,通过回调函数告知程序可以继续下一步,

kotlin 是一种多线程语言,与 java 的区别在于,kotlin 可以通过协程来实现 “非阻塞” 式写法,也即直接在主线程中开一个协程,并在协程中同步方式书写异步代码,

如此,当程序执行到异步块时,会直接挂起整个协程,不阻塞主线程,并且随即切换到子线程,执行阻塞操作,直到操作完,唤醒协程并提供异步任务的结果,来继续协程中的下一步,

也即 kotlin 协程虽然涉及线程切换,但理念上仍然遵照 “异步非阻塞”,

由于 kotlin 编译过程中会转换为 java 字节码,而 java 本身没有采取类似 js 的非阻塞机制,故此可认为,kotlin 是单纯语法层面上实现的异步非阻塞,通过一系列 “编译时自动生成代码” 实现 “同步方式写异步代码”,

多线程的非阻塞

js 由于单线程,对于 “计算密集型任务”,仍然只能同步执行,越往后的请求,用户等待响应的时间便越久,

所以尽管 google v8 引擎加持的 nodejs 通过生成机器码能使 js 性能发生质的飞跃,使之适合承担 “IO 密集型” 的 web 服务,如数据库读写、日志记录等,但 js 仍不适合计算密集型业务,例如加解密、甚至 AI 计算,

那么此时需要多线程的非阻塞,

也即对于 IO 密集型或计算密集型任务,都丢给线程池去管理和操作,

对于 IO 型线程池,由于阻塞不怎么占时间片,应尽可能多开和确保每个 IO 请求都被及时处理,那么可根据任务数动态创建和销毁线程,减少系统开销;计算型任务由于吃时间片,应尽可能减少时间上的损耗,那么计算型线程池通常线程数固定,避免创建销毁线程带来的时间开销,且确保每个线程分到的时间片最大化,尽快完成计算任务,

对此还是可以借助 kotlin 协程,通过设置合适的 Dispatcher,余下的协程自己会组织线程池完成任务调度,无需开发者操心细节,

协程

故此,java 相较于 js,支持计算密集型任务,

kotlin 相较于 java,支持同步方式写异步代码,并且设置 Dispatcher 即可安排合适的任务模式,

由于 kotlin 协程是语法层面实现的异步非阻塞,故此在执行 IO 密集型任务时,由于线程动态创建和频繁切换的缘故,效率可能反而没有单线程的 js 来的高,

综上

协程非常适合客户端和服务端开发,

对于 IO 密集型,协程能确保非阻塞;对于计算密集型,协程能减少上下文切换的损耗,

现代化语言基本都有协程,例如 kotlin、swift 都有 async/await 挂起函数。

值得注意的是,js 中也存在 async/await,不过那本质上不具备挂起和恢复能力,不属于协程。

版权声明

Copyright © 2019-present KunMinX 原创版权所有。

如需 转载本文,或引用、借鉴 本文 “引言、思路、结论、配图” 进行二次创作发行,须注明链接出处,否则我们保留追责权利。