syn
用于异步上下文的同步原语
Tokio程序往往被组织为一组任务,其中每个任务独立运行,并且可以在单独的物理线程上执行。该模块中提供的同步原语允许这些独立任务一起通信。
消息传递
Tokio程序中最常见的同步形式是消息传递。两个任务独立运行,并相互发送消息以进行同步。这样做的好处是避免共享状态。
消息传递是使用通道实现的。通道支持从一个生产者任务向一个或多个消费者任务发送消息。Tokio提供了几种风格的频道。每个通道风格都支持不同的消息传递模式。当一个通道支持多个生产者时,许多单独的任务可能会发送消息。当一个通道支持多个使用者时,许多不同的独立任务可能会接收消息。
Tokio提供了许多不同的通道风格,因为不同的消息传递模式最好用不同的实现来处理。
oneshot channel
oneshot channel通道支持从单个生产者向单个消费者发送单个值。此通道通常用于将计算结果发送给等待者。所以可以称为spsc。
举个栗子:
use tokio::sync::oneshot;
async fn some_computation() -> String {
//模拟某种计算
"represents the result of the computation".to_string()
}
#[tokio::main]
async fn main() {
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
let res = some_computation().await;
tx.send(res).unwrap();
});
// 当计算发生时做点其他事情
// 等待计算结果
let res = rx.await.unwrap();
}
请注意,如果任务在终止前产生计算结果作为其最终操作,则JoinHandle可用于接收该值,而不是为oneshot通道分配资源。等待JoinHandle返回Result。如果任务恐慌,Joinhandle会产生带有恐慌原因的Err。
举个例子:
async fn some_computation() -> String {
"the result of the computation".to_string()
}
#[tokio::main]
async fn main() {
let join_handle = tokio::spawn(async move {
some_computation().await
});
// Do other work while the computation is happening in the background
// Wait for the computation result
let res = join_handle.await.unwrap();
}
注意:JoinHandle允许一个任务在完成时传递一个值给等待它的其他任务或线程。这通常通过一个future或promise对象来实现,其中future代表一个尚未完成的异步操作的结果,而promise则用于设置这个结果。
mpsc channel
mpsc通道支持从多个生产者向单个消费者发送多个值。此通道通常用于向任务发送工作或接收许多计算的结果。
如果您想从一个生产者向一个消费者发送许多消息,这也是您应该使用的渠道。没有专用的spsc通道要注意到oneshot是一次性的。
举个栗子:使用mpsc递增地流式传输一系列计算的结果。
use tokio::sync::mpsc;
async fn some_computation(input: u32) -> String {
format!("the result of computation {}", input)
}
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(100);
//接收者是可变的
tokio::spawn(async move {
for i in 0..10 {
let res = some_computation(i).await;
tx.send(res).await.unwrap();
//通道将结果传送给了rx
}
});
while let Some(res) = rx.recv().await {
println!("got = {}", res);
}
}
mpsc::channel的参数是通道容量。这是在任何给定时间可以存储在通道中等待接收的最大值数。正确设置该值是实施稳健计划的关键,因为通道容量在处理背压方面起着关键作用。
资源管理的一种常见并发模式是生成一个专门用于管理该资源的任务,并使用其他任务之间的消息传递与资源交互。资源可以是任何不能同时使用的资源。一些示例包括套接字和程序状态。例如,如果多个任务需要通过单个套接字发送数据,则生成一个任务来管理套接字并使用通道进行同步。
举个例子:使用消息传递通过单个套接字发送来自多个任务的数据。
use tokio::io::{
self, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> io::Result<()> {
let mut socket = TcpStream::connect("www.example.com:1234").await?;
let (tx, mut rx) = mpsc::channel(100);
for _ in 0..10 {
// 每个任务都需要自己的“tx”句柄。这是通过克隆原始句柄来完成的。
let tx = tx.clone();
tokio::spawn(async move {
tx.send(&b"data to write"[..]).await.unwrap();
});
}
//通道的“rx”一半返回“None”once**all**`tx` clonesdrop。
//若要确保返回“None”,请删除当前任务所拥有的句柄。如果这个“tx”句柄没有被丢弃,那么总会有一个未处理的“tx’句柄。
drop(tx);
while let Some(res) = rx.recv().await {
socket.write_all(res).await?;
}
Ok(())
}
mpsc和oneshot信道可以组合以提供具有共享资源的请求/响应类型同步模式。生成一个任务来同步资源,并等待在mpsc通道上接收到的命令。每个命令都包括一个oneshot Sender,在该Sender上发送命令的结果。
举个栗子:使用任务来同步64位计数器。每个任务都发送一个“fetch and increment”命令。通过提供的oneshot通道发送增量之前的计数器值。
use tokio::sync::{
oneshot, mpsc};
use Command::Increment;
enum Command {
Increment,
// 其他命令可以加在这里
}
#[tokio::main]
async fn main() {
let (cmd_tx, mut cmd_rx) = mpsc::channel::<(Command, oneshot::Sender<u64>)>(100);
// 生成一个管理计数器的任务
tokio::spawn(async move {
let mut counter: u64 = 0;
while let Some((cmd, response)) = cmd_rx.recv().await {
match cmd {
Increment => {
let prev = counter;
counter += 1;
response.send(prev).unwrap();
}
}
}
});
let mut join_handles = vec![];
// 生成将发送增量命令的任务.
for _ in 0..10 {
let cmd_tx = cmd_tx.clone();
join_handles.push(tokio::spawn(async move {
let (resp_tx, resp_rx) = oneshot::channel();
cmd_tx.send((Increment, resp_tx)).await.ok().unwrap();
let res = resp_rx.await.unwrap();
println!("previous value = {}", res);
}));
}
// 等待所有任务完成
for join_handle in join_handles.drain(..) {
join_handle.await.unwrap();
}
}
broadcast channel广播通道
广播频道支持从许多生产者向许多消费者发送许多值。每个消费者都将收到每个值。此通道可用于实现pub/sub或“聊天”系统常见的“fan out”系统。
此通道的使用频率往往低于oneshot和mpsc,但仍有其用例。
如果您想将单个生产者的价值传播给许多消费者,这也是您应该使用的渠道。没有专门的spmc广播频道。
这里解释一下什么叫做fan out:
Fan-out系统是一种设计模式,主要用于处理消息传递和事件驱动的架构中。在这种模式下,一个单一的发送者(或发布者)可以向多个接收者(或订阅者)发送数据或事件通知。这种设计允许一个消息被广泛传播到多个不同的处理单元,而不需要每个接收者单独去请求或轮询数据。
基本的用法如下
use tokio::sync::broadcast;
#[tokio::main]
async fn main() {
let (tx, mut rx1) = broadcast::channel(16);
let mut rx2 = tx.subscribe();
tokio::spawn(async move {
assert_eq!(rx1.recv().await.unwrap(), 10);
assert_eq!(rx1.recv().await.unwrap(), 20);
});
tokio::spawn(async move {
assert_eq!(rx2.recv().await.unwrap(), 10);
assert_eq!(rx2.recv().await.unwrap(), 20);
});
tx.send(10).unwrap();
tx.send(20).unwrap();
}
watch channel
watch channel支持从多个生产者向多个消费者发送许多值。但是,只有最新的值存储在通道中。发送新值时会通知消费者,但不能保证消费者会看到所有值。
watch channel类似于容量为1的broadcast channel。
watch channel的使用情况包括广播配置变化或信号程序状态的变化,例如转换到关闭。
举个例子:使用watch channel向任务通知配置更改。在本例中,会定期检查配置文件。当文件发生更改时,配置更改会用信号通知消费者。
这里解释一下什么叫做句柄:
句柄(Handle)是计算机编程中的一个概念,它是一个抽象的数据类型,用于引用、操作和管理资源。句柄通常作为一个标识符,代表了系统中的一个对象、设备、文件、网络连接或其他资源。通过句柄,程序员可以在不直接操作底层资源的情况下,执行对资源的各种操作。你可以理解为一个泛型的引用
use tokio::sync::watch;
use tokio::time::{
self, Duration, Instant};
use std::io;
#[derive(Debug, Clone, Eq, PartialEq)]
struct Config {
timeout: Duration,
}
impl Config {
async fn load_from_file() -> io::Result<Config> {
// 此处为文件加载和反序列化逻辑
}
}
async fn my_async_operation() {
// Do something here
}
#[tokio::main]
async fn main() {
// 加载初始配置值
let mut config = Config::load_from_file().await.unwrap();
// 创建一个watch channel,初始化加载配置
let (tx, rx) = watch::channel(config.clone());
// 创建一个任务来监视文件
tokio::spawn(async move {
loop {
// 10秒检查一次
time::sleep(Duration::from_secs(10)).await;
// 加载配置文件
let new_config = Config::load_from_file().await.unwrap();
// 如果配置文件发生改变,就在watch channel中发送配置的值
if new_config != config {
tx.send(new_config.clone()).unwrap();
config = new_config;
}
}
});
let mut handles = vec![];
// 生成运行异步操作最多“超时”的任务。如果超时已过,请重新启动操作。
//该任务同时监视“配置”中的更改。
//当超时持续时间发生变化时,在不重新启动in-flight操作的情况下更新超时。
for _ in 0..5 {
// 克隆一个watch句柄用在这个task里
let mut rx = rx.clone();
let handle = tokio::spawn(async move {
// 开始初始操作并将future固定到堆栈。需要固定到堆栈才能在多次调用“select`
let op = my_async_operation();
tokio::pin!(op);
// 获得一个初始化配置值
let mut conf = rx.borrow().clone();
let mut op_start = Instant::now();
let sleep = time::sleep_until(op_start + conf.timeout);
tokio::pin!(sleep);
loop {
tokio::select! {
_ = &mut sleep => {
// 操作结束,重新启动
op.set(my_async_operation());
// 追踪新的启动时间
op_start = Instant::now();
// 重启超时
sleep.set(time::sleep_until(op_start + conf.timeout));
}
_ = rx.changed() => {
conf = rx.borrow_and_update().clone();
// 配置已更新。使用新的“timeout”值更新“sleep”。
sleep.as_mut().reset(op_start + conf.timeout);
}
_ = &mut op => {
// 操作完成
return
}
}
}
});
handles.push(handle);
}
for handle in handles.drain(..) {
handle.await.unwrap();
}
}
状态同步
剩下的同步原语主要关注同步状态。它们与std提供的版本是异步等效的。它们的操作方式与std对应版本类似,但将异步等待,而不是阻塞线程。
Barrier:确保多个任务在一起继续执行之前,将等待彼此到达程序中的某个点。
mutex:互斥机制,确保一次最多有一个线程能够访问某些数据。
notify:基本任务通知。Notify支持在不发送数据的情况下通知接收任务。在这种情况下,任务会唤醒并继续处理。
RwLock:(读写锁)提供互斥机制,允许同时使用多个读卡器,同时只允许使用一个写入器。在某些情况下,这可能比互斥更有效。
semaphore:限制并发量。信号量拥有许多许可证,任务可以请求这些许可证以进入关键部分。信号量可用于实现任何类型的限制或定界。