跳到主要内容

Ruby 的多线程

本章节让我们来学习 Ruby 的多线程。您将会了解到:什么是多线程,Ruby 中如何创建线程等知识。

1. Ruby 中的线程

通俗一点来讲,线程可以让程序同时执行多项操作。

比如:读取多个文件、处理多个请求、建立多个API连接。多线程可以更好地利用CPU的核心,CPU的一个核好比一个普通人,一个普通人只能干一件事,多个人可以分开干不同的事或干很多次同样的事。

注意事项:

在MRI(Matz 的 Ruby 解释器)中,这是运行 Ruby 应用程序的默认方式,只有在运行 I/O 绑定的应用程序时,您才能从线程中受益。由于存在 GIL(Global Interpreter Lock,是由编程语言解释器线程持有的互斥锁,以避免与其他线程共享不是线程安全的代码。),因此存在此限制。对于一般的 Ruby 和 Python 应用,即使在多核处理器上运行,使用 GIL 的解释器始终总是允许一次仅执行一个线程。

每个进程都有至少一个线程,您可以按需创建更多线程。

2. I/O 绑定应用程序

首先,我们需要讨论 CPU 绑定和 I/O 绑定应用程序之间的区别。

I/O 绑定应用程序是需要等待外部资源的应用程序:

  • API请求;
  • 数据库(查询结果);
  • 磁盘读取。

线程可以在等待资源可用时决定停止。

这意味着另一个线程可以运行并执行其任务,而不会浪费时间等待。

I/O 绑定应用程序的一个示例是 Web 爬虫(crawler)。

对于每个请求,爬虫都必须等待服务器响应,并且在等待时它什么也不能做。

您可以一次发出4个请求,并在它们返回时处理响应,这将使您更快地获取页面。

2.1 创建一个线程

您可以通过调用Thread.new创建一个新的Ruby线程。确保传递带有该线程需要运行的代码的块。

实例:

Thread.new { puts "hello from thread" }

# ---- 输出结果 ----

是不是很简单。

但你会发现,线程没有输出内容,这是因为Ruby 不等待线程完成

您需要在线程上调用join方法来修复上面的代码。

实例:

Thread.new { puts "hello from thread" }.join

# ---- 输出结果 ----
hello from thread

如果要创建多个线程,可以将它们放入数组中,并在每个线程上调用join

实例:

Thread.new { puts "hello from thread1" }.join
Thread.new { puts "hello from thread2" }.join
Thread.new { puts "hello from thread3" }.join

# ---- 输出结果 ----
hello from thread1
hello from thread2
hello from thread3

学习Ruby的线程时,我们要多参考 Ruby 线程的文档

2.2 线程与异常

如果线程内发生异常,它将在不停止程序或不显示任何错误消息的情况下静默死。

实例:

Thread.new { raise 'hell' }

# ---- 输出结果 ----

为了进行调试,您可能希望程序在发生不良情况时停止运行。

为此,您可以将 Thread 上的以下标志设置为 true:

Thread.abort_on_exception = true

在创建线程之前,请确保设置此标志。

实例:

Thread.abort_on_exception = true
Thread.new { raise 'hell' }
sleep(1)
# ---- 输出结果 ----
ruby.rb:2:in `block in <main>': hell (RuntimeError)

**注意事项:**这里需要增加sleep(1),否则不会抛出异常。

2.3 线程池

假设您要处理数百个项目,为每个项目启动一个线程将破坏您的系统资源。

它看起来像这样:

pages_to_crawl = %w( index about contact ... )
pages_to_crawl.each do |page|
Thread.new { puts page }
end

如果这样做,您将与服务器启动数百个连接,因此这可能不是一个好主意。

一种解决方案是使用线程池。线程池使您可以在任何给定时间控制活动线程的数量。

您可以建立自己的池,但是我不建议你这样去做,Ruby有一个Gem可以为您完成这个操作。

实例:

require 'celluloid'
class Worker
include Celluloid
def process\_page(url)
puts url
end
end
pages_to_crawl = %w( index about contact products ... )
worker_pool = Worker.pool(size: 5)
# If you need to collect the return values check out 'futures'
pages_to_crawl.each do |page|
worker_pool.process\_page(page)
end

这次只有5个线程在运行,完成后他们将选择下一个项目。

2.4 资源竞争风险

您必须知道并发代码存在一些问题,例如,线程容易出现资源竞争状况,比如同一时刻操纵了一个变量。竞争条件是当事情发生混乱并弄乱时。

另一个问题是死锁deadlock)这是当一个线程拥有对某个资源的独占访问权(使用互斥锁(mutex)之类的锁定系统)而从未释放它时,这使得所有其他线程都无法访问它。

为避免这些问题,最好避免使用原始线程,并坚持使用一些已经为您处理好细节的Gem。

3. 小结

本章节中我们学习到了如何使用 Ruby 来创建一个线程。如何让创建的线程抛出异常,线程池是什么,线程中存在的风险有什么。