6.1 并发

Loko 中的并发存在于两个不同的层面:

当 Loko 启动时,它会立即为调度器(也称为 pid 0)设置一个 Loko 进程。引导加载器已经为其创建了堆和栈。调度器负责启动 pid 1,处理中断,管理抢占,进程间的消息传递和其他维护任务。第一个启动的调度器还负责引导系统中所有其他处理器。

目前所有其他处理器在引导后都会启动但在初始化后停止。需要做一些工作来为它们分配新的调度器进程。

除了调度器和带有纤程的 Loko 进程之外,还有拥有自己页表的普通用户模式进程。你可以将所有 FORTRAN 和 C 程序放在这里。

纤程是基于 Concurrent ML 的轻量级并发系统。其实现深受 Andy Wingo 的文章 Growing FibersA New Concurrent ML 的启发。Concurrent ML 构成了并发的基本原理,可用于实现更高级的并发,如 Go 通道或 Erlang 进程。

有关如何在程序中使用纤程的详细信息,see 纤程。你还可以在某种程度上参考 Guile fibers 手册;其中包含大量背景信息。

Loko 中的实现与 Guile fibers 有以下不同之处:

XXX:上述实现存在一个损坏的 dynamic-wind。请参阅 bug 跟踪器中的 issue #26。

API 与 Guile fibers 兼容,因此为 Guile fibers 编写的程序的并发部分应该可以在 Loko fibers 下工作。最大的例外是 Guile 的 (fibers internals) 库,它管理纤程调度器。Loko 每个 Loko 进程只有一个调度器。另一个区别是 Loko 上的 I/O 默认是非阻塞的。

纤程之所以吸引人的很大一部分原因是代码可以像非并发那样编写。你可以自由地读写管道和网络流,而无需显式处理数据何时可用或文件描述符何时可写入的轮询。其中一个示例程序是一个小型 web 服务器,为每个连接的客户端创建一个纤程。

Linux 上的 Loko 会注意在文件描述符上设置 O_NONBLOCK,并在系统调用返回 EAGAIN(也称为 EWOULDBLOCK)时挂起当前纤程。Loko 使用 epoll 来发现文件描述符何时就绪。

但 Linux 不会对常规文件实现 EAGAIN,因此从文件读取可能会阻塞。当这种情况发生时,它会阻止其他纤程运行。标准 I/O 是非阻塞的,但使用同一终端的其他程序可能会因此感到困惑和/或将其关闭。在 Linux 上,即使是内存访问也可能阻塞,因为页面可能被交换到磁盘(关闭 swap 不是好主意)。你的程序二进制文件在启动时由 Linux mmap 加载,未修改的页面可以从磁盘读回,因此 Linux 可以自由地从内存中逐出这些页面。所以在 Linux 上拥有任何可靠的响应程序是具有挑战性的。如果磁盘出现任何问题,所有进程都可能进入不可中断的睡眠状态。

裸机硬件上的 Loko 没有任何操作会阻塞其他纤程运行。

6.1.1 为什么纤程不可抢占

这是解释。Loko 进程可以临时使用寄存器,使其包含任意位模式。如果这样的寄存器被保存到续延对象中,垃圾回收器将无法处理。同一 Loko 中的其他纤程共享同一个堆,因此纤程不能像 Loko 进程被抢占时那样简单地挂起和搁置。

这个问题的一个常见解决方案是编译器在代码的各个位置插入计数器。这些计数器在代码的安全点递增,用于实现基于软件的定时器中断。此解决方案会带来一些开销,并且需要特别注意以免破坏紧密循环的性能。除非找到其他解决方案,否则这可能会在以后实现。

6.1.2 Loko 进程

目前进程的用例相当有限,但它们是获得抢占式并发的唯一方式。

Linux 上的 Loko 目前有一个非常初级的调度器,无法处理多个进程。