概述
前段时间看到朋友 @贺兰星辰 发布了 Bilibili – 过去、现在和未来 —— Java 的现代化之路,其中在 5:38 附近开始提及协程:
这引起了我对线程协程区别的思考及其调度方式的思考。尽管屏幕上的四个词“用户态线程”“用户态调度”“轻量级”和“降低心智负担”都在提及协程被广为使用,但都感觉有那么一点隔靴搔痒。尤其是“用户态线程”一词,更令人疑惑。因此 2025 年 3 月 7 日中午与贺兰开展了一次讨论,先将相关内容和后续思考简记如下。
思考
抢占式调度 vs 非抢占式调度?
起初我认为协程线程的核心区别在于调度方式,其区别在于触发调度的代码是否是程序主动执行的。值得一提的是,程序主动执行的代码不一定是程序自身的代码,也可能是编译器或运行时会插入和执行的代码,总之,它是开发者可以预见将在运行时在当前进程内执行的代码。
协程有挂起点,只在此处插入代码以检查是否需要切换协程。由于无需在任意时刻调度,不需要中断之类的机制,所以可以在用户态实现非抢占式调度,由此也得出了其“用户态调度”和“轻量级”的特点;而线程采用抢占式调度,进而二者得以区分。
贺兰指出此理解错误,因为仍然存在抢占式调度的协程,而且“有栈协程一般都是抢占式的”,因此其并不能作为区分依据。
对于第一点,经过了解,Go 协程的信号量调度机制确实是抢占式调度的,它是为了避免协程进行 CPU 密集型计算时,连续运行太长时间而设置的。这种抢占式调度需要操作系统的参与,已经一定程度上与其“用户态调度”冲突,且并不普遍,应当视作协程实现上的差异,而非协程本身的特点。
对于第二点,有栈协程和是否抢占实现并没有什么关系,它也可以主动保存上下文(例如 Linux 的 getcontext
和 setcontext
)并切换协程,也看不到倾向于使用抢占式调度的动机。
由此观之,协程的实现确实多样,它不一定是“用户态”的,不一定是非抢占式调度的。那协程线程的区别到底是什么呢?
代码形式 vs 执行载体?
协程的本质是回调:可以像写同步代码那样,在挂起点后写回调代码。为了支持通过协程组织代码,各种语言有自己的协程实现方法(或曰“协程实现”),它们并不影响“协程到底是什么”的答案。在前面的讨论中,我们正是因为没有分清协程和协程实现的区别,误把协程实现的特点作为协程的特点,从而陷入了疑惑。
抛开有栈无栈和调度方式的迷雾,不难发现协程是组织程序代码的一种方法,而线程则是执行这些代码的载体。
后记
实际上,关于协程线程区别的讨论并不能对我们编程带来什么帮助。不过,倘若能为读者提供一个看待协程的思路,那这次讨论和本文便发挥其最大作用了。