rust关于tokio下的状态同步—资源共享机制

Barrier:确保多个任务在一起继续执行之前,将等待彼此到达程序中的某个点。

mutex:互斥机制,确保一次最多有一个线程能够访问某些数据。

notify:基本任务通知。Notify支持在不发送数据的情况下通知接收任务。在这种情况下,任务会唤醒并继续处理。

RwLock:(读写锁)提供互斥机制,允许同时使用多个读卡器,同时只允许使用一个写入器。在某些情况下,这可能比互斥更有效。

semaphore:限制并发量。信号量拥有许多许可证,任务可以请求这些许可证以进入关键部分。信号量可用于实现任何类型的限制或定界。

barrier

use tokio::sync::Barrier;
use std::sync::Arc;

#[tokio::main]
async fn main(){
    let mut handles = Vec::with_capacity(10);
// 创建一个容量为10的vector数组
    let barrier = Arc::new(Barrier::new(10));
// 创建一个可以等待10个任务的barrier实例,并进行线程分发
    for _ in 0..10 {
	// c也拥有这个barrier实例
         let c = barrier.clone();
    // 相同的消息将会无交错的打印
        handles.push(tokio::spawn(async move {
            println!("before wait");
            // c.wait() 是对 tokio::sync::Barrier 实例的调用,它是一个异步操作,
            // 用于等待所有持有 Barrier 克隆的参与者都到达这个调用点。
            let wait_result = c.wait().await;
            println!("after wait");
         wait_result
    }));
}

// 打印完所有“await”的消息后才会解析
let mut num_leaders = 0;
for handle in handles {
    let wait_result = handle.await.unwrap();
    if wait_result.is_leader() {
        num_leaders += 1;
    }
}

// 只有一个barrier会被解析为leader
assert_eq!(num_leaders, 1);
}

输出的结果如下

before wait
before wait
before wait
before wait
before wait
before wait
before wait
before wait
before wait
before wait
after wait
after wait
after wait
after wait
after wait
after wait
after wait
after wait
after wait
after wait

c.wait()提供了一个is_leader()方法来确定这个返回是不是leader,可以在leader中做一些特别的操作

let c = barrier.clone(); // 克隆 Barrier 实例。
let wait_result = c.wait().await; // 等待所有任务到达 Barrier。

if wait_result.is_leader() {
    // 当前任务是 "leader"
    println!("I am the leader!");
    // 这里可以执行一些只有 "leader" 才能执行的操作。
} else {
    // 当前任务不是 "leader"
    println!("I am a follower.");
    // 这里可以执行所有任务都需要完成的操作。
}

Mutex

Tokio下的mutex是一种异步锁。
这种类型的作用类似于std::sync::Mutex,
但有两个主要区别:锁是一种异步方法,因此不会阻塞,锁保护被设计为跨.await点。

与流行的观点相反,在异步代码中使用标准库中的普通Mutex是可以的,而且通常是首选
异步Mutex相对于阻塞Mutex提供的功能是能够在==.await点上保持其锁定==。这使得异步mutex比阻塞mutex更昂贵,因此在可以使用阻塞mutex的情况下,阻塞mutex应该是首选。异步互斥的主要用例是提供对IO资源(如数据库连接)的共享可变访问。如果互斥体后面的值只是数据,那么通常可以使用阻塞互斥体例如标准库或parking_lot中的互斥体。
请注意,尽管编译器不会阻止std Mutex在任务无法在线程之间移动的情况下在.await点之间保持其保护,但这实际上永远不会导致正确的并发代码,因为它很容易导致死锁。
一种常见的模式是包裹Arc<Mutex<…>在一个结构中,该结构提供非异步方法来对中的数据执行操作,并且只锁定这些方法中的互斥对象。mini-redis示例提供了此模式的说明。
此外,当您确实希望共享访问IO资源时,通常最好生成一个任务来管理IO资源,并使用消息传递与该任务进行通信。

use tokio::sync::Mutex;
use std::sync::Arc;

#[tokio::main]
async fn main() {
	// 搞一个指针指向
    let data1 = Arc::new(Mutex::new(0));
    // 克隆一个指针
    let data2 = Arc::clone(&data1);
    tokio::spawn(async move {
        let mut lock = data2.lock().await;
        //锁中的内容加1
        *lock += 1;
    });
    let mut lock = data1.lock().await;
    *lock += 1;
}

看第二个例子

use tokio::sync::Mutex;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let count = Arc::new(Mutex::new(0));

    for i in 0..5 {
        let my_count = Arc::clone(&count);
        tokio::spawn(async move {
            for j in 0..10 {
                let mut lock = my_count.lock().await;
                *lock += 1;
                println!("{} {} {}", i, j, lock);
            }
        });
    }

    loop {
        if *count.lock().await >= 50 {
            break;
        }
    }
    println!("Count hit 50.");
}

