跳转到内容

Ruby 黑客指南/线程

来自维基教科书,开放的书籍,开放的世界

Ruby 接口

[编辑 | 编辑源代码]

仔细想想,我还没有展示在实践中使用 Ruby 线程的例子。现在只是个简单的介绍

Thread.new {
  while true
    puts 'from thread'
  end
}
while true
  puts 'from main'
end

如果您执行此程序,您应该在输出中看到“来自线程”和“来自主线程”混合在一起。

当然,除了创建多个线程之外,还有很多方法可以控制它们。没有像 Java 中那样的synchronize关键字,但提供了像MutexQueueMonitor这样的常用原语,下面的 API 可以用于对线程本身的操作。

线程 API

[编辑 | 编辑源代码]

Thread.pass - 将执行传递给另一个线程
Thread.kill(th) - 结束线程th
Thread.exit - 结束此线程
Thread.stop - 暂时暂停此线程
Thread#join - 等待接收线程结束
Thread#wakeup - 恢复先前暂停的线程

Ruby 线程

[编辑 | 编辑源代码]

乍一看,线程似乎都是一起运行的,但实际上它们是轮流执行的,每个线程都运行一小段时间。严格来说,在多 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 变得非常大,因此我们在此重点关注重要部分。仅查看这两个成员nextprev(它们都是rb_thread_t结构),您可能会认为rb_thread_t是一个双向链表。但实际上,它不仅仅是一个双向链表;它的两端相遇。换句话说,它是一个循环双向链表。这是一个重要的点。当您添加静态变量main_threadcurr_thread时,整个数据结构看起来像图 1。

图 1:管理线程的数据结构

main_thread 是程序启动时存在的线程。换句话说,它是“第一个”线程。curr_thread 当然是当前线程;也就是说,当前正在运行的线程。main_thread 的值在整个进程操作过程中不会改变,但curr_thread 的值会迅速改变。

以这种方式形成循环的线程,选择下一个线程很简单:只需沿着next链接并选择该线程即可。仅凭这一点,您就可以在一定程度上均匀地运行所有线程。

华夏公益教科书