< 返回版块

colatea 发表于 2023-09-12 18:21

Tags:tokio

在看tokio官方文档的时候,有描述使用标准库的锁和使用tokio的锁,说有个错误理解,是不是所有的异步代码都要用tokio的锁。还给了一条规则,但是没有理解什么叫跨await调用lock???

希望大佬给个例子,能复现错误使用且锁住最好?感谢

原文如下: On using std::sync::Mutex Note that std::sync::Mutex and not tokio::sync::Mutex is used to guard the HashMap. A common error is to unconditionally use tokio::sync::Mutex from within async code. An async mutex is a mutex that is locked across calls to .await.

A synchronous mutex will block the current thread when waiting to acquire the lock. This, in turn, will block other tasks from processing. However, switching to tokio::sync::Mutex usually does not help as the asynchronous mutex uses a synchronous mutex internally.

As a rule of thumb, using a synchronous mutex from within asynchronous code is fine as long as contention remains low and the lock is not held across calls to .await.

评论区

写评论
Cherrs 2023-09-13 16:04

实际应用的话,典型的场景就是在共享IO资源的时候使用tokio的Mutex,可以看看tokio的docs.rs,介绍的更详细

作者 colatea 2023-09-13 11:08

大致明白了。感谢各位大佬指导

Bai-Jinlin 2023-09-13 09:55
use std::sync::Mutex;

async fn noop() {}

fn foo<Fut: Sync + Send + 'static>(f: Fut) {}

fn main() {
    foo(async {
        let mutex = Mutex::new(1);
        let guard = mutex.lock().unwrap();
        noop().await;
        drop(guard);
    })
}

这个代码就会报错。现在分析一下为什么会报错。 最直观的原因时因为async块生成的future是!Send的,因为Mutex lock生成的guard是!Send的,并且跨越了await,所以整个future就是!Send的。倘若没有跨过await,那就是Send。

现在分析一下为什么这样做,tokio默认是调度在多个线程上的,有多个执行流,并且同个future可能在不同的线程上运行,所以spawn要求Send+Sync+'static。

await点意味着future poll可能返回了pending,这时的future是有可能被tokio调度到其他线程运行的。

假如这段代码可以编译过去就会出现一个线程加锁,解锁在非加锁线程上的情况。所以此future是!Send。 倘若没有跨越await,自然也就保证了在同一个线程上加锁并且解锁的情况,自然future就是Send了。

hangj 2023-09-12 23:17
use tokio::sync::Mutex; // note! This uses the Tokio mutex

// This compiles!
// (but restructuring the code would be better in this case)
async fn increment_and_do_stuff(mutex: &Mutex<i32>) {
    let mut lock = mutex.lock().await;
    *lock += 1;

    do_something_async().await;
    // lock 在这里还没有被释放
}  // lock goes out of scope here 出了这里才被释放

这就叫跨 .await 占用 lock

通常情况下,我们希望 mutex 用完之后尽快释放掉。这个例子中执行完 *lock += 1 之后,lock 的任务已经完成了,没必要等到跨过后面的 .await 之后再释放

因为 tokio 的 Mutex 允许这样用,所以当我们写出类似上面这种不符合最佳实践的代码时编译器不会有啥提示。而使用 std 的 Mutex 直接就不允许你跨 .await 持有 lock,逼你写出符合最佳的代码

所以只有当你自己明确知道,我的需求就是要在多个 .await 间持有 lock,这个时候用 tokio 的 Mutex 才合适

hangj 2023-09-12 22:58

这段代码会报错:


use std::sync::{Mutex, MutexGuard};

async fn increment_and_do_stuff(mutex: &Mutex<i32>) {
    let mut lock: MutexGuard<i32> = mutex.lock().unwrap();
    *lock += 1;

    do_something_async().await;
} // lock goes out of scope here

这段不会报错


use tokio::sync::Mutex; // note! This uses the Tokio mutex

// This compiles!
// (but restructuring the code would be better in this case)
async fn increment_and_do_stuff(mutex: &Mutex<i32>) {
    let mut lock = mutex.lock().await;
    *lock += 1;

    do_something_async().await;
} // lock goes out of scope here

https://tokio.rs/tokio/tutorial/shared-state

fakeshadow 2023-09-12 19:04
fn lock(lock: std::sync::Mutex<()>) -> impl std::future::Future<Output = ()> + Send {
    async move {
        let guard = lock.lock().unwrap();
        std::future::pending::<()>().await;
        let _g = guard;
    }
}

fn async_lock(lock: tokio::sync::Mutex<()>) -> impl std::future::Future<Output = ()> + Send {
    async move {
        let guard = lock.lock().await;
        std::future::pending::<()>().await;
        let _g = guard;
    }
}

简单来说,MutexGuard生命周期必须跨越await的时候可以使用async,其他情况可以优先考虑sync

1 共 6 条评论, 1 页