#async-io #stdio #async #stdin #io-stream #stderr #stdout

async-blocking-stdio

std::io::std{in(), out(), err()} 但异步

2个版本

0.1.1 2024年8月14日
0.1.0 2024年8月14日

#277 in 异步

Download history 192/week @ 2024-08-10

192 每月下载量

GPL-3.0-or-later

35KB
312

async-blocking-stdio

异步版本的 std::io::stdoutstd::io::stderrstd::io::stdin 的句柄

快速示例

此示例锁定 stdout 并打印一些文本。注意我们如何在 await 点保持锁。

stdoutstderr 在程序退出时会被刷新,就像正常的标准输出一样。然而,如果你希望确保数据提供的时间,也可以手动刷新(在同步API使用时,存在一些 注意事项)。

futures_lite::future::block_on(async {
    use async_blocking_stdio as astdio;
    use futures_lite::io::AsyncWriteExt as _;
    let mut stdout_handle = astdio::stdout().lock().await;
    stdout_handle.write_all(b"Hello world!\n").await?;
    stdout_handle.write_all(b"Hello jay\n").await?;
    stdout_handle.write_all(b"Hiiiiiiii\n").await
}).unwrap()

还有一个方便的扩展特质,可以直接从 std::io 获取句柄

futures_lite::future::block_on(async {
    use futures_lite::io::AsyncWriteExt as _;
    // you could do this: use async_blocking_stdio::StdioExt as _;
    // but there is also a prelude:
    use async_blocking_stdio::prelude::*;
    let mut stdout_handle = std::io::Stdout::async_handle().lock().await;
    stdout_handle.write_all(b"Hello world!\n").await?;
    stdout_handle.write_all(b"Hello jay\n").await?;
    stdout_handle.write_all(b"Hiiiiiiii\n").await
}).unwrap()

主要功能

此crate在许多方面与标准库中的同步版本的功能类似。

你可以通过以下函数访问各种句柄:

与标准库的句柄不同,您不能直接使用自动锁定在这些句柄上执行IO操作(由于异步版本的io trait的结构)- 这将需要特殊的状态。

相反,您只能访问标准库的“副本”,使其能够创建可操作的stdio流的锁定版本。这个锁可以在await点之间保持(它是一个异步兼容的同步原语),并且可以使用由futures_lite::io定义的异步IO trait操作。

与标准库的stdio锁不同,该库产生的锁不是可重入的。这意味着在await一个尝试锁定相同结构的未来的等待期间保持锁会导致死锁 - 尽管如果该函数使用同步版本,则不会发生这种情况,因为标准库锁是可重入的。

动机

目前,在异步上下文中使用Stdio句柄的主要方法要么是直接以同步方式读取或写入它们(相信它们的延迟足够低以避免阻塞异步执行器),要么使用blocking::Unblock包装流。

前者允许您锁定stdio流以确保一组IO操作不会交错 - 虽然您不能在await点保持Mutex到标准IO流,否则会导致问题 - 而后者不允许您执行锁定,但允许您执行异步IO。

它不允许您通过锁来防止交错的重要原因是,您不能使用到标准流的锁定句柄来构建一个Unblock,因为它不是Send - 相反,您需要使用按需锁定句柄(StdoutStderrStdin)。

即使您可以使用锁定句柄,由于每次操作stdio流时都会创建新的管道资源(例如,非常大的缓冲区),并且每次都必须创建一个新的blocking::Unblock,这会非常低效。

此外,由于输入缓冲,它还会在标准输入上造成大量数据丢失,因为每个未阻塞的结构都有自己的内部输入,并且每次您完成使用这样的句柄进行输出操作时,您都需要等待刷新输出,然后再销毁临时的异步流(这将比仅使用全局同步接口低效得多)。

此软件包主要针对相对复杂的使用场景。对于简单场景,您可能只需考虑使用标准的IO流(即使它们是同步的),因为它们本身具有一些内部缓冲,可能满足您的需求,如果您不处理复杂的异步任务。此软件包的主要功能包括

  • 它为每个标准IO流设置一个唯一的全局实例,可以异步访问和锁定。这意味着,只要您使用此软件包并考虑到与其他用户未通过此软件包的注意事项,所有异步访问都避免了交错。
    这与每个希望异步使用stdio的库都设置自己的blocking::Unblock,并使用全局的std::io处理不同。因为这些会有各自独立的缓冲区。
    然后,每个写入不同异步处理函数的功能会偶尔与其他异步写入交错,因为偶尔需要解锁和重新锁定底层未阻塞的std::io处理。即使在刷新的情况下,如果发送的数据量足够大,这也会适用。
    这是注意事项部分所述的原因。
  • 共享的全局实例还允许全局缓冲标准IO,避免了上述使用临时缓冲(关于数据丢失和刷新)的所有问题。
  • 它设置了异步流版本,试图在程序退出时刷新的行为与同步流非常相似。这避免了需要从程序内部手动刷新(除非您想确保在特定点数据可见)的需要。
  • 它允许您在await点之间使用异步友好的锁定版本流,这在您想在连续的、原子的块中读取或写入stdio时很有用,即使子任务可能需要等待。

解决方案

此软件包通过提供对包含blocking::Unblock包装版本的std::io::Stdoutstd::io::Stderrstd::io::Stdin处理的全局静态的统一接口来解决此问题 - 这些静态本身通过异步安全的互斥锁进行同步,处理程序可以通过crate::stdoutcrate::stderrcrate::stdin获取。

它还反映了标准库实现的一些功能 - 例如,在程序退出时尝试刷新任何缓冲区(由于底层使用blocking::Unblock,这非常重要 - 这内部缓冲区相当大)。

具体的实现细节可能在将来有所不同(例如,这个crate不关心公平锁定,只保证输出不会被破坏,并且后端不总是保证保持blocking::Unblock),但语义保持相似。

所使用的解决方案 - 使用标准库 std-stream 处理程序的 "单操作自动锁定" 版本 - 与标准 IO 流的非异步用户或从标准库直接构建自己的 blocking::Unblock 包装器的用户交互时有一些注意事项

注意事项

因为底层阻塞实现需要使用 std::io::Stdoutstd::io::Stderrstd::io::Stdin 的非显式同步版本,如果它必须执行多个底层 io 操作,那么相同流同步用户可能会将其 IO 调用与库的异步用户混合。

这基本上是不可避免的,但如果你想确保没有混合,你也可以通过下面描述的方法进行同步访问。

对于 stdin,这意味着你总是想确保在锁定后(无论是同步还是异步)进行任何输入,并且由于混合操作的注意事项,你应该强烈考虑只使用一个顶级句柄来管理从 stdin 的输入。

同步访问

即使大多数程序是异步的,你也可能想在流上执行一些同步操作。为此,有一些有用的知识

  • 句柄提供使用异步锁定的能力,但它们还提供在同步上下文中锁定它们的方法(locklock_blocking,还有 try_lock)。然而,你绝不应该在异步上下文中使用同步锁定。
  • 你可以使用 BlockOn 从异步流获取同步流。文档警告需要刷新事物,但实际上,这不需要在持有同步包装器类型时发生(你可以将其留给异步代码,或者对于这个库,如果它足够可靠以满足你的需求,留给清理基础设施)。
    同时,还有一些实用函数可以直接在锁定时获取包装好的句柄,这些函数可以在从 crate::stdoutcrate::stderrcrate::stdin 获取的句柄类型中找到。

依赖关系

~1MB
~17K SLoC