3.4 Go 的磁盘 IO
3.4 Go 的磁盘 IO
Go 的程序员视角看起来,无论收发网络请求,还是读写磁盘,runtime 已经包装成异步形式,自然地使用协程视角去统一处理,不必担心阻塞问题。既然我们已经理解磁盘 I/O 和网络 I/O 在系统调用上的不同,以及应对阻塞、提高效率的方法。那么 Go 语言是如何处理的呢?磁盘 IO 使用了 linux 的异步技术吗?
1 GMP 模型与磁盘 I/O 的交互
- G(Goroutine): 表示一个Go程序的用户级线程,它包含了一个程序计数器和栈等信息。
- P(Processor): 代表一个逻辑处理器,负责调度和执行goroutine。每个P关联一个goroutine队列。
- M(Machine): 代表一个操作系统线程,负责实际的执行。一个M可以绑定一个P。
磁盘I/O流程:
-
当某个 goroutine 发起磁盘读写操作时,该 goroutine 会被分配到系统线程(M)上执行。由于同步 I/O 操作具有阻塞特性,会导致当前 M 进入阻塞状态。
-
为避免单个 M 的阻塞影响处理器(P)及其管理的其他 goroutine 的执行效率,Go 运行时系统会智能地进行资源重组:
- 在满足特定条件时(如 I/O 完成或超时触发)
- 运行时会将 P 从阻塞的 M 上解绑
-
同时,运行时系统会创建新的 M 并与解绑的 P 重新关联,确保该 P 能继续调度执行其他就绪的 goroutine,维持程序的并发性能。
-
当原始 M 完成 I/O 操作后:
- 首先尝试重新获取可用的 P 继续执行
- 若无法立即获取 P,则该 M 会转入空闲状态
- 被阻塞的 goroutine 在获得执行资源后会被重新调度
原来如此,其实算是一种我们熟悉的模式:同步 IO 线程池模型。核心思想是既然阻塞,就专门扔到一个线程池去做,IO 完成后原来的协程继续执行。在用户层面这个操作是 “异步” 的。
这种技术选择,往往和项目启动时的技术栈、可移植性等等很多因素有关,有些 issue1 也探讨了 Go io_uring 的可行性和收益。
2 Tokio 的阻塞处理
在 Rust 异步框架 Tokio
中,也是类似的思路。提供 tokio::task::spawn_blocking
接口,供用户将阻塞操作自行扔进专用线程池,避免阻塞整个 runtime。
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
async fn write_file() -> std::io::Result<()> {
// tokio 框架自带的异步文件 IO
let mut file = File::create("foo.txt").await?;
file.write_all(b"Hello, Tokio!").await?;
// 如果必须用同步库(如std::fs)
tokio::task::spawn_blocking(|| {
std::fs::write("bar.txt", b"Blocking write").unwrap();
}).await?;
Ok(())
}