在这个例子中,这里有一些值得注意的地方。
互斥对象被封装在一个Arc中,以允许它在线程之间共享。
每个派生的任务都会获得一个锁,并在每次迭代时释放它。
Mutex保护的数据的更改是通过取消引用所获得的锁来完成的,如第13和20行所示。
Tokio的Mutex以简单的FIFO(先进先出)风格工作,所有对锁定的调用都按执行顺序完成。这样,Mutex在如何将锁分配给内部数据方面是“公平的”和可预测的。锁在每次迭代后都会被释放和重新获取,所以基本上,每个线程在增加一次值后都会转到行的后面。请注意,线程启动之间的时间有一些不可预测性,但一旦线程启动,它们就会可预测地交替。最后,由于在任何给定时间都只有一个有效锁, 因此在更改内部值时不可能出现竞争条件。
请注意,与std::sync::Mutex不同,当持有MutexGuard的线程死机时,此实现不会毒害互斥体。在这种情况下,互斥锁将被解锁。如果恐慌被捕获,这可能会使互斥对象保护的数据处于不一致的状态。

notify

通知单个任务唤醒。
Notify提供了一种将事件通知给单个任务的基本机制。Notify本身不携带任何数据。相反,它将用于向另一个任务发出执行操作的信号。
Notify可以被认为是一个以0个许可开始的信号量。notified().await方法等待许可证变为可用,如果当前没有可用的许可证,则notify_one()设置许可证。
Notify的同步细节类似于thread::park和thread::unpark。Notify值包含一个许可证。notified().await等待许可证可用,使用许可证,然后继续。notify_one()设置许可,如果有挂起的任务,则唤醒该任务。
如果notify_one()是在notified().await之前调用的,那么对notify().await的下一次调用将立即完成,消耗许可证。任何后续对notified().await的调用都将等待新的许可证。
如果在notified().await之前多次调用notify_one(),则只存储一个许可证。对notified().await的下一次调用将立即完成,但之后的调用将等待新的许可证。

use tokio::sync::Notify;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    //指向Notify对象的指针
    let notify = Arc::new(Notify::new());
    //克隆了一个指针分发对象
    let notify2 = notify.clone();

    let handle = tokio::spawn(async move {
        //这里等待接受通知
        notify2.notified().await;
        println!("received notification");
    });

    println!("sending notification");
    //在这里发送通知
    notify.notify_one();

    // Wait for task to receive notification.
    handle.await.unwrap();
}

直到handle调用了await才开始跑这个线程

使用通知实现的send\rscv方法

use tokio::sync::Notify;

use std::collections::VecDeque;
use std::sync::Mutex;

struct Channel<T> {
    values: Mutex<VecDeque<T>>,
    notify: Notify,
}

impl<T> Channel<T> {
    pub fn send(&self, value: T) {
        self.values.lock().unwrap()
            .push_back(value);

        // 通知消费者一个值变得有效了
        self.notify.notify_one();
}

    // 这是一个单一的使用者通道,因此不允许同时调用recv`。
    pub async fn recv(&self) -> T {
        loop {
            // 排空值
            if let Some(value) = self.values.lock().unwrap().pop_front() {
                return value;
            }

            // 等待值变得有效
            self.notify.notified().await;	
        }
    }
}

未绑定的多生产者多消费者(mpmc)渠道。
要启用的调用很重要,因为否则,如果有两个要recv的调用和两个要并行发送的调用,可能会发生以下情况:
对try_recv的两个调用都返回None。
两个新元素都添加到矢量中。
notify_one方法被调用两次,只向notify添加一个许可证。
对recv的两个调用都到达通知的未来。其中一个消耗了许可证,另一个永远睡不着。
通过在try_recv之前调用enable将Notified futures添加到列表中,第三步中的notify_one调用将从列表中删除futures并将其标记为Notified,而不是向notify添加许可证。这确保了两个未来都被唤醒。
请注意,只有当同时调用recv时,才会发生此故障。这就是为什么上面的mpsc示例不需要调用来启用。### 未做完

use tokio::sync::Notify;

use std::collections::VecDeque;
use std::sync::Mutex;

struct Channel<T> {
    messages: Mutex<VecDeque<T>>,
    notify_on_sent: Notify,
}

impl<T> Channel<T> {
    pub fn send(&self, msg: T) {
        let mut locked_queue = self.messages.lock().unwrap();
        locked_queue.push_back(msg);
        drop(locked_queue);

        // 向“recv”呼叫中当前正在等待的某个呼叫发送通知。
        self.notify_on_sent.notify_one();
    }

