单机百万并发:协程开始吊打线程了?
著名的C10K问题——即单机如何支持1万并发连接,想必大家都有所了解,这个问题推动了epoll/kqueue等IO多路复用机制的诞生。

在这场技术演进中,线程模型的局限性逐渐显现。。。
这篇文章中我们来分析下为什么只使用线程不容易做到单机百万并发。
首先来看下线程模型在高并发场景下的资源消耗问题。
线程作为操作系统进程内的执行单元,其开销主要体现在两个方面:资源占用和上下文切换成本。
提到资源占用很多资料会讲线程的栈是MB级别而协程的栈是KB级别,因此百万并发下线程占据的内存过多,然而这并不完全正确。
因为就栈的物理内存占用来说,线程可能并不比协程多。
所谓线程的栈大小为MB级别仅仅是虚拟内存大小,假设Linux内核为每个线程默认设置 8MB 虚拟内存,那么在32位系统下虚拟地址空间仅 4GB,512 个线程就会耗尽(8MB × 512 = 4GB),而64位系统虽虚拟空间巨大,但线程数仍受 vm.max_map_count 等内核参数限制。

现在假设我们放开内存参数的限制。
如果线程的栈在函数真正运行时仅使用 1KB,那么真正的物理内存也就只分配 1KB,剩余虚拟空间仅是地址保留,不占物理内存,此时单线程的物理内存占用与协程基本相当。
因此用户态栈的内存占用并不是关键原因。
既然用户态栈的资源占用不是线程的瓶颈,那么线程还有其它问题吗?
我们知道线程切换依赖操作系统内核的抢占式调度,每次切换涉及陷入内核态以及从内核态恢复到用户态,这涉及到用户态到内核态的上下文切换,需要保存/恢复完整上下文信息,以及内核中涉及锁的竞争,单机百万并发(线程)仅仅浪费在这里的CPU时间就足以形成性能瓶颈。

而协程则实现在用户态,协程的切换无需陷入内核,因此不需要保存/恢复完整的上下文信息,仅需保存少量寄存器,全程无需内核介入。
传统线程模型采用的是抢占式调度,即操作系统可以在任何时刻强制中断正在执行的线程,将CPU时间分配给其他线程。
这种调度方式对于Linux这种通用操作系统来说是必须的,但缺点是上下文切换成本高。
而协程采用的是协作式调度,它的核心理念是:协程主动让出CPU资源,而不是被动等待被抢占,而协程能做到这一点当然是因为协程完全是用户态的事情,用户态程序相信自己。
而对于线程来说,内核决不能把CPU的分配权交给用户态程序,因此不是线程做不到协作式调度而是不能。
具体来说,协程会在以下几种情况下主动让出执行权:
- 执行IO操作时(如网络请求)
- 显式调用类
yield函数时 - 等待锁或其他同步原语时
协作式这种调度方式的优势在于:
- 切换时机可预测:协程切换发生在代码的明确位置
- 避免了不必要的切换:只有在真正需要等待的时候才会切换,减少了无效切换
- 降低了同步原语的复杂度:许多情况下可以避免使用复杂的锁机制
这里绝不是说协作式调度就比抢占式调度好,仅仅是在IO密集型应用中,协作式调度模式的优势更为明显,使得利用协程可以轻易实现单机百万并发。
协作式调度要比抢占式调度简单太多,不是内核实现不了而是通用内核不能把CPU分配权交给用户态。
因此,不是线程不够强,而是一些特定场景下协程重构了游戏规则。





