掌握并发

感谢作者 Thijs Cadier 原文链接

你的应用会同时被多个用户所使用而你也想使你的应用尽可能得快。于是乎,你需要某种方法来处理并发。但你无需恐惧,大部分的 Web 服务器默认都替我们处理好了。但当你需要扩展应用时,你会希望以最有效的方式来利用并发。

不同类型的并发

并发有很多种处理方式:多进程,多线程以及事件驱动。它们各有优劣。通过本文,你将能了解到它们的不同之处以及使用时机。

多进程 (Unicorn)

这是处理并发的最简单方式。一个 master process 通过 fork 自身来操作多个 worker processes。worker processes 是真正用来处理请求的,而 master process 则负责管理 worker processes。

multi-process

每个 worker process 都在内存中拥有完整的代码拷贝,这使得多进程这种方式非常地耗内存并且难以扩展。

多进程总结
用例

你知道的一个非 Ruby 的例子大概是 Chrome 浏览器。它就是利用多进程架构使每一个标签页都拥有一个进程。它能在不致使整个应用挂掉的情况下,允许单个标签页崩溃。在 Chrome 的例子中,多进程架构还有助于单个标签页间的分离。

优势
  • 最易实现
  • 无需考虑线程安全
  • 单个 worker process 的崩溃不会危及其余部分
劣势
  • 每个进程都会在内存中载入完整的代码库导致其会消耗大量内存。因此,无法将其扩展至能够有效应对海量的并发。

多线程 (Puma)

这种线程模式允许单个进程能够在同时处理大量请求。它通过在单个进程中运行多个线程来实现。

与多进程模式相反,所有的线程都运行于同一个进程中。这意味着它们共享诸如全局变量之类的数据。因此,每个线程都只会占用一小块额外的内存。

threaded

全局锁

这就将我们引至了 MRI 的全局锁 (GIL) 的领域。 全局锁是所有 Ruby 代码执行时都会有的锁。即使我们的线程以并行方式运行,GIL 会确保同时能够激活的线程只有一个。

IO 的运行独立于全局锁之外。当你执行了一个数据库查询等待结果返回时,它不会被锁住。另一个线程有机会去完成其他任务。如果你在线程中有许多数学运算或是很多关于数组,哈希的操作,并且你使用的还是 MRI 的话,那你将只能利用单核的性能。在大多数情况下,你仍然需要多进程来充分利用你机器的性能。或者你可以使用没有全局锁的 Rubinius 或 jRuby。

线程安全

如果你使用多线程的话,你需要小心的编写代码使它们以线程安全的方式来操作共享的数据。举例来说,你可以通过使用 Mutex 在你操作前锁定共享的数据结构。这样可以确保在你改变数据,其他线程工作时所依赖的数据仍然是最新的。

多线程总结
用例

这是个“中间路线”的选项。它被大量用于那些需要处理有大量 short requests 负载的标准 Web 应用(像是繁忙的 Web 应用)。

优势

与多进程相比占用内存更小

劣势
  • 你务必确保你的代码是线程安全的。
  • 如果一个线程崩溃了,它有可能会导致整个进程挂掉。
  • 全局锁会锁定除了 IO 之外的所有操作。

事件循环 (Thin)

当你需要做许多并发 IO 操作时就会用到事件循环。这个模式本身并不强制要求同时处理多个请求,但是它却是一种处理大并发用户数的有效方式。

下面你会看到一段用 Ruby 写的非常简单的事件循环代码。这个循环会从 event_queue 中取出事件并处理掉。如果没有需要处理的事件,它就会休眠然后再次检查队列中是否有待处理的事件。

loop do
   if event_queue.any?
     handle_event(event_queue.pop)
   else
     sleep 0.1
   end
 end
图表版的说明

在这份图表中,我们会对其做更深入的了解。事件循环现在与操作系统,队列和内存一起上演了一段美妙的舞蹈。

event-loops

分步讲解
  1. 操作系统保持对网络和磁盘可用性的追踪。
  2. 当操作系统认为 I/O 已准备好时,它会向队列发送一个事件。
  3. 队列由一系列的事件构成。事件循环会从中选取最上面的一个。
  4. 事件循环处理事件
  5. 事件循环使用内存来存储关于连接的元数据。
  6. 它可以直接再次向事件队列发送事件。例如,一个基于事件内容的关闭该队列的消息。(For example, a message to shut down the queue based on the contents of an event. 贴个原文,这句看着好绕)
  7. 如果它需要做一个 I/O 操作。它会告诉操作系统,它对一个特定的 I/O 操作感兴趣。操作系统保持对网络和磁盘可用性追踪的同时,在 I/O 就绪时,添加一个事件。
事件循环总结
用例

你的用户会用到大量并发连接时。思考诸如 Slack 这类服务。或是 Chrome 的通知功能。

优势
  • 几乎没有连接会造成过高的内存开销
  • 能扩展成处理大数量级的并行连接
劣势
  • 该模式难以理解
  • 每批次的处理数量必须小且可预测以避免队列的过快增长

你该用哪种呢?

我们希望这篇文章已使你能更好的理解了不同的并发模型。对于开发者而言,这可能是个较难领悟的主题。但理解并发模型将使你有正确的工具来进行实验并能够正确的配置你的应用。

总结
  • 对绝大多数应用而言,多线程是合情合理。 Ruby/Rails 生态圈看起来正缓慢的向这个方向前行。
  • 如果你运行的是带有 long-running streams 的高并发应用,事件循环允许你能够扩展。
  • 如果你没有大流量的站点或是你觉得你的子进程会崩溃就选择老派的多进程。

另外,在多进程多线程中的一个线程内运行一个事件循环也是可能的。噢,是的,你还可以有你自己的玩法,尝试吧!