Ruby 黑客指南/线程
仔细想想,我还没有展示在实践中使用 Ruby 线程的例子。现在只是个简单的介绍
Thread.new {
while true
puts 'from thread'
end
}
while true
puts 'from main'
end
如果您执行此程序,您应该在输出中看到“来自线程”和“来自主线程”混合在一起。
当然,除了创建多个线程之外,还有很多方法可以控制它们。没有像 Java 中那样的synchronize
关键字,但提供了像Mutex
、Queue
和Monitor
这样的常用原语,下面的 API 可以用于对线程本身的操作。
Thread.pass
- 将执行传递给另一个线程
Thread.kill(th)
- 结束线程th
Thread.exit
- 结束此线程
Thread.stop
- 暂时暂停此线程
Thread#join
- 等待接收线程结束
Thread#wakeup
- 恢复先前暂停的线程
乍一看,线程似乎都是一起运行的,但实际上它们是轮流执行的,每个线程都运行一小段时间。严格来说,在多 CPU 机器上,可以同时运行多个线程,但即使如此,如果线程数超过 CPU 数量,线程也必须轮流运行。
Ruby 仍然有一个 GIL(全局解释器锁)。由于这个锁,ruby 解释器严格来说一次只能运行一个线程。但是,当一个线程被阻塞(例如,等待网络数据到达)时,解释器可以在阻塞线程等待时切换到另一个线程。目前,如果您想用 ruby 真正同时运行多个线程,您将不得不运行多个解释器。这种技术通常被 unicorn 等 Web 服务器使用。已经做了很多工作来减轻 GIL 的影响,将来它可能会完全消失。不过,对于大多数目的来说,目前的情况已经足够了。
现在我们将更详细地讨论 Ruby 线程的特性。在讨论线程时,可以讨论它们是否是抢占式的。
在抢占式线程系统中,即使线程用户没有明确切换线程,线程也会自行切换。反过来看,线程切换的时间无法由用户控制。
另一方面,在非抢占式线程系统中,只要线程用户没有明确地说“你现在可以将控制权传递给下一个线程”,线程就不会切换。反过来看,很明显线程的用户可以控制线程可以切换的位置。
这种区别也适用于进程。在这种情况下,抢占式被认为是“优越”的方法。例如,如果一个程序存在导致它陷入无限循环的错误,进程将无法切换。换句话说,一个用户程序可能会锁住整个系统;这不是一件好事。Windows 3.1 以 MS-DOS 为基础,因此它的进程切换是非抢占式的,但 Windows 95 是抢占式的。因此,Windows 95 更加健壮,可以说 Windows 95 比 Windows 3.1 “优越”。
那么 Ruby 线程是什么样的呢?在 Ruby 级别,线程是抢占式的,而在 C 级别,线程是非抢占式的。换句话说,在编写 C 代码时,您可以几乎精确地指定线程切换的时间。
为什么 Ruby 线程是这样的?线程确实很方便,但在使用它们时必须考虑一些因素。也就是说,代码必须适应线程(代码必须是线程安全的)。也就是说,如果在 C 级别抢占线程切换,我们使用的所有 C 库都必须是线程安全的。
然而,实际上还有很多 C 库还没有线程安全。如果我们通过将线程安全作为要求来减少可以使用库的数量,那么为使扩展库易于编写而付出的所有努力将毫无意义。因此,对于 Ruby 来说,在 Ruby 级别使线程非抢占式是理性的选择。
我们了解到,在 C 级别,Ruby 线程是非抢占式的。也就是说,您的线程运行一段时间后,它会自愿放弃对另一个线程的控制。因此,让我们考虑一个即将停止运行的执行线程。它应该将控制权传递给哪个线程?不,首先我们需要知道 Ruby 线程在内部是如何表示的。让我们看看管理线程的变量和数据结构。
▼ 线程管理结构
864 typedef struct thread * rb_thread_t;
865 static rb_thread_t curr_thread = 0;
866 static rb_thread_t main_thread;
7301 struct thread {
7302 struct thread *next, *prev;
(eval.c)
由于各种原因,struct thread
变得非常大,因此我们在此重点关注重要部分。仅查看这两个成员next
和prev
(它们都是rb_thread_t
结构),您可能会认为rb_thread_t
是一个双向链表。但实际上,它不仅仅是一个双向链表;它的两端相遇。换句话说,它是一个循环双向链表。这是一个重要的点。当您添加静态变量main_thread
和curr_thread
时,整个数据结构看起来像图 1。
图 1:管理线程的数据结构
main_thread
是程序启动时存在的线程。换句话说,它是“第一个”线程。curr_thread
当然是当前线程;也就是说,当前正在运行的线程。main_thread
的值在整个进程操作过程中不会改变,但curr_thread
的值会迅速改变。
以这种方式形成循环的线程,选择下一个线程很简单:只需沿着next
链接并选择该线程即可。仅凭这一点,您就可以在一定程度上均匀地运行所有线程。