并发与并行
并发(concurrency)和并行(parallelism)是不同的两个概念。举例:2个任务在单核的CPU上运行,互相交替执行直到结束,这叫并发;2个任务在多核的CPU上执行,同一时刻2个任务都在运行,这叫并行。
多进程与多线程
进程与线程的比较,各有优缺点:
- 内存使用:进程比线程多
- 僵尸实例:如果父进程比子进程先退出,子进程会成为僵尸进程;如果父进程退出,包含的线程都会被杀掉,不存在僵尸线程
- 上下文切换:进程比线程开销大
- 内存共享:进程有自己的内存空间,进程间隔离;进程内的线程间共享内存,需要处理内存的并发访问和修改
- 交互:进程间交互通过IPC机制;线程间通过队列或共享内存交互
- 管理效率:进程的创建和销毁比线程要慢
- 调试效率:进程比线程要更容易调试
Ruby中的进程使用
Ruby中通过fork系统调用获得当前进程的一个副本,如:
100.times do |i|
fork do
Mailer.deliver do
from "eki_#{i}@eqbalq.com"
to "jill_#{i}@example.com"
subject "Threading and Forking (#{i})"
body "Some content"
end
end
end
Process.waitall
Process.waitall是指等待所有的子进程都执行完成再退出。fork do..end会创建个新进程,并将block在其中执行。
Ruby中的线程使用
Ruby中通过Thread.new创建新的线程,如:
threads = []
100.times do |i|
threads << Thread.new do
Mailer.deliver do
from "eki_#{i}@eqbalq.com"
to "jill_#{i}@example.com"
subject "Threading and Forking (#{i})"
body "Some content"
end
end
end
threads.map(&:join)
如果是在MRI上执行上述代码,消耗的时间和同步方式的结果是基本一致的。如果是在JRuby这种没有GVL的Ruby解释器上执行,线程会起到效果。究其原因,使用MRI时,不管是单核还是多核的CPU,执行Ruby代码时候,进程内同一个时刻只能有一个线程持有这个GVL锁。
Ruby中的多进程gem
- Resque:基于redis的Ruby库,用于创建后台任务,将任务置于多个队列上,后续执行。
- Unicorn:支持Rack应用的HTTP服务器,特点是低延迟、高吞吐。有用到Unix的内核特性。
Ruby中的多线程gem
- Sidekiq:功能齐全的处理后台任务Ruby库,易于与Rails应用整合,性能好。
- Puma:支持并发的Web服务器。
- Thin:既快又简单的Web服务器。
Ruby中选进程还是线程?
进程的优点是简单,主要成本是内存消耗较大(尤其解释器没有开启CoW的时候),而且也要考虑父子进程间共享的文件描述符、信号量这些。
是否选择多线程,也取决于使用了哪种Ruby实现。针对没有GVL的Ruby实现,应用多线程结合线程池是个不错的选择。针对MRI,如果程序中IO访问占比较多的话,使用多线程还是可以提高吞吐量的。