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 来创建一个线程。如何让创建的线程抛出异常,线程池是什么,线程中存在的风险有什么。