本章导读

file-async 模块把文件 IO 从阻塞模型升到提交-完成模型:xrtAsyncFileReadAt/WriteAt 提交操作立即返回 Future——IO 在绑定的任务池线程上执行(第 22 章池的并发版),调用线程拿 Future 继续干别的;xrtFutureWait 等待完成、xrtFutureValue 取回结果(读操作的结果是 xfiledata——数据视图+大小+偏移+EOF 标记)。整文件便捷层 xrtFileReadAllAsync/WriteAllAsync 一步到位。本章讲模型语义、Future 纪律(异步边界的铁律)与同步/异步的取舍。

48.1引入

一个日志归档服务要搬运大文件:同步 Read 一块、处理一块、写一块——单线程串行,磁盘等待时间全部计入处理线程。想并发搬三个文件?同步模型下要三个线程各自阻塞(线程是昂贵的资源,第 55 章的账本)。异步模型换一个思路:提交"读这块"的请求后线程就自由了——系统在后台完成 IO,完成时通知你;一个线程可以同时挂着几十个未完成的 IO。磁盘的并行潜力(NCQ/SSD 多通道)与线程的解放同时发生。

代价是心智模型的切换:从"调用返回即完成"到"调用返回只是受理"——完成时刻、回调线程、失败路径全都变了形状。本章的任务是把这套模型的纪律讲清楚;它也是第 63 章网络引擎(事件驱动)的文件版预演——两章共享同一套"提交-完成"思维。

48.2概念

提交-完成模型

1绑定xrtAsyncFileOpen(路径, 标志, 任务池) → 异步句柄(挂在你创建的有界任务池上)
2提交xrtAsyncFileReadAt/WriteAt(句柄, 偏移, ...) → Future 受理即返回
3执行任务池线程完成 IO;有界队列(如 32)提供背压——池满提交失败不无限堆
4完成FutureWait 等待 / FutureValue 取 xfiledata;Close 也是异步且返回 Future

关键认知:拿到 Future 不等于完成——受理与完成之间,调用线程自由但数据未就绪;一切对结果的使用必须先过 Wait(或卷六的 continuation 组合)。任务池是你创建的(xrtTaskPoolCreate({线程数, 队列深度, 标志}))——有界队列的背压在"池满提交失败"处生效,而不是无限堆积后 OOM(第 21 章背压阀的文件版)。

Future 纪律:异步边界三律

第一律:值是借用的——Future 完成后 FutureValue 取回的 xfiledata 是借用视图,时效到下一次操作;要长期持有自己拷贝(第 3 章借用纪律)。第二律:缓冲冻结期——提交到完成之间,操作的缓冲不可触碰(IO 线程正在读写它);要提前复用就上缓冲池,每块绑一次 IO。第三律:失败也是完成——Future 的失败态与成功态走同一套等待/取值接口;短读以 End 标记 EOF(xfiledata 的布尔位),失败有错误详情——只写成功分支的等待代码在磁盘满时会挂死或误判。

两种等待姿势(选择即架构决策)

姿势入口适用
阻塞等待xrtFutureWait + FutureValue简单场景——链式依赖、批量提交后等齐
Future 组合Future 进卷六组合子(continuation/并发汇聚)并发体系——多 IO 结果的编排

阻塞等待不是倒退:"提交多个 + 统一等待" 的批量模式仍比纯同步快(IO 在池上并行了),只是完成处理串行——文件批处理的务实选择。Future 组合是异步与并发体系的正式握手——第 59 章展开 continuation,第 60 章任务组展开并发汇聚。整读整写的便捷层(async_whole 范例):xrtFileReadAllAsync/WriteAllAsync 整文件一步到位——带硬上限变体(ReadAllLimitAsync——超限即失败,资源边界在异步层的哨位)。

与同步层的取舍(不是所有 IO 都该异步)

