探索 Rust 异步简化编程( 三 )


my_tcp_stream.read(&mut buf).await?;async_drop(my_tcp_stream).await;当然,如果用户忘记调用async drop怎么办?毕竟,编译器在阻塞Rust中会自动处理drop,而且这是个非常强大的功能 。而且,注意上述代码有一个小问题:?操作符在读取错误时会跳过async_drop调用 。Rust编译器能对此问题给出警告,但怎么修复呢?有办法让?与明示的async_drop兼容吗?

探索 Rust 异步简化编程

文章插图
 
去掉.await如果不要求明示的async drop调用,而是去掉await关键字怎么样?同学A就不需要在调用异步函数(如socket.read_u32().await)之后使用.await了 。在异步上下文中调用异步函数时,.await就变成了隐含的 。
似乎这是如今Rust的一大进步 。但我们依然可以对这个假设提出质疑 。隐含的.await只能在异步语句中发生,因此它的应用比较有限,而且依赖于上下文 。同学A只有通过查看函数定义,才能知道自己位于某个异步上下文中 。此外,如果IDE能高亮显示某个yield点,就会非常方便 。
去掉.await还有另一个好处:它能让异步Rust与阻塞Rust一直 。阻塞的概念已经是隐含的了 。在阻塞Rust中,我们并不会写my_socket.read(buffer).block? 。当同学A编写异步聊天服务器时,他注意到的唯一区别就是必须用async关键字来标记函数 。同学A可以凭直觉想象异步代码的执行 。“懒future”的问题不再出现,而同学A也不能无意间做下面的事,并对先输出“two”的情况感到困惑 。
async fn my_fn_one { println!("one");}async fn my_fn_two { println!("two");}async fn mixup { let one = my_fn_one; let two = my_fn_two; join!(two, one);}.await的RFC中的确有一些对于隐含await的讨论 。当时,反对隐含await的最有力的观点是,await调用正好标记了async语句可以被中止的点 。如果采用保证完成的future,这个观点就不那么有力了 。当然,对于可以安全中止的异步语句,我们还应该保留await关键字吗?这个问题需要一个答案 。但无论如何,去掉“.await”是一个非常大的挑战,必须谨慎行事 。需要进行易用性研究,表明其优点大于缺点才行 。
探索 Rust 异步简化编程

文章插图
 
带有作用域的任务到目前为止,同学A已经可以使用异步Rust构建聊天服务器,而且不需要学习太多新概念,也不会遇到无法预测的行为 。他了解了select!,但编译器会强制在类似于通道的类型中进行选择 。除此之外,同学A还给函数添加了async,而且运行良好 。他把代码展示给同学B看,并询问是否需要将套接字放在一个Arc中 。同学B建议他阅读一下带有作用域的任务(scoped tasks),借此避免分配 。
带有作用域的任务等价于crossbeam的“带有作用域的线程”,只不过是异步的 。这个任务可以通过生成者借用数据 。同学A可以使用带有作用域的任务来避免在连接处理函数中使用Arc:
async fn handle_connection(socket: TcpStream, channel: Channel) { task::scope(async |scope| { let read_task = scope.spawn(async || { while let Some(line_in) in parse_line(&socket)? { broadcast_line(line_in)?; } Ok() }); loop { // `channel` and JoinHandle are both "channel-like" types. select! { _ = read_task.join => { // The connection closed or we encountered an error, // exit the loop break; } line_out = channel.recv => { if write_line(&writer, line_out).is_err { break; } } } } });}保证安全的关键是要保证,作用域的生存周期要大于在该作用域范围内生成的所有任务,换句话说,确保异步语句能够完成 。但有一个缺点 。启用带有作用域的任务会使“Future::poll”变得不安全,因为必须对future的完成情况进行轮询,以保证内存安全性 。这种不安全性会导致Future的实现更难 。为了降低难度,我们需要尽可能避免用户自己实现Future,包括实现类似于AsyncRead、AsyncIterator等traits 。我相信这是一个可以达到的目标 。
除了带有作用域的任务之外,保证异步语句的完成,还可以在使用io-uring或与C++ future集成时,让指针能正确地从任务传递到内核 。某些情况下,还可能在生成子任务时避免分配,对于某些嵌入式环境非常有用,尽管这种情况需要一个略微不同的API 。


推荐阅读