ruby中并发并行与全局锁详解
前言
本文主要给大家介绍了关于ruby并发并行和全局锁的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。
并发和并行
在开发时,我们经常会接触到两个概念:并发和并行,几乎所有谈到并发和并行的文章都会提到一点:并发并不等于并行.那么如何理解这句话呢?
- 并发:厨师同时接收到了2个客人点了的菜单需要处理.
- 顺序执行:如果只有一个厨师,那么他只能一个菜单接着一个菜单的去完成.
- 并行执行:如果有两个厨师,那么就可以并行,两个人一起做菜.
将这个例子扩展到我们的web开发中,就可以这样理解:
- 并发:服务器同时收到了两个客户端发起的请求.
- 顺序执行:服务器只有一个进程(线程)处理请求,完成了第一个请求才能完成第二个请求,所以第二个请求就需要等待.
- 并行执行:服务器有两个进程(线程)处理请求,两个请求都能得到响应,而不存在先后的问题.
根据上述所描述的例子,我们在ruby中怎么去模拟出这样的一个并发行为呢?看下面这一段代码:
1、顺序执行:
模拟只有一个线程时的操作.
require'benchmark' deff1 puts"sleep3secondsinf1\n" sleep3 end deff2 puts"sleep2secondsinf2\n" sleep2 end Benchmark.bmdo|b| b.reportdo f1 f2 end end ## ##usersystemtotalreal ##sleep3secondsinf1 ##sleep2secondsinf2 ##0.0000000.0000000.000000(5.009620)
上述代码很简单,用sleep模拟耗时的操作.顺序执行时候的消耗时间.
2、并行执行
模拟多线程时的操作
#接上述代码 Benchmark.bmdo|b| b.reportdo threads=[] threads<我们发现多线程下耗时和f1的耗时相近,这与我们预期的一样,采用多线程可以实现并行.
Ruby的多线程能够应付IOBlock,当某个线程处于IOBlock状态时,其它的线程还可以继续执行,从而使整体处理时间大幅缩短.
Ruby中的线程
上述的代码示例中使用了ruby中Thread的线程类,Ruby可以很容易地写Thread类的多线程程序.Ruby线程是一个轻量级的和有效的方式,以实现在你的代码的并行.
接下来来描述一段并发时的情景
defthread_test time=Time.now threads=3.times.mapdo Thread.newdo sleep3 end end puts"不用等3秒就可以看到我:#{Time.now-time}" threads.map(&:join) puts"现在需要等3秒才可以看到我:#{Time.now-time}" end test ##不用等3秒就可以看到我:8.6e-05 ##现在需要等3秒才可以看到我:3.003699Thread的创建是非阻塞的,所以文字立即就可以输出.这样就模拟了一个并发的行为.每个线程sleep3秒,在阻塞的情况下,多线程可以实现并行.
那么这个时候我们是不是就完成了并行的能力呢?
很遗憾,我上述的描述中只是提到了我们在非阻塞的情况下可以模拟了并行.让我们再看一下别的例子:
require'benchmark' defmultiple_threads count=0 threads=4.times.mapdo Thread.newdo 2500000.times{count+=1} end end threads.map(&:join) end defsingle_threads time=Time.now count=0 Thread.newdo 10000000.times{count+=1} end.join end Benchmark.bmdo|b| b.report{multiple_threads} b.report{single_threads} end ##usersystemtotalreal ##0.6000000.0100000.610000(0.607230) ##0.6100000.0000000.610000(0.623237)从这里可以看出,即便我们将同一个任务分成了4个线程并行,但是时间并没有减少,这是为什么呢?
因为有全局锁(GIL)的存在!!!
全局锁
我们通常使用的ruby采用了一种称之为GIL的机制.
即便我们希望使用多线程来实现代码的并行,由于这个全局锁的存在,每次只有一个线程能够执行代码,至于哪个线程能够执行,这个取决于底层操作系统的实现。
即便我们拥有多个CPU,也只是为每个线程的执行多提供了几个选择而已。
我们上面代码中每次只有一个线程可以执行count+=1.
Ruby多线程并不能重复利用多核CPU,使用多线程后整体所花时间并不缩短,反而由于线程切换的影响,所花时间可能还略有增加。
但是我们之前sleep的时候,明明实现了并行啊!
这个就是Ruby设计高级的地方——所有的阻塞操作是可以并行的,包括读写文件,网络请求在内的操作都是可以并行的.
require'benchmark' require'net/http' #模拟网络请求 defmultiple_threads uri=URI("http://www.baidu.com") threads=4.times.mapdo Thread.newdo 25.times{Net::HTTP.get(uri)} end end threads.map(&:join) end defsingle_threads uri=URI("http://www.baidu.com") Thread.newdo 100.times{Net::HTTP.get(uri)} end.join end Benchmark.bmdo|b| b.report{multiple_threads} b.report{single_threads} end usersystemtotalreal 0.2400000.1100000.350000(3.659640) 0.2700000.1200000.390000(14.167703)在网络请求时程序发生了阻塞,而这些阻塞在Ruby的运行下是可以并行的,所以在耗时上大大缩短了.
GIL的思考
那么,既然有了这个GIL锁的存在,是否意味着我们的代码就是线程安全了呢?
很遗憾不是的,GIL在ruby执行中会某一些工作点时切换到另一个工作线程去,如果共享了一些类变量时就有可能踩坑.
那么,GIL在ruby代码的执行中什么时候会切换到另外一个线程去工作呢?
有几个明确的工作点:
- 方法的调用和方法的返回,在这两个地方都会检查一下当前线程的gil的锁是否超时,是否要调度到另外线程去工作
- 所有io相关的操作,也会释放gil的锁让其它线程来工作
- 在c扩展的代码中手动释放gil的锁
- 还有一个比较难理解,就是rubystack进入cstack的时候也会触发gil的检测
一个例子
@a=1 r=[] 10.timesdo|e| Thread.new{ @c=1 @c+=@a r<<[e,@c] } end r ##[[3,2],[1,2],[2,2],[0,2],[5,2],[6,2],[7,2],[8,2],[9,2],[4,2]]上述中r里虽然e的前后顺序不一样,但是@c的值始终保持为2,即每个线程时都能保留好当前的@c的值.没有线程简的调度.
如果在上述代码线程中加入可能会触发GIL的操作例如puts打印到屏幕:
@a=1 r=[] 10.timesdo|e| Thread.new{ @c=1 puts@c @c+=@a r<<[e,@c] } end r ##[[2,2],[0,2],[4,3],[5,4],[7,5],[9,6],[1,7],[3,8],[6,9],[8,10]]这个就会触发GIL的lock,数据异常了.
小结
Web应用大多是IO密集型的,利用Ruby多进程+多线程模型将能大幅提升系统吞吐量.其原因在于:当Ruby某个线程处于IOBlock状态时,其它的线程还可以继续执行,从而降低IOBlock对整体的影响.但由于存在RubyGIL(GlobalInterpreterLock),MRIRuby并不能真正利用多线程进行并行计算.
PS.据说JRuby去除了GIL,是真正意义的多线程,既能应付IOBlock,也能充分利用多核CPU加快整体运算速度,有计划了解一些.
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。