最近在看《Working With Ruby Thread》这本书,以下是我对前几章内容的一点总结 : )
几乎所有谈到并发和并行的文章都会提到一点:并发并不等于并行。那么如何理解这句话呢,这里以餐馆下订单为例子进行说明:
并发:同时有 2 桌客人点了菜,厨师同时接收到了两个菜单
顺序执行:如果只有一个厨师,那么他只能一个菜单,一个菜单的去完成
并行执行:如果有两个厨师,那么就可以并行,两个人一起做菜
将这个例子扩展到我们的 web 开发中,就可以这样理解:
并发:同时有两个客户端对服务器发起了请求
顺序执行:服务器只有一个进程(线程)处理请求,完成了第一个请求才能完成第二个请求,所以第二个请求就需要等待。
并行执行:服务器有两个进程(线程)处理请求,两个请求都能得到响应,而不存在先后的问题。
那么,ruby 中如何描述一个并发的行为呢,看这样一段代码:
threads = 3.times.map do
Thread.new do
sleep 3
end
end
puts "不用等3秒就可以看到我"
threads.map(&:join)
Thread 的创建是非阻塞的,所以文字立即就可以输出,这样就模拟了一个并发的行为。
接下来,对代码做一点修改:
time = Time.now
threads = 3.times.map do
Thread.new do
sleep 3
end
end
threads.map(&:join)
puts "现在需要等3秒才可以看到我"
p Time.now - time
当我们执行 join 的时候,只有等到所有线程的任务都执行完成,才会最后输出。所以我们需要等待 3 秒才能看到输出的文字。
但是,等等,这里是不是就是实现了并行了呢?
从表面上来看是这样,但是很遗憾,这是一种伪并行,我们再对代码做一点修改:
require 'benchmark'
def multiple_threads
count = 0
threads = 4.times.map do
Thread.new do
2_500_000.times { count += 1}
end
end
threads.map(&:join)
end
def single_threads
time = Time.now
count = 0
Thread.new do
10_000_000.times { count += 1}
end.join
end
Benchmark.bm do |b|
b.report { multiple_threads }
b.report { single_threads }
end
user system total real
0.510000 0.000000 0.510000 ( 0.508958)
0.500000 0.000000 0.500000 ( 0.506755)
从这里可以看出,即便我们将同一个任务分成了 4 个线程并行,但是时间并没有减少,这是为什么呢?
因为有 GIL 的存在!!!
MRI,也就是我们通常使用的 ruby 采用了一种称之为 GIL 的机制,看看它的解释:
The GIL is a global lock around the execution of Ruby code
If one of those MRI processes spawns multiple threads, that group of threads will share the GIL for that process.
If one of these threads wants to execute some Ruby code, it will have to acquire this lock. One, and only one, thread can hold the lock at any given time. While one thread holds the lock, other threads need to wait for their turn to acquire the lock
--------- Working With Ruby Threads By Jesse Storimer -----------
也就是说,即便我们希望使用多线程来实现代码的并行,由于这个全局锁的存在,每次只有一个线程能够执行代码,至于哪个线程能够执行,这个取决于底层操作系统的实现。
即便我们拥有多个 CPU,也只是为每个线程的执行多提供了几个选择而已。
但是我们之前sleep
的时候,明明实现了并行啊!
这个就是 Ruby 设计高级的地方——所有的阻塞操作是可以并行的,也就是说包括读写文件,网络请求在内的操作都是可以并行的,有代码为证:)
require 'benchmark'
require 'net/http'
def multiple_threads
uri = URI("http://www.baidu.com")
threads = 4.times.map do
Thread.new do
25.times { Net::HTTP.get(uri) }
end
end
threads.map(&:join)
end
def single_threads
uri = URI("http://www.baidu.com")
Thread.new do
100.times { Net::HTTP.get(uri) }
end.join
end
Benchmark.bm do |b|
b.report { multiple_threads }
b.report { single_threads }
end
user system total real
0.240000 0.110000 0.350000 ( 3.659640)
0.270000 0.120000 0.390000 ( 14.167703)
那么,既然有了这个锁的存在,是否意味着我们的代码就是线程安全了呢?很遗憾,不是!因为我们无法控制什么时候操作系统会终止我们当前线程的执行,并切换到另外一个线程上。
class MultipleThreadTest
@n = 0
def self.cal
10000.times.map do
Thread.start { @n += 1 }
end
@n
end
end
p MultipleThreadTest.cal # 9584
对于n += 1
这种非线程安全的代码,即便有锁的存在,依旧是不安全的。
最后,我们用 Sidekiq 的作者 Mike Perham 的话来结束这篇入门文章:
As soon as you introduce the Thread constant, you've probably just introduced 5 new bugs into your code.
更多的内容请参考:《Working With Ruby Thread》这本书 : )