pipeline 模式在编程中很常见。使用linux命令行时,经常会用到管道连接两个命令:如 ls /tmp | grep hello.txt,查找/tmp目录下是否存在hello.txt文件。其中管道操作符就是两个命令的pipeline, 通过管道将 ls 命令行的输出作为 grep 命令行的输入,从而让数据流动起来。回到编程中,数据流动带来的好处是可以将编程逻辑进行拆分,不同的线程(协程,进程)实现不同的逻辑模块,降低编码难度。
写一个Rust实例练练手:
use std::thread;
use std::sync::mpsc;
fn run_thread_1() -> (thread::JoinHandle<()>, mpsc::Receiver<String>) {
let (tx, rx) = mpsc::channel();
let hdl = thread::spawn(move || {
tx.send("thread_1".to_owned()).unwrap();
});
(hdl, rx)
}
fn run_thread_2(rx1: mpsc::Receiver<String>) -> (thread::JoinHandle<()>, mpsc::Receiver<String>){
let (tx, rx) = mpsc::channel();
let hdl = thread::spawn(move || {
if let Ok(mut msg) = rx1.recv() {
msg.push_str("-> thread_2");
tx.send(msg).unwrap();
}
});
(hdl, rx)
}
fn run_thread_3(rx2: mpsc::Receiver<String>) -> thread::JoinHandle<()> {
let hdl = thread::spawn(move || {
if let Ok(msg) = rx2.recv() {
println!("{} -> thread_3", msg);
}
});
hdl
}
fn main() {
let mut thread_v = vec![];
let (hdl, rx1) = run_thread_1();
thread_v.push(hdl);
let (hdl, rx2) = run_thread_2(rx1);
thread_v.push(hdl);
let hdl = run_thread_3(rx2);
thread_v.push(hdl);
for hdl in thread_v {
hdl.join().unwrap();
}
}
实例程序创建三个线程,线程1将数据发送到线程2,线程2将数据发送到线程3,线程三将数据打印出来,主线程等待三个线程返回。程序通过channel将三个线程的数据流连接起来,就像工厂的生产流水线一样,一个原材料经过各个工序加工,最终形成产品。
题外话
- Rust的channel通过所有权转移的方式传输数据的,线程将通讯的成本很低。C 中也有一个pipe 系统调用,通过内核缓冲区传递数据(深拷贝数据),一般用于进程间通讯。
- C 中线程间实现类似 Rust/Go channel的功能也不难。通过mutex 和 cond 实现线程阻塞等待,通过自旋锁(无锁)队列实现缓冲数据功能,发送端添加数据到队列,触发条件变量唤醒线程,线程从队列中接收数据,从而实现channel功能。队列通过保存数据指针传递数据,效果上更像是Rust channel 转移所有权的方式。