大部分编程语言都支持异步编程,异步编程作为一种并发编程模型,可以使用少量的操作系统线程完成大量的并发任务。通过多线程也可以提高系统处理任务的效率。本文记录说明他们之间的差异与各自的优势,并会给出一个简单HTTP SERVER(Rust语言实现)多线程版本与异步版本的基准测试
并发模型
与常规的顺序编程相比,并发编程不那么标准化,不同语言有各自所支持的并发编程模型,并发性可以由不同的并发编程模型表达。以下为常见并发编程模型
OS threads
线程适用于少量任务的使用场景,线程通常附带CPU、内存的开销。线程间的切换是有成本的且很昂贵,即使是空闲的线程也在消耗系统资源。线程池可以解决一部分开销问题,但不能完全解决,使用线程不足以支持大量IO绑定型任务,如:服务器、数据库等。多线程对于现有代码的改造不依赖于特定的编程模型,不需要进行大量的代码重构就可以让现有的同步代码支持多线程
Event-driven programming
事件驱动编程与回调结合使用,可以有很好的性能表现,但通常会导致冗长的非线性控制流,很难遵循程序的数据流与错误传播
Coroutines
协程与线程一样,不需要特定的编程模型,很容易进行代码改造。与异步相似,能够支持处理大量的任务。协程抽象了对系统编程和自定义运行时实现器很重要的低级细节
The actor model
参与者模型将所有并发计算划分为称为参与者的单元,这些单元通过会出错的消息传递进行通信,就像在分布式系统中一样。参与者模型可以被高效实现,但它留下了许多实际问题,如流程控制和重试逻辑
Rust中的异步
异步可以降低CPU、内存开销,尤其对于大量IO绑定型任务的工作负载。同等条件下,可以比多线程处理更多的任务数量级。异步并不意味着一定比多线程好,对于小任务量的工作负载,多线程同样能够胜任。与其他编程语言不同,Rust的异步是惰性的,只有在发生 polled 的时候异步代码才会真正执行。Rust异步支持单线程,也支持多线程
HTTP SERVER 基准测试
使用WRK进行HTTP基准测试
1 2 3 4 5 6 7
| docker run --rm 8lovelife/wrk -txxx -cxxx -d30 \ --timeout=15 http://host.docker.internal:port
-t: 线程数 -c: HTTP连接数 -d: 测试持续时间。-d30 表示持续测试30s N/A: 宕机
|
ROUND 1
- |
QPS |
THROUGHPUT |
AVG |
MEMORY QUOTA |
CPU QUOTA |
TIMEOUT |
V1 single-thread |
0.63 |
19 |
12.23s |
10M |
2 |
0 |
V2 multi-thread |
6.32 |
190 |
1.51s |
10M |
2 |
0 |
V3 thread-pool |
6.29 |
189 |
1.52s |
10M |
2 |
0 |
V4 async single-thread |
6.32 |
190 |
1.51s |
10M |
2 |
0 |
V5 async multi-thread |
6.32 |
190 |
1.51s |
10M |
2 |
0 |
ROUND 2
- |
QPS |
THROUGHPUT |
AVG |
MEMORY QUOTA |
CPU QUOTA |
TIMEOUT |
V1 single-thread |
0.63 |
19 |
16.50s |
10M |
2 |
0 |
V2 multi-thread |
63.21 |
1900 |
1.51s |
10M |
2 |
0 |
V3 thread-pool |
6.61 |
199 |
11.67 |
10M |
2 |
0 |
V4 async single-thread |
63.22 |
1900 |
1.51 |
10M |
2 |
0 |
V5 async multi-thread |
63.18 |
1900 |
1.51 |
10M |
2 |
0 |
ROUND 3
- |
QPS |
THROUGHPUT |
AVG |
MEMORY QUOTA |
CPU QUOTA |
TIMEOUT |
V1 single-thread |
0.63 |
19 |
16.50s |
10M |
2 |
0 |
V2 multi-thread |
NA |
N/A |
N/A |
10M |
2 |
N/A |
V3 thread-pool |
6.60 |
199 |
15.74s |
10M |
2 |
0 |
V4 async single-thread |
614.44 |
18529 |
1.54s |
10M |
2 |
0 |
V5 async multi-thread |
615.77 |
18565 |
1.54s |
10M |
2 |
0 |
ROUND 4
- |
QPS |
THROUGHPUT |
AVG |
MEMORY QUOTA |
CPU QUOTA |
TIMEOUT |
V1 single-thread |
0.73 |
23 |
16.50s |
10M |
2 |
5 |
V2 multi-thread |
NA |
N/A |
N/A |
10M |
2 |
N/A |
V3 thread-pool |
6.78 |
209 |
13.52s |
10M |
2 |
10 |
V4 async single-thread |
673.95 |
20734 |
2.72s |
10M |
2 |
0 |
V5 async multi-thread |
635.85 |
19738 |
3.49s |
10M |
2 |
0 |
ROUND 5
- |
QPS |
THROUGHPUT |
AVG |
MEMORY QUOTA |
CPU QUOTA |
TIMEOUT |
V1 single thread |
0.73 |
23 |
16.50s |
10M |
2 |
5 |
V2 multi-thread |
N/A |
N/A |
N/A |
10M |
2 |
N/A |
V3 thread-pool |
7.59 |
239 |
15.96s |
10M |
2 |
40 |
V4 async single-thread |
593.99 |
20044 |
4.64s |
10M |
2 |
0 |
V4 async single-thread |
911.34 |
28938 |
2.15s |
20M |
2 |
0 |
V5 async multi-thread |
N/A |
N/A |
N/A |
10M |
2 |
N/A |
V5 async multi-thread |
896.92 |
28978 |
2.42s |
20M |
2 |
0 |
参考
Asynchronous Programming in Rust
附录
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| version: '3.4'
services: webserver_v1: image: 8lovelife/webserver:v1 ports: - 17878:7878 deploy: resources: limits: cpus: '2' memory: 10M
webserver_v2: image: 8lovelife/webserver:v2 ports: - 27878:7878 deploy: resources: limits: cpus: '2' memory: 10M
webserver_v3: image: 8lovelife/webserver:v3 ports: - 37878:7878 deploy: resources: limits: cpus: '2' memory: 10M
webserver_v4: image: 8lovelife/webserver:v4 ports: - 47878:7878 deploy: resources: limits: cpus: '2' memory: 20M
webserver_v5: image: 8lovelife/webserver:v5 ports: - 57878:7878 deploy: resources: limits: cpus: '2' memory: 20M
|
Sometimes you have to sacrifice everything you love to save everything you love. - Murph
Interstellar