非异步方法中Task执行异常:直接运行idList为空,调试步进正常
问题分析与解决方案
首先,你遇到的问题核心有两个:循环闭包导致的变量捕获陷阱,以及非线程安全集合的并发修改问题。断点步进时正常是因为手动调试会让每个任务在循环迭代阶段就执行完毕,变量值能保持当前迭代的正确状态;但直接运行时,任务会在循环结束后才批量执行,此时捕获的循环变量已经变成了循环终止的数值,导致所有任务都请求了错误的页面,自然没有数据加入idList。
具体问题拆解
闭包捕获循环变量的坑
你的for循环里,i是在循环外声明的变量,所有任务的lambda表达式捕获的都是同一个i的引用。当任务真正启动执行时,循环已经跑完,i的值已经变成了response.meta.page.total_pages,所以所有任务都会调用GetDocuments(i+1)——也就是请求超出总页数的页面,返回的response.results自然是空的,idList也就没有数据了。List
的线程安全问题
哪怕闭包问题解决了,多个任务同时调用idList.AddRange()也会引发并发冲突,可能出现数据丢失、重复甚至异常,因为List<T>的方法本身不是线程安全的。任务创建方式的冗余与风险
使用new Task(() => { ... }).Start()不如直接用Task.Run()简洁安全,而且你在lambda里调用异步方法的.Result,容易引发潜在的死锁风险(虽然你用了ConfigureAwait(false)降低了概率,但依然不推荐这种写法)。
修复后的代码示例
var currentPage = 1; List<Result> idList = new List<Result>(); // 用GetAwaiter().GetResult()替代.Result,降低死锁风险 var response = this.GetDocuments(currentPage).GetAwaiter().GetResult(); idList.AddRange(response.results); // 先把第一页数据加入集合 var tasks = new List<Task>(); var totalPages = response.meta.page.total_pages; var lockObj = new object(); // 用于线程同步的锁对象 // 从第2页开始循环,避免重复获取第一页 for (var i = 1; i < totalPages; ++i) { var page = i + 1; // 关键!在循环内部声明变量保存当前页码,避免闭包捕获问题 // 用Task.Run创建异步任务,直接使用async lambda tasks.Add(Task.Run(async () => { var pageResponse = await this.GetDocuments(page).ConfigureAwait(false); if (pageResponse?.results != null) { lock (lockObj) { // 加锁保护非线程安全的List,避免并发冲突 idList.AddRange(pageResponse.results); } } })); } Task.WhenAll(tasks).Wait(); // 等待所有任务完成
额外优化建议
- 如果业务允许,尽量把整个方法改成异步方法,用
await Task.WhenAll(tasks)替代.Wait(),这样能彻底避免死锁风险,代码也更符合异步编程的规范。 - 要是不关心数据顺序,可以用
ConcurrentBag<Result>替代List<Result>,它是线程安全的集合,不需要手动加锁,能简化代码。
内容的提问来源于stack exchange,提问作者Hawkzey
相关产品推荐
相关产品推荐

