简体   繁体   English

Rust中的共享互斥锁问题(为Arc实现AsyncRead/AsyncWrite <mutex<ipstack> &gt;) </mutex<ipstack>

[英]The shared mutex problem in Rust (implementing AsyncRead/AsyncWrite for Arc<Mutex<IpStack>>)

Suppose I have an userspace TCP/IP stack.假设我有一个用户空间 TCP/IP 堆栈。 It's natural that I wrap it in Arc<Mutex<>> so I can share it with my threads.我很自然地将它包装在Arc<Mutex<>>中,这样我就可以与我的线程共享它。

It's also natural that I want to implement AsyncRead and AsyncWrite for it, so libraries that expect impl AsyncWrite and impl AsyncRead like hyper can use it.我想为其实现AsyncReadAsyncWrite也是很自然的,因此期望impl AsyncWriteimpl AsyncRead hyper的库可以使用它。

This is an example:这是一个例子:

use core::task::Context;
use std::pin::Pin;
use std::sync::Arc;
use core::task::Poll;
use tokio::io::{AsyncRead, AsyncWrite};

struct IpStack{}

impl IpStack {
    pub fn send(self, data: &[u8]) {
        
    }
    
    //TODO: async or not?
    pub fn receive<F>(self, f: F) 
        where F: Fn(Option<&[u8]>){
        
    }
}

pub struct Socket {
    stack: Arc<futures::lock::Mutex<IpStack>>,
}

impl AsyncRead for Socket {
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>
    ) -> Poll<std::io::Result<()>> {
        //How should I lock and call IpStack::read here?
        Poll::Ready(Ok(()))
    }
}

impl AsyncWrite for Socket {
    fn poll_write(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<Result<usize, std::io::Error>> {
        //How should I lock and call IpStack::send here?
        Poll::Ready(Ok(buf.len()))
    }
    //poll_flush and poll_shutdown...
}

Playground 操场

I don't see anything wrong with my assumptions and I don't see another better way to share a stack with multiple threads unless I wrap it in Arc<Mutex<>>我看不出我的假设有什么问题,而且我看不到另一种与多个线程共享堆栈的更好方法,除非我将它包装在Arc<Mutex<>>

This is similar to try_lock on futures::lock::Mutex outside of async?这类似于在 async 之外的 futures::lock::Mutex 上的 try_lock? which caught my interest.这引起了我的兴趣。

How should I lock the mutex without blocking?我应该如何在不阻塞的情况下锁定互斥锁? Notice that once I got the lock, the IpStack is not async, it has calls that block.请注意,一旦我获得了锁, IpStack就不是异步的,它调用了那个块。 I would like to implement async to it too, but I don't know it the problem will get much harder.我也想对其实现异步,但我不知道问题会变得更加困难。 Or would the problem get simpler if it had async calls?或者如果它有异步调用,问题会变得更简单吗?

I found the tokio documentation page on tokio::sync::Mutex pretty helpful: https://docs.rs/tokio/1.6.0/tokio/sync/struct.Mutex.html我发现tokio::sync::Mutex上的 tokio 文档页面非常有用: https://docs.rs/tokio/1.6.0/tokio/sync/struct.Mutex.html

From your description it sounds you want:从你的描述听起来你想要:

  • Non-blocking operations非阻塞操作
  • One big data structure that manages all the IO resources managed by the userspace TCP/IP stack一种管理用户空间 TCP/IP 堆栈管理的所有 IO 资源的大数据结构
  • To share that one big data structure across threads跨线程共享一个大数据结构

I would suggest exploring something like an actor and use message passing to communicate with a task spawned to manage the TCP/IP resources.我建议探索像演员这样的东西,并使用消息传递与产生的用于管理 TCP/IP 资源的任务进行通信。 I think you could wrap the API kind of like the mini-redis example cited in tokio 's documentation to implement AsyncRead and AsyncWrite .我认为您可以包装 API 类似于tokio的文档中引用的mini-redis示例来实现AsyncReadAsyncWrite It might be easier to start with an API that returns futures of complete results and then work on streaming.从返回完整结果的期货然后处理流媒体的 API 开始可能更容易。 I think this would be easier to make correct.我认为这更容易纠正。 Could be fun to exercise it with loom .织机锻炼它可能会很有趣。

I think if you were intent on synchronizing access to the TCP/IP stack through a mutex you'd probably end up with an Arc<Mutex<...>> but with an API that wraps the mutex locks like mini-redis .我认为,如果您打算通过互斥锁同步对 TCP/IP 堆栈的访问,您可能最终会得到一个Arc<Mutex<...>> ,但会得到一个像mini-redis一样包装互斥锁的 API 。 The suggestion the tokio documentation makes is that their Mutex implementation is more appropriate for managing IO resources rather than sharing raw data and that does fit your situation I think. tokio文档提出的建议是,他们的 Mutex 实现更适合管理 IO 资源,而不是共享原始数据,我认为这确实适合您的情况。

You should not use an asynchronous mutex for this.应该为此使用异步互斥锁。 Use a standard std::sync::Mutex .使用标准std::sync::Mutex

Asynchronous mutexes like futures::lock::Mutex and tokio::sync::Mutex allow locking to be awaited instead of blocking so they are safe to use in async contexts.futures::lock::Mutextokio::sync::Mutex这样的异步互斥锁允许等待锁定而不是阻塞,因此它们在async上下文中使用是安全的。 They are designed to be used across await s.它们旨在跨await使用。 This is precisely what you don't want to happen!这正是您不希望发生的事情! Locking across an await means that the mutex is locked for potentially a very long time and would prevent other asynchronous tasks wanting to use the IpStack from making progress.锁定await意味着互斥锁可能被锁定很长时间,并且会阻止其他想要使用IpStack的异步任务取得进展。

ImplementingAsyncRead / AsyncWrite is straight-forward in theory: either it can be completed immediately, or it coordinates through some mechanism to notify the context's waker when the data is ready and returns immediately.实现AsyncRead / AsyncWrite在理论上是直截了当的:要么可以立即完成,要么通过某种机制进行协调,在数据准备好并立即返回时通知上下文的唤醒器。 Neither case requires extended use of the underlying IpStack , so its safe to use a non-asynchronous mutex.这两种情况都不需要扩展使用底层IpStack ,因此使用非异步互斥锁是安全的。

use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite};