    pub fn try_recv(&self) -> Option<T> {
        let mut locked_queue = self.messages.lock().unwrap();
        locked_queue.pop_front()
    }

    pub async fn recv(&self) -> T {
        let future = self.notify_on_sent.notified();
        tokio::pin!(future);

        loop {
            // Make sure that no wakeup is lost if we get
            // `None` from `try_recv`.
            future.as_mut().enable();

            if let Some(msg) = self.try_recv() {
                return msg;
            }

            // Wait for a call to `notify_one`.
            //
            // This uses `.as_mut()` to avoid consuming the future,
            // which lets us call `Pin::set` below.
            future.as_mut().await;

            // Reset the future in case another call to
            // `try_recv` got the message before us.
            future.set(self.notify_on_sent.notified());
        }
    }
}

Rwlock

本质上是一种写优先的锁,读写进程一起争夺一个mutex,写进程可以阻塞还未进入访问区的读进程防止写进程饿死

异步读写器锁。
这种类型的锁允许在任何时间点有多个读取器或最多一个写入器。此锁的写入部分通常允许修改底层数据(独占访问),而此锁的读取部分通常允许只读访问(共享访问)。
相比之下,Mutex不区分获取锁的读写器,因此导致任何等待锁可用的任务都会产生。只要写入程序没有持有锁,RwLock将允许任何数量的读卡器获取锁。
Tokio的读写锁的优先策略是公平的(或写偏好),以确保读者不会饿死作家。使用用于等待锁定的任务的先进先出队列来确保公平性;如果希望获取写锁的任务位于队列的头,则在释放写锁之前,不会发出读锁。这与Rust标准库的std::sync::RwLock形成对比,后者的优先级策略取决于操作系统的实现。
类型参数T表示该锁所保护的数据。需要T满足Send才能在线程之间共享。从锁定方法返回的RAII保护实现Deref(写方法实现DerefMut),以允许访问锁的内容。

use tokio::sync::RwLock;

#[tokio::main]
async fn main() {
    let lock = RwLock::new(5);

    // many reader locks can be held at once
    {
        let r1 = lock.read().await;
        let r2 = lock.read().await;
        assert_eq!(*r1, 5);
        assert_eq!(*r2, 5);
    } // read locks are dropped at this point

    // only one write lock may be held, however
    {
        let mut w = lock.write().await;
        *w += 1;
        assert_eq!(*w, 6);
    } // write lock is dropped here
}

文档里很多用法还可以深入研究一下

Semaphore

计数执行异步许可证获取的信号量。
信号量维护一组许可。许可证用于同步访问共享资源。信号量与互斥量的不同之处在于,它可以允许多个并发调用方一次访问共享资源。
当调用了获取并且信号量具有剩余的许可时,函数会立即返回一个许可。但是,如果没有剩余的许可可用,则获取(异步)等待,直到丢弃未完成的许可为止。此时,释放的许可证被分配给调用者。
这个信号量是公平的,这意味着许可证是按照要求的顺序发放的。当acquire_many参与时,这种公平性也适用,因此,如果对队列前面的acquire_may的调用请求的许可证比当前可用的许可证多,这可以阻止要获取的调用完成,即使信号量有足够的许可证来完成要获取的调用。
要在轮询函数中使用信号量,可以使用PollSemaphore实用程序。

use tokio::sync::{Semaphore, TryAcquireError};

#[tokio::main]
async fn main() {
    let semaphore = Semaphore::new(3);

    let a_permit = semaphore.acquire().await.unwrap();
    let two_permits = semaphore.acquire_many(2).await.unwrap();

    assert_eq!(semaphore.available_permits(), 0);

    let permit_attempt = semaphore.try_acquire();
    assert_eq!(permit_attempt.err(), Some(TryAcquireError::NoPermits));
}

例子:限制程序中同时打开的文件数

大多数操作系统对打开的文件句柄的数量都有限制。即使在没有明确限制的系统中,资源约束也会隐式地设置打开文件数的上限。如果您的程序试图打开大量文件并超过此限制,将导致错误。
此示例使用具有100个许可证的Semaphore。通过在访问文件之前从Semaphore获得许可,您可以确保您的程序一次打开的文件不超过100个。当试图打开第101个文件时,程序将等待许可证可用后再继续打开另一个文件。

use std::io::Result;
use tokio::fs::File;
use tokio::sync::Semaphore;
use tokio::io::AsyncWriteExt;

static PERMITS: Semaphore = Semaphore::const_new(100);

async fn write_to_file(message: &[u8]) -> Result<()> {
    let _permit = PERMITS.acquire().await.unwrap();
    let mut buffer = File::create("example.txt").await?;
    buffer.write_all(message).await?;
    Ok(()) // Permit goes out of scope here, and is available again for acquisition
}
  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值