理念分析概述

在 Rust 中由于语言设计理念、安全、性能的多方面考虑,并没有采用 Go 语言大道至简的方式,而是选择了 多线程与 async/await 相结合

  • 优点:可控性更强、性能更高
  • 缺点:复杂度不低
  • 使用复杂度换取可控性和性能

惯用写法有很多。系统程序员常用的设计理念如下

  • 后台线程(background thread):一个后台线程只负责一件事,而且 周期性“醒来”去做这件事
  • 线程池(worker pool):通过任务队列与客户端通信
  • 管道(pipeline):将数据从一个线程导入另一个线程,每个线程只做一小部分工作
  • 数据并行(data parallelism):假设(不管正确与否)整个计算机主要用于一项大型计算,这个大型计算进而又拆分成 n 个小任务,在 n 个线程上执行,希望所有 n 个机器的核心同时工作
  • 同步对象海(sea of synchronized object):多个线程拥有同一数据权限,使用基于互斥量等低级原语的临时锁方案避免争用
  • 原子整数操作(atomic integer operation):允许多核心通过以一个机器字大小的字段传递信息而实现通信

总结来说,主要 使用 Rust 线程 的 3 种方式

  • 并行分叉-合并:线程的基本执行流程
  • 管道:线程的通信(消息传递)
  • 共享可修改状态:线程的同步

并行分叉–合并:线程执行流程

Rust 多线程的理念 - 图1

使用分叉–合并手段多线程处理文件。这个模式就叫作并行分叉–合并。分叉( fork )就是启动一个新线程,而合并( join )就是等待线程完成。并行分叉–合并有如下优点

  • 非常简单。分叉–合并很容易实现,在 Rust 中也很容易正确处理
  • 避免瓶颈。分叉–合并过程中不涉及给共享资源加锁。只有在任务结束后,一个线程才需要等待另一个线程。在执行任务期间,每个线程都可以自由运行。这样可以保证降低任务切换的开销
  • 性能计算大致直观。在最好的情况下,启动 4 个线程可以只用四分之一时间完成任务
  • 容易推断程序是否正确。只要线程之间是真正隔离的,分叉–合并程序就是确定性的

分叉–合并的主要缺点是要求工作单元隔离

管道(channel):线程通信(消息传递)

管道(channel)是把值从一个线程发送到另一个线程的单向管道(发送值、接收值)。换句话说,它是一个线程安全的队列。下图中展示了通道的用法。通道与 Unix 中的管道有点类似,都是一端发送数据,另一端接收数据,而且这两端通常由不同的线程所有。但 Unix 管道发送的是字节,而通道发送的是 Rust 值。sender.send(item) 把一个值放进通道, receiver.recv() 则移除一个值。所有权也从发送线程转移到了接收线程。如果通道是空的, receiver.recv() 则会一直阻塞到有值发送

Rust 多线程的理念 - 图2

使用通道,线程可以通过传值实现通信。这是线程间协作的一种很简单的方式,无须使用锁或者共享内存。下图展示了一个 Unix 管道的示例,其中涉及的所有 3 个程序是完全可以同时运行的。Rust 通道比 Unix 管道快。发送值是转移而不是复制,而转移的值即使数据结构包含几兆数据也是很快的

Rust 多线程的理念 - 图3

Rust 通道是经过认真优化的,在刚创建通道时,Rust 使用的是“一次性”队列实现。如果只是用这个通道发送一个对象,那可以保证开销最小。如果发送第二个值,Rust则会切换到一个不同的队列实现。这个实现会从长远考虑,准备让通道传输很多值,同时又保持分配开销最小化。如果你选择克隆 Sender ,Rust 则必须回退到另一个实现,该实现可以保证多个线程同时发送值时的安全。不过即使是这3个实现中最慢的实现也是没有锁的队列,因此发送和接收值最多只是几个原子操作,涉及一次堆内存分配,外加转移自身。只有在队列为空且接收线程因此需要休眠时才需要系统调用。当然,此时经过通道的流量无论如何也不是最大的

共享可修改状态:线程同步

互斥量(或者叫锁)用于强制多线程依次访问特定的数据。互斥量的作用体现在以下几方面

  • 防止数据争用,即避免多个线程并发读写同一块内存
  • 即使没有数据争用,即使所有读写在程序中都是顺序执行,如果没有互斥量,不同线程的操作也可能以任意方式相互交错
  • 互斥量支持通过 不变性(invariant)编程,即受保护数据由你负责初始化但由每个临界区来维护的规则