struct IpStack {}

pub struct Socket {
    stack: Arc<Mutex<IpStack>>,
}

impl AsyncRead for Socket {
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        let ip_stack = self.stack.lock().unwrap();

        // do your stuff

        Poll::Ready(Ok(()))
    }
}

I don't see another better way to share a stack with multiple threads unless I wrap it in Arc<Mutex<>> .除非我将它包装在Arc<Mutex<>>中,否则我看不到另一种与多个线程共享堆栈的更好方法。

A Mutex is certainly the most straightforward way to implement something like this, but I would suggest an inversion of control. Mutex无疑是实现此类功能的最直接方法,但我建议控制反转。

In the Mutex -based model, the IpStack is really driven by the Socket s, which consider the IpStack to be a shared resource.在基于Mutex的 model 中, IpStack实际上是由Socket驱动的,它认为IpStack是共享资源。 This results in a problem:这会导致一个问题:

  • If a Socket blocks on locking the stack, it violates the contract of AsyncRead by spending an unbounded amount of time executing.如果Socket在锁定堆栈时阻塞,它会花费无限的时间来执行,从而违反了AsyncRead的约定。
  • If a Socket doesn't block on locking the stack, choosing instead to use try_lock() , it may be starved because it doesn't remain "in line" for the lock.如果Socket在锁定堆栈时没有阻塞,而是选择使用try_lock() ,它可能会被饿死,因为它不会保持“排队”锁定。 A fair locking algorithm, such as that provided by parking_lot , can't save you from starvation if you don't wait.如果您不等待,公平的锁定算法(例如parking_lot提供的算法)无法让您免于饥饿。

Instead, you might approach the problem the way the system network stack does.相反,您可能会以系统网络堆栈的方式处理问题。 Sockets are not actors: the network stack drives the sockets, not the other way around. Sockets 不是参与者:网络堆栈驱动 sockets,而不是相反。

In practice, this means that the IpStack should have some means for polling the sockets to determine which one(s) to write to/read from next.在实践中,这意味着IpStack应该有一些方法来轮询 sockets 以确定下一个要写入/读取的对象。 OS interfaces for this purpose, though not directly applicable, may provide some inspiration.用于此目的的操作系统接口虽然不能直接应用,但可能会提供一些启发。 Classically, BSD provided select(2) and poll(2) ;经典地,BSD 提供了select(2)poll(2) these days, APIs like epoll(7) (Linux) and kqueue(2) (FreeBSD) are preferred for large numbers of connections.如今, epoll(7) (Linux) 和kqueue(2) (FreeBSD) 等 API 成为大量连接的首选。

A dead simple strategy, loosely modeled on select / poll , is to repeatedly scan a list of Socket connections in round-robin fashion, handling their pending data as soon as it is available.一个非常简单的策略,松散地建模在select / poll上,是以循环方式重复扫描Socket连接列表,一旦可用就处理它们的待处理数据。

For a basic implementation, some concrete steps are:对于基本实现,一些具体步骤是:

  • When creating a new Socket , a bidirectional channel (ie one bounded channel in each direction) is established between it and the IpStack .当创建一个新的Socket时,会在它和IpStack之间建立一个双向通道(即每个方向一个有界通道)。
  • AsyncWrite on a Socket attempts to send data over the outgoing channel to the IpStack . Socket上的AsyncWrite尝试通过传出通道向IpStack发送数据。 If the channel is full, return Poll::Pending .如果通道已满,则返回Poll::Pending
  • AsyncRead on a Socket attempts to receive data over the incoming channel from the IpStack . Socket上的AsyncRead尝试通过来自IpStack的传入通道接收数据。 If the channel is empty, return Poll::Pending .如果通道为空,则返回Poll::Pending
  • The IpStack must be driven externally (for instance, in an event loop on another thread) to continually poll the open sockets for available data, and to deliver incoming data to the correct sockets. IpStack必须由外部驱动(例如,在另一个线程上的事件循环中)以不断轮询打开的 sockets 以获取可用数据,并将传入数据传递到正确的 sockets。 By allowing the IpStack to control which sockets' data is sent, you can avoid the starvation problem of the Mutex solution.通过允许IpStack控制发送哪些套接字的数据,您可以避免Mutex解决方案的饥饿问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM