本章导读
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概念
提交-完成模型
关键认知:拿到 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:
#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; }
刚才发生了什么。 ① 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——整读整写的便捷层:
#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; }
刚才发生了什么。 ① 便捷层把"分块提交-收集完成"打包成一次调用——整文件的异步读写不暴露分块细节。② 它解决的是"内容整体才是我要的"场景(配置加载、资源读入)——分块与拼装由库代劳。③ 与第 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 完成后才真正归还;批量提交后的"等齐"不是等其中一个而是等全部。把这句话贴在显示器上,异步代码的八成坑位它都能预防。
常见错误
症状:异步吞吐不升反降;任务池队列积压;同池的其他异步操作集体延迟。
原因:回调占用任务池线程——池的吞吐被重计算吃掉,排队中的 IO 全部等待。
static void onDone(ptr pValue) { ProcessEntireFile(pValue); /* Future 回调里做重计算——占用调度线程 */ }
static void onDone(ptr pValue) { xrtTaskSubmit(gPool, DoHeavyWork, pValue, NULL); /* 轻活:转投任务池 */ } /* 重活在池任务里做——Future 回调只做转发(第 60 章任务体系的用法) */
症状:偶发写入脏数据或读到半新半旧——调用线程在动缓冲,池线程的 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 个条目)
点击查看完整签名、参数约束、返回值与错误说明