大家好,我是梦兽。一个 WEB 全栈开发和 Rust 爱好者。如果你对 Rust 非常感兴趣,可以关注梦兽编程公众号获取群,进入和梦兽一起交流。
Rust 拥有一个强大的特性,称为通道(channels)。通道是一种在不同线程之间发送数据的方式。你可以将通道想象成一根管道:管道的一端发送数据,而另一端接收数据。
这是本文的第一部分,在这里我将解释基本概念:通道是什么以及典型的通道编程模式。

cdn-images-1.readmedium.com/v2/resize:f…
「关于作者的几句话」
自 2019 年起,我便开始在关键任务生产环境中使用 Rust,包括高性能和实时应用。主要为工业物联网项目提供解决方案,特别是在高能和其他重工业领域。
「什么是通道?」
通道存在于许多编程语言中。正如前面提到的,从概念上讲,通道就像是一个管道。它有两个端点:生产者和消费者。生产者向通道发送数据,而消费者从中接收数据。数据通常是按照先进先出(FIFO,First-In, First-Out)的顺序发送的。某些通道实现可以支持多个生产者或消费者。
从技术角度来看,通道是一个带有锁的数据缓冲区,以及在其后用于放入和获取数据的方法。这个锁用于同步对缓冲区的访问。
当没有消费者存在时,生产者会被拒绝发送数据。当没有生产者存在时,消费者允许获取数据直到缓冲区为空。之后,通道被认为是“关闭”的。
流行的通道实现包括:
- 标准 Rust Channel:作为 Rust 标准库的一部分。经过充分测试且可靠,用途广泛,适用于大多数情况。
- Crossbeam channels. 非常适合高性能应用。
- Flume. 一种高性能的通道实现,与 Crossbeam 类似。它同时支持同步和异步通信方法。
- Tokio channels. 作为 Tokio 库的一部分。专为异步应用设计。
- async_channel. 一种高性能的异步通道实现。
- 针对特定用途的专业通道(例如实时通信、嵌入式系统等)。 RoboPLC 项目,它专注于实时应用的开发。这会在第二部分介绍它。
「一次性通道」
一次性通道是一种只能用于发送单一值的通道。它具有不同的编程实现,更像是一个一次性信号。由于一次性通道具有不同的用途,本文不会重点介绍它们,除非有机会撰写第三部分。
「典型的通道编程模式」
「常见模式」
通道可以是有限制容量的(bounded)或无限制容量的(unbounded)。
- 「有限制容量的通道」:具有有限的缓冲区容量,当缓冲区满时,生产者会被阻塞或拒绝发送数据。这是使用通道最安全的方式,但如果使用不当可能会导致死锁。
- 「无限制容量的通道」:具有无限的缓冲区容量。生产者永远不会被阻塞,始终可以发送数据。这可能导致内存耗尽,如果生产者的速度超过了消费者的处理速度。应该谨慎使用。
在大多数实现中,创建后的通道会被拆分为“发送器”实例,其中包含“send”(阻塞)和“try_send”(非阻塞)方法;以及“接收器”实例,其中包含“recv”(阻塞)和“try_recv”(非阻塞)方法。接收器通常也可以用作阻塞迭代器。
「生产者-消费者模式」
这是一种简单的模式,其中一个或多个生产者向单个消费者发送数据。适合简单的并行处理。在大型项目中应谨慎使用,因为其灵活性可能导致项目架构不清晰。

cdn-images-1.readmedium.com/v2/resize:f…
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
// Producer
let producer = thread::spawn(move || {
let data = vec![1, 2, 3, 4, 5];
for x in data {
tx.send(x).unwrap();
println!("Sent {}", x);
}
});
// Consumer
let consumer = thread::spawn(move || {
for received in rx {
println!("Received {}", received);
}
});
producer.join().unwrap();
consumer.join().unwrap();
}
「管道模式」
这是一种模式,其中多个线程按链式连接。在以下情况下非常有用:
- 数据处理被划分为多个阶段。
- 在不同的通道实现之间进行转换。
- 在无限制容量和有限制容量的通道之间进行转换(例如,为了防止内存耗尽,未处理的数据会被丢弃或以不同的方式处理)。

cdn-images-1.readmedium.com/v2/resize:f…
use std::sync::mpsc;
use std::thread;
fn main() {
// Let us use a scope to avoid having to join all the threads
thread::scope(|scope| {
let (tx, rx) = mpsc::channel();
let (tx2, rx2) = mpsc::channel();
// Stage 1
scope.spawn(move || {
let data = vec![1, 2, 3, 4, 5];
for x in data {
tx.send(x * x).unwrap(); // Squares each number
}
});
// Stage 2
scope.spawn(move || {
for x in rx {
tx2.send(x + 1).unwrap(); // Increments each number
}
});
// Final output
scope.spawn(move || {
for y in rx2 {
println!("{}", y); // Prints result
}
});
});
}
「工作池模式」
这是一种模式,其中多个工作线程从单个通道中处理数据。这是一种典型的数据处理并行化模式,其中所有的工作线程执行相同的工作。
这种模式通常用于需要并行处理大量相似任务的情况。工作线程从共享的通道中取出任务来执行,并将结果返回给通道或直接处理结果。这种方法可以显著提高处理效率,尤其是在处理大量数据或执行计算密集型任务时。
工作池模式的一个关键优势是能够轻松地扩展处理能力,只需增加更多的工作线程即可。此外,它还提供了负载均衡,因为所有的工作线程都在共享同一个任务队列。

cdn-images-1.readmedium.com/v2/resize:f…
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::{mem, thread};
struct Worker {
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Option<i32>>>>) -> Self {
let thread = thread::spawn(move || loop {
let Some(job) = receiver.lock().unwrap().recv().unwrap() else {
println!("worker {} exited", id);
break;
};
println!("Worker {} received job {}", id, job);
//thread::sleep(std::time::Duration::from_secs(1));
});
Self { thread }
}
}
struct WorkerPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Option<i32>>,
}
impl WorkerPool {
fn new(size: usize) -> Self {
let (sender, receiver) = mpsc::channel();
// The standard Rust library does not provide multi-consumer channels
// (by the moment this article is published). In other implementations,
// a receiver is usually allowed to be cloned so no Arc+Mutex
// is requied.
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, receiver.clone()));
}
Self { workers, sender }
}
fn execute(&self, job: i32) {
self.sender.send(Some(job)).unwrap();
}
}
impl Drop for WorkerPool {
fn drop(&mut self) {
// send all workers a signal to exit
for _ in 0..self.workers.len() {
self.sender.send(None).unwrap();
}
// wait for all workers to exit
for worker in mem::take(&mut self.workers) {
worker.thread.join().unwrap();
}
}
}
fn main() {
let pool = WorkerPool::new(4);
for job in 0..8 {
pool.execute(job);
}
}
「发布订阅(数据中心)模式」
这是一种模式,其中消费者通过一个“数据中心”对象订阅到通道。数据中心通常管理着订阅关系。
数据中心通常是一个被动的实例,本身并不处理数据。当生产者调用其方法时,发布订阅逻辑在生产者的线程中处理。
优点:
- 清晰且灵活的项目架构。
- 订阅可以集中管理,并且可以附加额外的逻辑,例如条件过滤。
缺点:
- 实现起来较为复杂。
- 如果实现不当,数据中心可能会成为瓶颈。
- 消息通常需要实现“Clone”特质,这意味着消息可能需要被复制。
发布订阅模式特别适用于需要广播数据到多个消费者的情况。这种模式允许灵活地添加和移除消费者,并且可以通过数据中心来控制订阅逻辑。然而,由于所有的订阅管理和消息分发都集中在数据中心,因此需要确保它的实现足够高效以避免成为性能瓶颈。

cdn-images-1.readmedium.com/v2/resize:f…
use std::collections::BTreeMap;
use std::sync::atomic::AtomicUsize;
use std::sync::mpsc::{self, Receiver, SyncSender};
use std::sync::{Arc, Mutex};
use std::thread;
type SubscriberId = usize;
struct PubSub<T> {
subscribers: Arc<Mutex<BTreeMap<SubscriberId, SyncSender<T>>>>,
next_id: AtomicUsize,
}
impl<T: Send + 'static> PubSub<T> {
fn new() -> Self {
Self {
subscribers: Arc::new(Mutex::new(BTreeMap::new())),
next_id: <_>::default(),
}
}
fn subscribe(&self) -> (SubscriberId, Receiver<T>) {
let (tx, rx) = mpsc::sync_channel(512);
let mut subscribers = self.subscribers.lock().unwrap();
let id = self
.next_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
subscribers.insert(id, tx);
(id, rx)
}
fn unsubscribe(&self, id: SubscriberId) {
let mut subscribers = self.subscribers.lock().unwrap();
subscribers.remove(&id);
}
// an example of wrong implementation: deadlocks the whole hub
// if any channel is full
fn publish_deadlocking(&self, message: T)
where
T: Clone,
{
let subscribers = self.subscribers.lock().unwrap();
for tx in subscribers.values() {
tx.send(message.clone()).unwrap();
}
}
// an example of correct implementation: subscribers are cloned before
// sending and the lock is released instantly. may lead to tiny overhead
// with cloning and data-races when a subscriber may receive a message
// after it has been unsubscribed, however the deadlock-free benefits
// fully compensate for all of that
fn publish(&self, message: T)
where
T: Clone,
{
let subscribers = self.subscribers.lock().unwrap().clone();
for tx in subscribers.values() {
// ignore error if a consumer has been already unsubscribed
// and dropped
let _ = tx.send(message.clone());
}
}
}
fn main() {
let hub = PubSub::new();
let (sub_id, rx) = hub.subscribe();
let handle = thread::spawn(move || {
for received in rx {
println!("Received: {:?}", received);
}
});
hub.publish("Hello, world!");
// In production code, subscribers should unsubscribe themselves
// automatically, by implementing Drop trait
hub.unsubscribe(sub_id);
handle.join().unwrap();
}
「Fan-in / Fan-out」
这是一种模式,其中生产者将数据发送到单个通道(「Fan-in」)。之后,一个或多个消费者从该通道收集数据(「Fan-out」)。
这种模式适合于并行处理和为单一操作聚合数据。通道被用作共同的结果存储。这可能是唯一一种无限制容量的通道始终安全使用的模式,因为任务的数量是严格定义的。
消费者可以选择等待所有生产者完成发送数据,或者立即开始接收数据。当生产者完成发送数据后,它们所在的线程会被停止。
这种模式常用于需要将来自多个来源的数据合并到一起处理的场景,或者需要将数据分发给多个处理单元进行并行处理的情况。扇入/扇出模式能够很好地支持这类需求,同时也能有效地管理资源。

