[ruby] Ruby中的并发与并行

511 阅读3分钟

并发与并行

并发(concurrency)和并行(parallelism)是不同的两个概念。举例:2个任务在单核的CPU上运行,互相交替执行直到结束,这叫并发;2个任务在多核的CPU上执行,同一时刻2个任务都在运行,这叫并行。

多进程与多线程

进程与线程的比较,各有优缺点:

  1. 内存使用:进程比线程多
  2. 僵尸实例:如果父进程比子进程先退出,子进程会成为僵尸进程;如果父进程退出,包含的线程都会被杀掉,不存在僵尸线程
  3. 上下文切换:进程比线程开销大
  4. 内存共享:进程有自己的内存空间,进程间隔离;进程内的线程间共享内存,需要处理内存的并发访问和修改
  5. 交互:进程间交互通过IPC机制;线程间通过队列或共享内存交互
  6. 管理效率:进程的创建和销毁比线程要慢
  7. 调试效率:进程比线程要更容易调试

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

  1. Resque:基于redis的Ruby库,用于创建后台任务,将任务置于多个队列上,后续执行。
  2. Unicorn:支持Rack应用的HTTP服务器,特点是低延迟、高吞吐。有用到Unix的内核特性。

Ruby中的多线程gem

  1. Sidekiq:功能齐全的处理后台任务Ruby库,易于与Rails应用整合,性能好。
  2. Puma:支持并发的Web服务器。
  3. Thin:既快又简单的Web服务器。

Ruby中选进程还是线程?

进程的优点是简单,主要成本是内存消耗较大(尤其解释器没有开启CoW的时候),而且也要考虑父子进程间共享的文件描述符、信号量这些。

是否选择多线程,也取决于使用了哪种Ruby实现。针对没有GVL的Ruby实现,应用多线程结合线程池是个不错的选择。针对MRI,如果程序中IO访问占比较多的话,使用多线程还是可以提高吞吐量的。

References

Ruby concurrency and parallelism: a practical tutorial