避免在 Rust 库中重复同步和异步代码

Meg*_*tal 3 asynchronous rust async-await

我最近遇到了一个提供同步和异步接口的库。可以使用功能标志启用异步async,并且异步/同步函数通过编译器指令进行区分。

例如,同步函数如下所示:

#[cfg(not(feature = "async"))]
fn perform_query<A: ToSocketAddrs>(&self, payload: &[u8], addr: A) -> Result<Vec<u8>>
{
    // More than 100 lines of code with occasional calls to sync UdpSocket::send_to and recv.
}
Run Code Online (Sandbox Code Playgroud)

这就是异步函数的样子:

#[cfg(feature = "async")]
async fn perform_query<A: ToSocketAddrs>(&self, payload: &[u8], addr: A) -> Result<Vec<u8>>
{
    // More than 100 lines of code with occasional calls to async UdpSocket::send_to and recv.
    // Apart from 3-4 await lines, it does mostly the same thing as its sync counterpart.
}
Run Code Online (Sandbox Code Playgroud)

我发现并修复了同步代码中的一些错误,现在我也即将修复异步代码。但后来我注意到,由于这个大函数是完全重复的,我需要将我的修复补丁到异步函数中,然后我开始思考,为什么这个函数的大部分首先是重复的?从长远来看,维护这段代码似乎是地狱,所以我想通过删除这个函数的重复数据来帮个忙……然后我遇到了一些问题,这些问题让我意识到它并不像我想象的那么微不足道。我肯定可以用编译器指令区分这几行,我什至可以编写一个宏,UdpSocket根据是否async启用该功能来插入调用的同步/异步版本。但后来我意识到我无法通过编译器指令选择函数头,因为这#[cfg...]将适用于整个函数,所以如果我这样做,我会遇到大量语法错误:

#[cfg(not(feature = "async"))]
fn perform_query<A: ToSocketAddrs>(&self, payload: &[u8], addr: A) -> Result<Vec<u8>>
#[cfg(feature = "async")]
async fn perform_query<A: ToSocketAddrs>(&self, payload: &[u8], addr: A) -> Result<Vec<u8>>
{
    // Deduplicated code with occasional differentiation of sync / async UdpSocket calls.
}
Run Code Online (Sandbox Code Playgroud)

我还想到只有核心的异步函数,然后异步和同步包装函数来调用它,无论库是被编译为同步还是异步,但是我无法从同步函数调用异步函数,或者在至少我需要使用异步运行时来执行一些丑陋的魔法awaitpoll然后将结果作为同步传递,但是无论如何,库的同步构建也必须导入异步运行时,这会更好避免了。

我当前的想法是将数据包的处理转移到单独的同步函数中,这些函数将从同步和异步包装器中调用,这些函数仅处理实际的调用UdpSocket,但我不确定这是否是正确的方法。我的意思是,难道没有更流畅、更优雅的方式吗?对此的一般方法是什么?或者为同步和异步构建重复大量的函数是正常的吗?正如您可能猜到的,我没有异步编程的经验。

Kev*_*eid 5

\n

我还想到只有核心的异步函数,然后异步和同步包装函数来调用它 \xe2\x80\xa6 那么库的同步构建也必须导入异步运行时 \xe2\x80\xa6

\n
\n

这就是reqwest提供其阻塞接口的方式。我认为,如果库足够大,异步运行时不会产生大量额外的编译成本,那么这是一种非常好的方法。它的优点是所有 IO 在所有情况下都以完全相同的方式工作,从而减少了出现细微错误的机会。

\n
\n

我当前的想法是将数据包的处理转移到单独的同步函数中,这些函数将从同步和异步包装器中调用,这些函数仅处理实际的UdpSocket调用

\n
\n

我建议您使用此选项 \xe2\x80\x94\xc2\xa0将算法与 IO 分开。它具有超越您当前目标的重复代码删除功能的优势:

\n
    \n
  • 当您可以将数据包算法表示为简单的函数调用 \xe2\x80\x94\xc2\xa0 尤其是处理 IO \xe2\x80\x94 中的边缘情况的函数调用时,为数据包算法编写单元测试可能会更容易,而不是必须这样做设置一个对等 UDP 套接字来测试任何内容。

    \n
  • \n
  • 如果您将算法公开,则可以在不寻常的情况下使用它们,例如不与操作系统的网络堆栈交互的情况:

    \n
      \n
    • no_std environments where the networking is custom and not known to Rust std or your async IO library
    • \n
    • analysis of captures of traffic (non-real-time)
    • \n
    • reimplementing the IO side for special requirements (e.g. passing specific flags to the OS) while still being able to use your library\'s algorithms
    • \n
    \n
  • \n
  • Error handling, a necessary part of IO, may be clearer if it is less intertwined with the algorithms, as such a split would require

    \n
  • \n
\n

This style of library design is sometimes called \xe2\x80\x9csans I/O\xe2\x80\x9d (at least by Python programmers). You can see it in Rust with, for example, the http library which provides HTTP parsing algorithms but no IO whatsoever.

\n