平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“Rust并发编程入门:多进程创建与等待子进程”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。
Rust的并发编程
并发,是指在宏观意义上同一时间处理多个任务。
并发的方式一般包含为三种:
多进程、多线程以及最近几年刚刚火起来的协程。
多进程并发
新建子进程
首先,我们新建两个项目,一个为子进程,一个为主进程。
在子进程的main.rs里,编写如下所示代码:
use std::thread::sleep;
use std::time::Duration;
fn main() {
println!("Hello, world!");
sleep(Duration::from_secs(5)); // 休息5秒钟
println!("Bye, world!");
}
在这个场景下,程序先输出"Hello, world!",随后休息5秒钟,最后打印"Bye, world!"退出。
在主进程的main.rs里,编写如下所示代码:
use std::process::Command;
fn main() {
Command::new("../sub/target/debug/sub.exe").spawn().unwrap();
}
落到代码里,执行主进程时,发现子进程输出了"Hello, world!",但是没有下文了。
结合项目来看,有经验的我们都知道,这是因为主进程启动了子进程后立刻退出了。我们需等待子进程结束。
等待子进程结束
在这个场景下,要等待子进程结束,需采用一个变量保存子进程对象,随后调用子进程的wait方法:
use std::process::Command;
fn main() {
let mut p = Command::new("../sub/target/debug/sub.exe").spawn().unwrap();
p.wait().unwrap();
}
结合项目来看,这里需留意,因为wait方法会改变子进程对象的状态,所以子进程对象必须是可变的。
在这个场景下,与wait对应的,还有一个try_wait方法,try_wait不会阻塞主进程,无论子进程是否结束,都会得到Result,同时将状态保存到Ok中:
use std::thread::sleep;
use std::time::Duration;
use std::process::Command;
fn main() {
let mut p = Command::new("../sub/target/debug/sub.exe").spawn().unwrap();
loop {
let status = p.try_wait().unwrap();
match status {
Some(status) => {
if status.success() {
println!("sub process over");
} else {
println!("sub process error");
}
break;
}
None => {
sleep(Duration::from_secs(1));
continue;
}
}
}
}
结合项目来看,Ok中保存是一个Option,当子进程结束时,为Some,没有进程时为None。
指定子进程的命令行参数
修改一下子进程,使子进程能够借助命令行参数来决定sleep的时间。
接收命令行参数能够采用标准库的env模块:
use std::env::args;
use std::thread::sleep;
use std::time::Duration;
fn main() {
println!("Hello, world!");
let mut args = args();
let secs = args.nth(1).unwrap().parse::().unwrap();
sleep(Duration::from_secs(secs));
println!("Bye, world!");
}
从实现思路看,args得到一个比较复杂的结构,能够用nth来取得第几个参数的值,第0个是exe的名称,第1个是传给exe的第1个参数。因为nth有可能是None,所以得到的是Option,能够用unwrap()取得参数的字符串。
Duration::from_secs接收的是u64类型,需把字符串采用parse::()转成u64类型,转换过程可能会失败,所以parse得到Result,同样能够采用unwrap()取得转换后的值。
从实现思路看,下面,该修改主进程了。主进程的修改比较轻松,只需在Command::new之后添加.arg即可:
use std::process::Command;
fn main() {
let mut p = Command::new("../sub/target/debug/sub.exe").arg("5").spawn().unwrap();
p.wait().unwrap();
}
从实现思路看,若是多个参数,能够采用args函数,将参数合同时成一个数组传入就能够了。
进程间通信
Rust当然也能够借助管道进行进程间通信。
落到代码里,修改子进程代码,将函数的参数设置为接收数据的条数,每接收到一条数据就原样得到,直到达到设定的条数后退出:
use std::io;
use std::env::args;
fn main() {
println!("Hello, world!");
let mut args = args();
let count = args.nth(1).unwrap().parse::().unwrap();
let mut index = 0;
let stdin = io::stdin(); // 打开标准输入
while index < count {
let mut s = String::new();
stdin.read_line(&mut s).unwrap(); // 读取标准输入中的一行
println!("{}", s.trim()); // trim掉最后的\n
index += 1;
}
println!("Bye, world!");
}
从实现思路看,然后在主进程中,循环向子进程的标准输入里写入消息,同时读取标准输出的内容:
use std::process::{Command, Stdio};
use std::io::{BufRead, Write, BufReader};
fn main() {
// 消息数组
let msglist = ["msg1", "msg2", "msg3", "msg4", "msg5"];
// 启动子进程
let mut p = Command::new("../sub/target/debug/sub.exe")
.arg(msglist.len().to_string()) // 传递消息的个数
.stdin(Stdio::piped()) // 将子进程的标准输入重定向到管道
.stdout(Stdio::piped()) // 将子进程的标准输出重定向到管道
.spawn()
.unwrap();
let p_stdin = p.stdin.as_mut().unwrap();
let mut p_stdout = BufReader::new(p.stdout.as_mut().unwrap());
let mut line = String::new();
// 接收Hello world
p_stdout.read_line(&mut line).unwrap();
println!("{}", line);
// 循化发送消息
for msg in msglist.iter() {
// 发送消息
println!("write to stdid:{}", msg);
p_stdin.write(msg.as_bytes()).unwrap();
p_stdin.write("\n".as_bytes()).unwrap(); // 发送\n,子进程的read_line才会响应
// 接收消息
line.clear(); // 需要清空,否则会保留上次的结果
p_stdout.read_line(&mut line).unwrap();
println!("read from stdout:{}", line);
}
// 接收Bye world
line.clear();
p_stdout.read_line(&mut line).unwrap();
println!("{}", line);
// 等待子进程结束
p.wait().unwrap();
}
总结
落到代码里,总的来说,Rust多进程新建适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。

