综述
现代的异步编程中有如下的几个概念
协程 coroutine : 用户态的线程,可在某些特定的操作(如IO读取)时被挂起,以让出CPU供其他协程使用。
队列 channel: 队列用于将多个协程连接起来
调度运行时 runtime: 调度运行时管理多个协程,为协程分配计算资源(CPU),挂起、恢复协程
由于协程是非常轻量的,所以可以在一个进程中大量的创建,runtime
会实际创建系统线程(一般为恰好的物理CPU数),并将协程映射到实际的物理线程上执行,这个有时候称为 M:N模型
。好的 runtime 会使得系统整体的性能随着物理CPU的增加而线性增加。
Golang 是原生支持上述模型的语言,这也是 Golang
与众不同的主要特性,在 Golang
中,通过关键词 go
即可轻松开启一个协程,通过关键词 chan
则可以定义一个队列,Golang
内置了调度运行时来支撑异步编程。
Rust 在 2019年的 1.39
版本中,加入 async/.await
关键词,为异步编程提供了基础支撑,之后,随着 Rust
生态中的主要异步运行时框架之一 tokio 1 发布,Rust
编写异步系统也变得跟 Golang
一样方便。
Kotlin 是一个基于 JVM 的语言,它语言层面原生支持协程,但由于 JVM 现在还不支持协程,所以它是在 JVM 之上提供了的调度运行时和队列。顺便,阿里巴巴的 Dragonwell JDK 在 OpenJDK 的基础上可以选择开启 Wisp2 特性,来使得 JVM 中的 Thread 不再是系统线程,而是一个协程。JDK 19 开始增加了预览版的轻量级线程(协程),也许在下一个 JDK LTS 会有正式版。
下表对比了使用这两种语言对异步编程的特性支持
Golang | Rust | Kotlin | |
---|---|---|---|
协程 | 语言内置 | 由异步运行时框架提供 | 语言内置 |
队列 | 语言内置 | 由异步运行时框架提供 | 语言内置 |
调度运行时 | 语言内置,不可更改 | 多个实现, tokio/async_std/... | 语言内置 |
异步函数 | 无需区分 | 需显式的定义 | 需显式定义 |
队列类型 | 无需特指,只有一种 mpmc | 可特指,不同的场景提供不同实现 | 无需特指 |
垃圾回收 | 通过GC算法进行垃圾回收 | 无GC,资源超出作用域即释放 | 通过GC算法进行垃圾回收 |
oneshot: 代表一个发送者,一个接收者的队列
mpsc: 代表多个发送者,一个接收者的队列
spmc/broadcast: 代表一个发送者,多个接收者的队列
mpmc/channel: 代表多个发送者,多个接收者的队列
根据场景的不同,选择不同的队列,不同的运行时,可以得到更好的性能,但 Golang
和 Kotlin
简化了这些选择,一般来说,简化会带来性能的损失,本文测评 Go/Rust(tokio)/Kotlin 的调度和队列性能。
场景设计
测评的逻辑如下
创建 N 个接收协程&