cdn-images-1.readmedium.com/v2/resize:f…
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
// Fan-in: Distributing work to multiple workers
for i in 0..5 {
let tx_clone = tx.clone();
thread::spawn(move || {
tx_clone.send(i * 2).unwrap(); // Each worker sends a message
});
}
drop(tx); // Close the original sender
// Fan-out: Collecting results from multiple workers
for received in rx {
println!("Received {}", received);
}
}
「Actor 模型」
这是一种看起来类似于工作池模式但更为复杂的模式。每个演员都有自己的通道用于通信,并执行不同的任务。
在演员模型中,每个演员都是一个独立的行为单位,拥有自己的状态和行为。演员通过消息传递进行通信,而不是通过共享内存。当一个演员接收到消息时,它会根据消息的内容更新自己的状态,并可能向其他演员发送新的消息。
演员模型非常适合于构建高度并发和分布式的系统,因为它能够很好地处理并发性和隔离性问题。这种模式有助于构建易于理解和维护的系统,同时也能够处理故障恢复和容错等问题。
Rust中tokio-actor支持了这种特性

cdn-images-1.readmedium.com/v2/resize:f…
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
struct Actor {
sender: Sender<Option<String>>,
receiver: Mutex<Option<Receiver<Option<String>>>>,
name: String,
quiet: bool,
handle: Mutex<Option<thread::JoinHandle<()>>>,
}
impl Actor {
fn new(name: String, quiet: bool) -> Arc<Self> {
let (tx, rx) = mpsc::channel();
Actor {
sender: tx,
// put the receiver under a Mutex to get it back when the actor
// is started
receiver: Mutex::new(Some(rx)),
name,
quiet,
handle: <_>::default(),
}
.into()
}
fn start(self: &Arc<Self>) {
let receiver = self.receiver.lock().unwrap().take().unwrap();
let me = self.clone();
let handle = thread::spawn(move || {
for message in receiver {
if let Some(msg) = message {
if !me.quiet {
println!("{} received: {}", me.name, msg);
}
} else {
break;
}
}
println!("{} stopped", me.name);
});
self.handle.lock().unwrap().replace(handle);
}
fn send(&self, message: String) {
self.sender.send(Some(message)).unwrap();
}
fn stop(&self) {
if let Some(handle) = self.handle.lock().unwrap().take() {
self.sender.send(None).unwrap();
handle.join().unwrap();
}
}
}
fn main() {
let actor1 = Actor::new("Actor1".to_string(), true);
let actor2 = Actor::new("Actor2".to_string(), false);
actor1.start();
actor2.start();
actor1.send("Hello from Main to Actor1".to_string());
actor2.send("Hello from Main to Actor2".to_string());
actor1.stop();
actor2.stop();
}
结束语
通道是并行编程的强大工具。它们用途广泛,可以以多种方式使用。然而,如果使用不当,它们也可能成为许多问题的源头。
在下一部分中,我们将重点关注通道实现的一些技术方面,以及如果不考虑这些方面可能会遇到的问题。
如果您有任何具体的技术细节或问题想要了解,或者希望探讨有关通道实现的具体方面,请随时告诉我。
创建和维护这个博客以及相关的库带来了十分庞大的工作量,即便我十分热爱它们,仍然需要你们的支持。或者转发文章。通过赞助我,可以让我有能投入更多时间与精力在创造新内容,开发新功能上。赞助我最好的办法是微信公众号看看广告。
本文使用 markdown.com.cn 排版