随着项目越做越大,对 MRI Ruby 的几种并发服务模型也有了一部分的了解,也抛出来和大家聊聊,希望有点新的收获。
众所周知,MRI Ruby 是一个拥有 GIL 的 Ruby 实现,先天决定了在同一时刻,只会有一个线程被运行,虽然依然无法在根本上解决线程安全的问题,但是根本上来说,一个基础架构的偏向,会导致整体语言相关的社区的导向,简而言之,在 ruby 的世界里,绝大部分时间里,Thread,这个词,就是被忽略的命,纵使 rails,也是才开始重视多线程的问题。
然而,开发越来越多,做的越来越多,慢慢发现,GIL 真的是一个好大好大的限制,但,即使在这样的限制条件下,我们依然在尝试着各种不同的并发模型。
所有的并发模型,在 MRI 的 ruby 这个有 GIL 的实现下,最大的目的就是:
分离 IO 操作和 CPU 操作,让 IO 操作在执行的同时,CPU 并不会堵塞在 IO 等待中,从而实现更高的程序效率。
由 function1 --- io1 --- function2 --- io2 --- 这样的执行策略变成: function1 -- function2 -- funtion1 --- function 2 io1 --------io2---------io1-----------io2----
用一个比较粗略的方式来比较下这几种并发服务模型的效率. 假设,我们认为每个 function 就是一个独立的事务,那么,在事务和事务之间切换的性能也代表了其不考虑 IO 干扰的最高的并行能力,也就是说,只考虑这几个模型下的 Context Switch 的能力。
做了这几年,用到的最多的,就这几种了:
EventMachine 是最早接触的了,本质上来说,EM 是一个巨大的 for 循环,将 IO 操作独立具体事务之外,将控制权让渡于事件核心,由事件核心以事件的方式触发流程继续。
写了一个粗略的测试其切换能力的代码:
require 'eventmachine'
$c = 0
Thread.start do
EM.run do
p = proc {
$c += 1
EM.next_tick(p)
}
EM.next_tick(p)
end
end
arr = []
20.times do
puts $c
arr << $c unless $c == 0
$c = 0
sleep 1
end
puts "avg: " + (arr.inject(0) { |r, i| r + i } / arr.length).to_s
得到的平均值:
avg: 96511
EventMachin 的缺点: callback 太多...
我们想要的代码是:
do_a
do_b
do_c
do_d
而不是
do_a {
do_b {
do_c {
do_d
}
}
}
然后就是 MultiThread: 很神奇,在 1.9 之前,ruby 甚至是不支持 Native 线程的,其线程创建在其 VM 之上,直到 1.9 之后,线程才真正使用了系统级别的线程实现,线程之间的调度,由用户级变成了内核级,从轻量级的实现变成了重量级的实现,切换速度下降了,倒是也带来了不少更接近与原生实践的能力。
#encoding: utf-8
require 'thread'
require 'irb'
$c = 0
2.times do
Thread.new do
while true do
# $cv.wait#($m)
$c += 1
# $cv.signal
Thread.pass
end
end
end
arr = []
20.times do
puts $c
arr << $c unless $c == 0
$c = 0
sleep 1
end
puts "avg: " + (arr.inject(0) { |r, i| r + i } / arr.length).to_s
结果:
# 1 thread, avg: 755507
# 2 thread, avg: 114600
# 3 thread, avg: 56838
# 4 thread, avg: 37229
# 5 thread, avg: 35603
# 100 thread, avg: 31825
困了...Celluloid 等过两天再写....