@
statumer 这是显然的强加因果。
网络编程的崛起之于协程已经是很早的事情了,实际上,nginx 和 Redis 都是单线程 IO 模型( Nginx 1.17 和 Redis 6 才支持的多线程网络 IO 模型),性能并不比后面很多所谓用了多线程 or 协程的应用差,那么协程在这里的作用是什么?从另一个角度,OpenResty 往 Nginx 里面嵌入了一个 LuaJIT ,底层 IO 逻辑完全没变的情况下,为什么比 Nginx 性能好那么多?这些和协程崛起相关没有任何直接联系。
关于二三点,既然你已经提到了 go ,为什么不去看看 go 的源码实现和 Scheduler 设计? Design Doc (
https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#heading=h.c3s328639mw9 )和 1.17 的源码 (
https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/runtime/proc.go;bpv=1;bpt=0 )都说明了 Go 的工作方式正是我所说的在多核系统下,会用另一个线程去跑 blocking syscall ,对于你的程序来说,这部分 syscall 就是 non-blocking 的。同样在 Java 最新的协程方案 Project Loom 里面,工作方式也是一样的 (
https://blogs.oracle.com/javamagazine/post/going-inside-javas-project-loom-and-virtual-threads )。特别要提示一点,虽然现在 Go 确实在可以 poll 读文件的情况下使用 epoll ,但是在 bsd 环境下 Go 现在还是使用 blocking syscall ,而更早的 Go 版本(我没记错是 1.12 以及之前)同样使用的 blocking 的文件 syscall 去读文件。
实际上,如果你真的对所谓的“只是对有无调用栈的取舍”做出了理解的话,你就不会陷入这种强加因果的关系。对于传统的线程,我们会有两种程度的开销:一个是线程自身占用 memory 的开销,这个和系统的默认栈大小有关,这导致了我们的内存占用会随着线程申请数量,这个线程自身的申请也需要通过 syscall ,时间和内存资源占用都存在 overhead ;另一个角度,虽然根据机器和系统不同数据有差异,但是普遍来说,native thread 切换的开销并不小,对于 常用 Linux 发行版来说,这个切换通常不少于 1ms 。当我们实现了用户侧调度,我们可以通过巧妙地 GC 管理等减少这种异步结构的申请和释放开销,通过剔除和当前应用无关的系统 OS 数据字段同步达到更快的上下文切换,甚至直接基于状态机模型舍弃这部分开销,得到普遍更好的执行时间。更进一步来说,OS Level 的调度是 generic 的,内核针对的是任何任务的调度,而对于我们的应用,我们和 runtime 更容易知道我们的调度侧重点和优势,能够把时间和优先级排布做的更精妙,自然能够带来一定的性能提升,这才是用户侧调度的意义所在,也是协程这类结构的性能提升所在。
我同意异步的 IO 会比起同步 IO 有性能提升,虽然平台不同变量较大,但在普遍比较下 IOCP 确实性能和系统利用率是强于 epoll 的,io_uring 确实是未来的方向。但是这部分的性能提升和线程以及协程本身开销优化没有任何关系,假如我们认为线程和协程切换都是零开销的话,那使用哪种结构都影响不了在同一种 IO 下的调度开销。顺带补充一点,线程的上下文切换和调度也是要 involve syscall 的,用户侧实现调度的话能够减少这些 syscall 的次数。
最后,异步是一个很复杂的概念,但是我们可以抛弃一些严谨性的情况下认为异步就是并发。对于并发来说,根据你观察的 level 不同,多进程对于操作系统来说是并发的,多线程对于应用是并发的,而我多次提到,所谓的协程就是一种用户调度抽象,实际上和多线程相比就是执行模型的差距,本质上和多线程没有区别。
最后,如果你认为在当时文章里描述的 LWP 和 Linux Thread 的调度没有任何区别的话,那可能不求甚解的是你。Java 1.3 是 2002 年发布的,而 Linux 真正实现了内核可抢占是在 2003 年的 2.6 版本。由于 1:N 的设计,当某个 task 开始调用 blocking syscall 的时候,其他的所有 task 都没有机会被唤醒,更别提调度到其他 core 去执行;而对于 Solaris 的调度,LWP 可以任意被调度到可用的 kernal thread 上,进而有机会被调度到其他 CPU core 上(
https://www.usenix.org/legacy/publications/library/proceedings/sa92/eykholt.pdf )。这些历史背景才是当时选择的原因。当然,Linux 现在对实时性的支持已经非常好了,所以 Project Loom 重拾历史积淀,再次把用户侧线程引入 Java 。