Wait the light to fall

线程

焉知非鱼

RFC 1, 作者 Bryan C. Warnock: 线程

在 Perl 中对真正的多线程架构的需求, 可能是, 也可能不是, 这才是最初被称为简单的 Perl, 然后是 Perl 6, 最后是 Raku 的真正动机。

大概是在 90 年代末或 10 年代初, 我们与一家大公司签订了合同, 该公司需要快速从网上下载东西。我们需要这些线程, 而它们最终在 Perl 5.8.8 中出现了。然而, 我们的线程是非常基本的, 不需要任何形式的通信, 只是光秃秃的并行的东西, 在它们下面, 使用的是操作系统进程, 在 Perl 虚拟机层面没有真正的线程。而它们是非常需要的。这就是为什么 RFC1 的由来:

Perl 中线程的实现

它最初是在8月1日提出的(所以才有了20周年纪念日的事情), 几个月后, 到9月28日终于被冻结了。

它基本上提出了一种实现低级线程的方法, 包括新的命名空间(全局的, 用于线程之间共享变量)以及 Threads 类, 有这个例子:

use Threads;
# the main thread has all four above in its arena

my $thread2 = Threads->new(\&start_thread2);
#...

sub start_thread2 { ... }

主线程是隐式的, 把其他的模块都拿到它的命名空间, 第二个模块继承主线程。总的来说是有道理的, 只不过这是一种很低级的使用线程的机制, 其实它看起来更像是一种处理进程的方式, 而不是我们现在所说的线程。还有另外一个 RFC 就是针对这些的, 它叫做“轻量级线程”, 这个 RFC 是在几周后开始的, 几乎在同一时间被冻结。它包含了图文并茂的比喻。

Perl → 瑞士军刀工具链; 带线程的 Perl → 改动工具链

除了明确的变量共享和它使用 Thread 而不是 Threads 作为主类之外, 很难看出它们之间有什么区别。

最终, Raku 的线程就选择了这个关键词: Thread。这使用 new 来创建一个线程, 但你必须发出一个 .run 来实际运行它。另外, 你也可以简单地使用 .start 来立即创建和运行一个线程。

#!/usr/bin/env raku

constant $interval = 100000;

my @threads = (^10).map: -> $i {
    Thread.start(
        name => "Checking primes from {$i * $interval } to { ($i+1)*$interval}",
        sub {
            for ($i * $interval)..^(($i+1)*$interval) -> $n {
		          next if ( $n %% 2 ) | ( $n %% 3 ) | ($n %% 5 );
		          say "Prime $n found in $*THREAD" if $n.is-prime;
	        }
        },
    );
}

.finish for @threads;

这几乎是直接从线程手册页面的例子中提取出来的, 显示了 Raku 和它早期的雏形之间的差异。它使用 map 来启动10个线程(使用 Range);每个线程都会在一个数字范围内工作, 检查当时是否有质数。在抄出几个简单的之后, 它将简单地使用 is-prime 函数检查, 如果这个数字是质数, 它将打印出这个数字和它所在的线程。通过 $*THREAD 变量可以很方便的内省出自己所处的线程, 这样就会打印出这样的结果:

...
Prime 76579 found in Thread<4>(Checking primes from 0 to 100000)
Prime 994997 found in Thread<13>(Checking primes from 900000 to 1000000)
Prime 655043 found in Thread<10>(Checking primes from 600000 to 700000)
Prime 483991 found in Thread<8>(Checking primes from 400000 to 500000)
Prime 169283 found in Thread<5>(Checking primes from 100000 to 200000)
Prime 995009 found in Thread<13>(Checking primes from 900000 to 1000000)
Prime 761533 found in Thread<11>(Checking primes from 700000 to 800000)
...

每个线程在设计上, 都有专门的特定范围, 比如13号线程从900K到1000K。用线程工作的效率要高得多, 但一个进程需要钉在一个特定的线程上才行。这就是为什么低级线程访问并不是创建并发程序的最佳方式。使用更高级别的 API 工作更有意义。

然而, 在2000年的时候, 就已经有了这样的见解:对于像 Raku 这样的现代百年语言, 需要一个线程引擎。而因为 Warnock’s Dilemma 而成名的 Bryan C. Warnock, 即使没有最初想法的洞察力, 至少在20年前的今天, 懒惰、急躁和狂妄地把它写进了最终成为 Raku 的第一个 RFC 中。

Warnock 的困境的起源, 根据维基百科, 差不多是在同一个月, 其实起源于 bootstrap(for perl6) 邮件列表。而这完全与那个 RFC 的响应不热烈有关, 这说明要么没人关心, 要么就是它太完美了。我倾向于后者, 所以谢谢你, Bryan。