C# async并行Task如何按完成顺序为任务分配递增int序号
并行任务序号递增问题解决方案
原有实现失效原因
- 传参逻辑错误:
int是值类型,并行任务启动时传入的x是当前值的拷贝,多个并行任务拿到的初始序号完全相同,任务内部对参数的修改不会影响外部变量,也不会同步给其他并行任务。 - 非线程安全操作:直接对普通
int做++自增不是原子操作,多线程同时执行时会出现竞态,导致序号重复、跳号。 - 并行等待逻辑缺失:调用
Task.WhenAll时没有正确收集并行任务的执行结果,本地变量x不会随并行任务完成自动更新。 - 逻辑和需求不匹配:需求要求序号随任务完成逐次递增,原有实现提前在任务启动时就传入固定序号,无法适配并行任务完成顺序不确定的特性。
实现思路
不需要把序号作为参数传入各个任务方法,直接在当前流程内维护一个共享计数器,使用线程安全的原子自增方法生成序号,保证多个并行任务同时完成时序号不会错乱:
- 保留Main方法内的计数器变量
int x = 1;,所有任务直接访问该变量,不要作为参数传递。 - 自增操作统一使用
Interlocked.Increment(ref x),该方法是CPU级别的原子操作,多线程下不会出现计数错误,自增后直接返回最新的序号值用于打印。 - 单个await任务、并行执行的任务列表都不需要手动返回序号更新外部变量,任务执行完成后直接调用原子自增方法拿到当前序号,打印带序号的任务描述即可。
Task.WhenAll仅负责等待当前批次所有并行任务执行完成,不需要额外处理返回值更新计数器。- 如果需要在任务触发(启动)时打印描述,将打印逻辑移动到对应任务执行代码的开头,直接读取当前计数器值即可,自增逻辑保持在任务完成位置,符合序号随任务完成递增的规则。
修正后代码示例
using System.Threading; // 其他业务所需using引用 static async Task Main(string[] args) { string sZip = @" some path "; string fZip = @" some path "; string sFolder = @" some path "; string fFolder = @" some path "; int x = 1; // 封装线程安全的序号获取逻辑,任务完成时调用即可拿到正确递增序号 int GetNextSeq() => Interlocked.Increment(ref x) - 1; // 第一个串行任务:删除第一个压缩包 await FirstZipDelete(sZip); Console.WriteLine($"[{GetNextSeq()}] 完成第一个旧压缩包删除"); // 第二批并行任务:下载第一个新压缩包、删除第二个旧压缩包 var sequence2 = new List<Task> { Task.Run(async () => { // 若需要启动时打印,在此处加打印逻辑即可,直接读取x当前值 await FirstZipDownload(sZip); Console.WriteLine($"[{GetNextSeq()}] 完成第一个新压缩包下载"); }), Task.Run(async () => { await SecondZipDelete(fZip); Console.WriteLine($"[{GetNextSeq()}] 完成第二个旧压缩包删除"); }) }; await Task.WhenAll(sequence2); // 第三批并行任务:下载第二个新压缩包、解压第一个压缩包 var sequence3 = new List<Task> { Task.Run(async () => { await SecondZipDownload(fZip); Console.WriteLine($"[{GetNextSeq()}] 完成第二个新压缩包下载"); }), Task.Run(async () => { await FirstZipExtract(sZip, sFolder); Console.WriteLine($"[{GetNextSeq()}] 完成第一个压缩包解压"); }) }; await Task.WhenAll(sequence3); // 最后一个串行任务:解压第二个压缩包 await SecondZipExtract(fZip, fFolder); Console.WriteLine($"[{GetNextSeq()}] 完成第二个压缩包解压"); } // 各任务方法简化定义,不需要再接收序号参数、返回int类型 static async Task FirstZipDelete(string path) { // 内部删除业务逻辑 await Task.CompletedTask; } static async Task FirstZipDownload(string path) { // 内部下载业务逻辑 await Task.CompletedTask; } static async Task SecondZipDelete(string path) { // 内部删除业务逻辑 await Task.CompletedTask; } static async Task SecondZipDownload(string path) { // 内部下载业务逻辑 await Task.CompletedTask; } static async Task FirstZipExtract(string zipPath, string folderPath) { // 内部解压业务逻辑 await Task.CompletedTask; } static async Task SecondZipExtract(string zipPath, string folderPath) { // 内部解压业务逻辑 await Task.CompletedTask; }
补充说明
如果需要保持原有任务方法的签名不变,只需要把任务方法内的return ++x替换为return Interlocked.Increment(ref x),注意此时x要作为可修改的共享变量被任务方法访问(比如改为类级别的静态变量,或者通过闭包捕获),不能作为值参数传入。并行任务执行完成后,调用Task.WhenAll拿到所有任务返回的序号结果,取最大值即可更新本地的x变量,但这种实现方式不如直接封装原子序号生成逻辑简洁,出错概率更高。
内容的提问来源于stack exchange,提问作者Erik Zámečník
相关产品推荐
相关产品推荐

