#mutex #arc-mutex #async-context #async #lock #no-alloc #shared-data

无std borrow_mutex

不需要封装目标结构的异步Mutex

2个版本

0.1.1 2024年8月19日
0.1.0 2024年6月1日

异步 中排名 #326

Download history 158/week @ 2024-05-30 18/week @ 2024-06-06 3/week @ 2024-06-13 113/week @ 2024-08-15

每月下载 113

MIT 许可证

46KB
679

crates.io libs.rs documentation rust-version license

BorrowMutex

非常初版!请谨慎使用

BorrowMutex 是一个不需要封装目标结构的异步Mutex。相反,可以在任何给定时间将 &mut T 借给互斥锁。

这允许任何其他方借用 &mut T。在出借方等待期间,可变引用是可借用的,出借方可以等待直到有人想要借用。语义强制至少在一方在任何给定时间只有一个可变引用。

这让我们可以在没有 Arc<Mutex> 对象的情况下,在没有依赖任何类型的内部可变性,共享任何可变对象。它主要针对单线程执行器,其中内部可变性是一个不必要的复杂化。尽管如此,BorrowMutex 是 Send+Sync,并且可以在任何数量的线程中安全使用。

最常见的用例是有一个完全在它自己的异步上下文中处理的状态,但偶尔需要从外部访问——另一个异步上下文。

由于共享数据不需要在 Arc 内部包装,因此它不需要在堆上分配。事实上,BorrowMutex 完全没有进行任何分配。在 tests/borrow_basic.rs 中提供了一个简单的示例,其中 所有内容 都存储在栈上。

安全性

当忘记未来([core::mem::forget()]) 时,API 不稳定。为了方便,API 中没有任何标记为不安全的。

有关详细信息,请参阅 BorrowMutex::lend

希望未来的 Rust 版本可以通过额外的编译器注解禁止不稳定代码。

示例

use borrow_mutex::BorrowMutex;
use futures::FutureExt;

struct TestObject {
    counter: usize,
}

let mutex = BorrowMutex::<16, TestObject>::new();

let f1 = async {
    // try to borrow, await, and repeat until we get an Err.
    // The Err can be either:
    // - the mutex has too many concurrent borrowers (in this example we
    //   have just 1, and the max was 16)
    // - the mutex was terminated - i.e. because the lending side knows it
    //   won't lend anymore
    // We eventually expect the latter here
    while let Ok(mut test) = mutex.request_borrow().await {
        test.counter += 1; // mutate the object!
        println!("f1: counter: {}", test.counter);
        drop(test);
        // `test` is dropped, and so the mutex.lend().await on the
        // other side returns and can use the object freely again.
        // we'll request another borrow in 100ms
        smol::Timer::after(std::time::Duration::from_millis(100)).await;
    }
};

let f2 = async {
    let mut test = TestObject { counter: 1 };
    // local object we'll be sharing

    loop {
        if test.counter >= 20 {
            break;
        }
        // either sleep 200ms or lend if needed in the meantime
        futures::select! {
            _ = smol::Timer::after(std::time::Duration::from_millis(200)).fuse() => {
                if test.counter < 10 {
                    test.counter += 1;
                }
                println!("f2: counter: {}", test.counter);
            }
            _ = mutex.wait_to_lend().fuse() => {
                // there's someone waiting to borrow, lend
                mutex.lend(&mut test).unwrap().await
            }
        }
    }

    mutex.terminate().await;
};

futures::executor::block_on(async {
    futures::join!(f1, f2);
});

两个未来应该可以交替打印。有关完整的工作示例,请参阅 tests/borrow_basic.rs

如果未调用 Drop 会怎样?

不幸的是,会发生 未定义行为。当在 [core::mem::forget()] 或类似的 LendGuard 上调用时,我们可以让借用检查器相信借出的 &mut T 已不再使用,而实际上它仍在使用

# use borrow_mutex::BorrowMutex;
# use futures::Future;
# use futures::task::Context;
# use futures::task::Poll;
# use core::pin::pin;
struct TestStruct {
    counter: usize,
}
let mutex = BorrowMutex::<16, TestStruct>::new();
let mut test = TestStruct { counter: 1 };

let mut test_borrow = pin!(mutex.request_borrow());
let _ = test_borrow
    .as_mut()
    .poll(&mut Context::from_waker(&futures::task::noop_waker()));

let mut t1 = Box::pin(async {
    mutex.lend(&mut test).unwrap().await;
});

let _ = t1
    .as_mut()
    .poll(&mut Context::from_waker(&futures::task::noop_waker()));
std::mem::forget(t1);
// the compiler thinks `test` is no longer borrowed, but in fact it is

let Poll::Ready(Ok(mut test_borrow)) = test_borrow
    .as_mut()
    .poll(&mut Context::from_waker(&futures::task::noop_waker()))
else {
    panic!();
};

// now we get two mutable references, this is strictly UB
test_borrow.counter = 2;
test.counter = 6;
assert_eq!(test_borrow.counter, 2); // this fails

类似的 Rust 库正是由于这个原因使其 API 不安全 - 调用者的责任是不调用 [core::mem::forget()] 或类似的 (async-scoped)

然而,这种未定义行为在常规代码中很难触发。在未来的对象上调用 [core::mem::forget()] 几乎没有用处。

无运行时依赖