先泼一盆冷水:不是所有文件 IO 都该异步。小文件配置读写——同步三行(第 45 章)更简单,异步的复杂度白付。启动时的资源加载——串行语义更清晰,异步收益毫秒级。大文件搬运、并发批处理、IO 密集服务——异步的主场。口诀:IO 次数多、单次大、可并发——异步;零散小读写——同步。混合也常见(启动同步、运行期异步)。

48.3示例

完整程序:异步写读闭环

来自仓库范例 examples/file/async/main.c:

examples/file/async/main.c源码 ↗
#include <stdio.h>
#include <string.h>
#include <xrt.h>



/* 等待 Future 成功并返回借用值。 */
static ptr waitValue(xfuture* pFuture)
{
	if ( (pFuture == NULL) ||
		(xrtFutureWait(pFuture) != XWAIT_OK) ||
		(xrtFutureState(pFuture) != XFUTURE_RESOLVED) ) {
		return NULL;
	}
	return xrtFutureValue(pFuture);
}





















 */


/* 使用有界任务池完成异步绝对偏移读写。 */
int main(void)
{
	xtaskpoolconfig Config = { 2, 32, 0 };
	xfileoptions Options;
	xtaskpool* pPool = xrtTaskPoolCreate(&Config);
	xasyncfile* pFile = NULL;
	xfuture* pWrite = NULL;
	xfuture* pRead = NULL;
	xfuture* pClose = NULL;
	xfiledata* pData;
	int iResult = 1;

	xrtFileOptionsInit(&Options);
	Options.Flags = XFILE_READ | XFILE_WRITE |
		XFILE_CREATE | XFILE_TRUNCATE;
	if ( pPool == NULL ) {
		goto Exit;
	}
	pFile = xrtAsyncFileOpen(
		pPool,
		"xrt-async-example.txt",
		&Options
	);
	if ( pFile == NULL ) {
		goto Exit;
	}
	pWrite = xrtAsyncFileWriteAt(
		pFile,
		0,
		XRT_BYTES_LITERAL("hello async file")
	);
	if ( waitValue(pWrite) == NULL ) {
		goto Exit;
	}
	pRead = xrtAsyncFileReadAt(pFile, 0, 64);
	pData = (xfiledata*)waitValue(pRead);
	if ( pData == NULL ) {
		goto Exit;
	}
	printf(
		"%.*s\n",
		(int)pData->Size,
		(const char*)pData->Data
	);
	pClose = xrtAsyncFileClose(pFile);
	pFile = NULL;
	if ( (pClose == NULL) ||
		(xrtFutureWait(pClose) != XWAIT_OK) ||
		(xrtFutureState(pClose) != XFUTURE_RESOLVED) ) {
		goto Exit;
	}
	iResult = 0;

Exit:
	if ( pFile != NULL ) {
		xfuture* pPending = xrtAsyncFileClose(pFile);

		if ( pPending != NULL ) {
			(void)xrtFutureWait(pPending);
			xrtFutureDestroy(pPending);
		}
	}
	xrtFutureDestroy(pWrite);
	xrtFutureDestroy(pRead);
	xrtFutureDestroy(pClose);
	if ( pPool != NULL ) {
		(void)xrtTaskPoolDestroy(pPool);
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/file/async/main.c -lws2_32 -liphlpapihello async file

刚才发生了什么。 ① xtaskpoolconfig Config = { 2, 32, 0 }——两线程、队列深 32 的有界任务池:IO 在池线程执行、池满时提交失败(背压在文件层的落点)。② xrtAsyncFileOpen(池, 路径, 选项)把文件绑定到池——此后的读写都经池排队。③ xrtAsyncFileWriteAt 与 xrtAsyncFileReadAt 各返回 Future——范例的 waitValue 辅助(Wait + State 检查 + Value 取值)就是阻塞等待姿势的完整形态;读的结果经 xfiledata 携带数据视图与尺寸。④ 读写顺序经 Future 链保证——先等写完成再提交读,链式依赖靠等待显式建立(不等待就并发,顺序无保证)。⑤ Close 也是异步且返回 Future——关闭也要等完成,句柄资源才真正归还;输出 hello async file 即写读闭环验证。

完整程序:整文件异步

来自 examples/file/async_whole/main.c——整读整写的便捷层:

examples/file/async_whole/main.c源码 ↗
#include <stdio.h>
#include <xrt.h>


















 */


/* 使用显式任务池完成原子写入和整文件读取。 */
int main(void)
{
	xtaskpoolconfig Config = { 2, 16, 0 };
	xtaskpool* pPool = xrtTaskPoolCreate(&Config);
	xfuture* pWrite = NULL;
	xfuture* pRead = NULL;
	xfiledata* pData;
	int iResult = 1;
	static const char sPath[] = "xrt-async-whole-example.txt";

	if ( pPool == NULL ) {
		goto Exit;
	}
	pWrite = xrtFileWriteAtomicAsync(
		pPool,
		sPath,
		XRT_BYTES_LITERAL("hello async whole file")
	);
	if ( (pWrite == NULL) ||
		(xrtFutureWait(pWrite) != XWAIT_OK) ||
		(xrtFutureState(pWrite) != XFUTURE_RESOLVED) ) {
		goto Exit;
	}
	pRead = xrtFileReadAllAsync(pPool, sPath);
	if ( (pRead == NULL) ||
		(xrtFutureWait(pRead) != XWAIT_OK) ||
		(xrtFutureState(pRead) != XFUTURE_RESOLVED) ) {
		goto Exit;
	}
	pData = (xfiledata*)xrtFutureValue(pRead);
	if ( pData == NULL ) {
		goto Exit;
	}
	printf("%.*s\n", (int)pData->Size, (const char*)pData->Data);
	iResult = 0;

Exit:
	xrtFutureDestroy(pWrite);
	xrtFutureDestroy(pRead);
	if ( pPool != NULL ) {
		(void)xrtTaskPoolDestroy(pPool);
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/file/async_whole/main.c -lws2_32 -liphlpapihello async whole file

刚才发生了什么。 ① 便捷层把"分块提交-收集完成"打包成一次调用——整文件的异步读写不暴露分块细节。② 它解决的是"内容整体才是我要的"场景(配置加载、资源读入)——分块与拼装由库代劳。③ 与第 45 章同步整读的分工:小文件同步三行、大文件或需要并发时走这里——"便捷"不改变模型,只减少样板。

48.4契约

  • 受理即返回:提交函数的返回只表示受理;完成状态经回调或等待对象交付。
  • 回调线程铁律:回调运行在系统 IO 线程——轻活、线程化交接、失败分支齐全。
  • 批量等待:多提交 + 统一等待是合法形态——IO 并行的收益保留,完成处理串行。
  • Future 入口:异步操作可包成 Future 进并发体系(第 59 章)——组合与 continuation 的标准通道。
  • 取舍口诀:多、大、可并发 → 异步;零散小读写 → 同步;混合按阶段选。

从示例到工程:异步文件的三个宿主

大文件搬运(流水线主场):读块 A 的 Future 完成后触发写块 A 与读块 B 的提交——双缓冲轮转、全程零空闲等待;进阶练习就是这个流水线的骨架。批量归档(并发主场):N 个文件各提交整读 → Future 齐了逐个压缩 → 异步写回——池深度天然限制并发(背压内建),第 29 章压缩 + 第 22 章池在异步层的合流。服务启动预载(便捷层主场):ReadAllAsync 并发预载多个资源文件,主流程继续初始化,要用时 Wait——启动时间的重叠利用。三宿主共同点:并发度受池深度管控——异步不是无限并发,是"受控的并行"。

异步与第 60 章任务体系的衔接

本章的 Future 与任务池就是第 60 章任务体系的文件侧入口:xrtTaskSubmit 把任意函数提交进同一个池——文件 IO 与计算任务共享池、统一背压;Future 的组合子(第 59 章)把"读 A + 读 B 都完成后合并"表达为声明式编排;任务组(第 60 章)把"十个归档任务的生命周期与取消"统一管理。本章学的是原语层——池、句柄、Future 三件;卷六把它们装配成完整的并发工作流。带着这个视角预习:本章的每个坑在卷六都有对应的包装层解法。

一个心智锚点:受理 ≠ 完成

本章最重要的一句话值得单独成节:受理不等于完成。同步代码的直觉(调用返回=结果可用)在异步世界失效——Future 是"结果的承诺",不是结果本身。三个具体提醒:拿到 Future 后立即读数据是坑 2 的直接成因;Close 返回 Future 意味着"关闭也要等"——句柄资源在 Close 的 Future 完成后才真正归还;批量提交后的"等齐"不是等其中一个而是等全部。把这句话贴在显示器上,异步代码的八成坑位它都能预防。

常见错误

坑 1:Future 回调里做重活(吞吐杀手)

症状:异步吞吐不升反降;任务池队列积压;同池的其他异步操作集体延迟。

原因:回调占用任务池线程——池的吞吐被重计算吃掉,排队中的 IO 全部等待。

错误写法
static void onDone(ptr pValue)
{
	ProcessEntireFile(pValue);   /* Future 回调里做重计算——占用调度线程 */
}
正确写法
static void onDone(ptr pValue)
{
	xrtTaskSubmit(gPool, DoHeavyWork, pValue, NULL);   /* 轻活:转投任务池 */
}
/* 重活在池任务里做——Future 回调只做转发(第 60 章任务体系的用法) */
坑 2:提交后立刻动缓冲(冻结期纪律)

症状:偶发写入脏数据或读到半新半旧——调用线程在动缓冲,池线程的 IO 也在动它。

原因:缓冲在冻结期(提交到完成)被两侧并发触碰——第二律被跳过。

错误写法
xrtAsyncFileWriteAt(File, Offset, Buffer, Size, NULL);
memset(Buffer, 0, Size);   /* 提交后立刻清缓冲——池线程还在读它 */
正确写法
xrtAsyncFileWriteAt(File, Offset, Buffer, Size, NULL);
/* 缓冲在提交到 Wait 完成期间冻结——完成后才能动它 */
/* 要提前复用:换缓冲池(第 22 章),每块绑一次 IO */

编程练习

基础:双操作并发(异步收益的第一课)

两个文件各提交一次异步写、统一等待——对比"同步写两文件"的总耗时(SSD 上差异明显);输出两版本的耗时对照。

进阶:流水线搬运器

实现 copy_async(源, 目标):读块 A 的完成触发写块 A 与读块 B——流水线交替,全程零空闲等待。提示:固定块数的双缓冲(第 22 章池)轮转。

挑战:并发日志归档器

十个日志文件并发 gzip 归档(第 29 章流式压缩):异步读 + 压缩在工作线程 + 异步写,全链并发。验收标准:总耗时显著低于串行版(给出对照数字);峰值内存有界(并发数受缓冲池限制);第 6 章统计验证零泄漏。

小结

知识点速查
模型提交-完成:受理即返回 Future;Wait 后经 Value 取 xfiledata
任务池异步句柄绑定你创建的有界池;队列深度=背压阀,池满提交失败
Future 三律值是借用 / 缓冲冻结期 / 失败也是完成(End 位标记 EOF)
等待姿势Wait+Value(链式依赖与批量等齐)/ Future 组合(卷六编排)
便捷层ReadAllAsync/WriteAllAsync 整文件异步一步到位;Limit 变体带上限
取舍次数多、单次大、可并发 → 异步;零散小读写 → 同步

📖 本章涉及的 API 参考手册:file_async(41 个条目)file(132 个条目)

点击查看完整签名、参数约束、返回值与错误说明