随着 Rust 语言的不断发展,使用 Rust 完成后台服务场景的应用越来越多,本次将通过使用 Tokio 库来完成一个文件异步传输的场景,并针对大文件异步传输过程中出现的阻塞现象进行分析并提出解决思路
众所周知,Tokio库是基于异步编程模型构建的。异步编程模型可以使代码更加高效地利用系统资源,例如通过使用事件驱动的回调机制和非阻塞 I/O,使得在等待 I/O 操作完成时能够处理其他任务。同时还具备优秀的大规模并发表现:Tokio 的设计目标之一是支持大规模并发连接。它通过使用多线程和非阻塞 I/O 等技术,能够同时处理数万个连接,从而满足高并发场景下的需求。
利用 Tokio 提供的工具类, 我们实现一个非常简单的文件传输方法如下:
async fn transfer_test(mut socket: TcpStream, filename: String) -> tokio::io::Result<()>
{
let mut file = File::open(filename).await?;
// 读取文件的元数据
let metadata = fs::metadata(&filename).await?;
// 将文件大小写入到socket中,以便接收端预分配空间
socket.write_u64(metadata.len()).await?;
// 将文件内容写入socket
io::copy(&mut file, &mut socket).await?;
Ok(())
}
上述方法实现了一个最简单的文件数据传输逻辑,可很快便遇到了一个大文件( >3GB )传输的问题,大文件的传输导致此传输方式出现了长时间的阻塞执行现象,这对于一个异步IO传输来说,会直接影响异步tokio底层Runtime调度切换效率。
虽然Tokio 在底层Runtime 设计上对 CPU 密集计算等长时间同步阻塞类任务已经有了考虑,并提供了spawn_blocking将长时间阻塞操作放入专门的线程池进行处理,但这个方法只能放入同步 API ,导致我们无法共享使用包括tokio TcpStream在内的异步上下文资源,而需要为大文件传输再重头新写一整套基于同步传输的逻辑又非常麻烦,有没有更方便的处理方式呢?
这里就提到tokio的一个工具类SyncIoBridge,它实现了异步 IO与同步 IO 之间的桥接,通过在方法中进行简单的包装使用,我们就可以实现在异步上下文中直接使用同步 API,从而将大文件的耗时传输通过spawn_blocking直接放到专用线程池中执行,核心示意代码如下:
fn sync_transfer_largefile_demo(mut socket: tokio::net::TcpStream, filename: String) -> Result<()>
{
let mut file = File::open(filename)?;
//将异步的 Socket桥接为 同步Socket
let mut socket_sync = tokio_util::io::SyncIoBridge::new(socket);
socket_sync.write_u64(metadata.len())?;
// 将文件内容同步写入socket
std::io::copy(&mut file, &mut socket_sync)?;
Ok(())
}
通过这个方法,将阻塞操作进行分离,不再占用异步任务专用线程池的资源。
上述优化方式是相对简单的一种,无论是同步还是异步,大文件的传输本身就需要消耗大量的IO资源,此优化只能在一定程度上改善异步整体传输体验,未来还可以通过更复杂的传输算法和传输方式的优化,来进一步提升传输体验和速度。