代码详见 Repo: https://github.com/icbd/ruby3-MutlThread-vs-Ractor
Ractor 要点
不要通过共享内存来通信,而应该通过通信来共享内存.
CRuby 中, 每个 Ractor 持有各自的 GVL, 所以他们能实现真正的并行. 每个 Ractor 创建一个自己的线程.
-
Push 模式
Ractor#send
&Ractor.receive
. sender 给已知的 receiver 发消息. -
Pull 模式
Reactor.yield
&Reactor#take
. receiver 从已知的 sender 收消息.
既然是通过发消息来通信, 那么消息就一定是不可变的对象, 在 Ruby 里就是 freeze
的.
如果传入一个可变对象, 默认会通过深拷贝的方式 复制
一份, 当然也可以选择把对象 移动
到 Ractor 内部(这个对象在外面将不能被访问).
测试平台
MacBook Pro (16-inch, 2019)
2.6 GHz 六核Intel Core i7
测试 Route
只有 IO 耗时:
curl -v http://localhost:3000/io
只有计算密集:
curl -v http://localhost:3000/cpu
IO 耗时加计算密集:
curl -v http://localhost:3000/io-cpu
测试数据
ab -n 10 -c 1 http://localhost:3000/io
multi_threads.rb
Time taken for tests: 20.050 seconds
multi_ractor.rb
Time taken for tests: 20.054 seconds
ab -n 10 -c 10 http://localhost:3000/io
multi_threads.rb
Time taken for tests: 2.011 seconds
multi_ractor.rb
Time taken for tests: 2.005 seconds
ab -n 10 -c 1 http://localhost:3000/cpu
multi_threads.rb
Time taken for tests: 19.973 seconds
multi_ractor.rb
Time taken for tests: 19.651 seconds
ab -n 10 -c 10 http://localhost:3000/cpu
multi_threads.rb
Time taken for tests: 19.618 seconds
multi_ractor.rb
Time taken for tests: 3.160 seconds
数据解释
-n 10 -c 1
作为对照组, 一个 client 重复顺序请求十次, 计算得出平均每个请求的耗时.
-n 10 -c 10
作为实验组, 是个 client 总共请求十次, 查看并发情况下的总耗时.
另外, 由于 Ractor.recv
可以方便地在 loop 内取参数而不用每次创建新的 Ractor,
为了抹平创建线程的时间开销, 多线程模式我们使用了池化的线程池. (创建线程的开销在这个例子中其实可以忽略不计)
我们用 sleep
模式 IO
.
可以看到多线程模式和 Ractor 模式对于 IO 的并发都处理的很好, 这是因为 GIL 遇到 IO 就会释放. 对于 IO 密集型的 APP, 是否能使用多核其实影响并不大, 重点是对 IO 进行合理的异步处理.
我们用计算斐波那契来模拟 CPU 密集型计算.
可以看到多线程模式下, 并发总耗时跟顺序请求没什么差异, 因为在只使用单核的情况下, 顺序的 fib
已经能让单个 CPU 跑满了, 多线程对并发解决问题没有帮助, 反而还会白白增加线程调度的开销.
Ractor 模式处理 CPU 密集型计算的优势就很好的展现了. 每个 Ractor 持有各自的 GIL, Ractor 相互之间的 GIL 不会相互干扰, 能利用多 CPU 真正 并行 处理多个 fib
计算.