为何多Task执行时函数参数值混乱,结果出现大量重复?
问题原因分析
1. 错误共享值类型实例(或闭包延迟绑定问题)
你的代码存在一个核心错误:直接通过ExampleStruct类型访问实例字段number和otherData(代码里写成zip是笔误)。C#中值类型(struct)的实例字段必须通过具体实例访问,强行通过类型访问时,编译器会隐式创建一个静态默认实例,所有Task都会共享这个实例的字段值。
循环中你不断修改这个共享实例的number值,但Task.Run是异步执行的——当Task真正启动执行GetDataFor时,循环可能已经跑完最后一次迭代(i=999),所以大部分Task都会读取到最后设置的999。
退一步说,即便你创建了局部实例,在旧版C#(C#5之前)的for循环中,闭包是引用捕获循环内的变量,所有Task会指向同一个变量引用,最终也会导致多数Task拿到最后一次循环的值。
2. List非线程安全引发竞争条件
List<T>的Add方法没有线程安全保护,多个Task同时调用results.Add()时会出现竞争:比如两个线程同时写入列表同一位置,或列表扩容时内部状态不一致,这会导致元素被覆盖、丢失,最终出现重复值或缺失值。
修复方案
步骤1:创建独立实例并正确捕获
在循环内部创建专属的struct实例,确保每个Task拿到当前迭代的独立数据。C#5及以后版本中,循环内声明的局部变量会在每次迭代时创建新实例,直接使用即可:
for(int i = 0; i < 1000; i++) { ExampleStruct data = new ExampleStruct(); data.number = i.ToString(); data.otherData = "3333"; TList.Add(Task.Run(() => GetDataFor(data)).ContinueWith(ct => incrementProgress(worker))); }
步骤2:改用线程安全集合
用ConcurrentBag<string>(适用于无序场景)替代List<string>,它内置线程安全机制,避免并发写入的竞争问题:
// 替换原List<string> ConcurrentBag<string> results = new ConcurrentBag<string>(); private void GetDataFor(ExampleStruct dataInput) { results.Add(dataInput.number.ToString()); }
完整修复代码示例
public struct ExampleStruct { public string number; public string otherData; } ConcurrentBag<string> results = new ConcurrentBag<string>(); List<Task> TList = new List<Task>(); void mainFunc() { // 如果要1-1000,将i的起始值改为1,循环条件改为i<=1000 for(int i = 0; i < 1000; i++) { ExampleStruct data = new ExampleStruct(); data.number = i.ToString(); data.otherData = "3333"; TList.Add(Task.Run(() => GetDataFor(data)).ContinueWith(ct => incrementProgress(worker))); } Task.WaitAll(TList.ToArray()); // 此时results包含0-999所有值,顺序可能无序,但每个值仅出现一次 } private void GetDataFor(ExampleStruct dataInput) { results.Add(dataInput.number.ToString()); }
内容的提问来源于stack exchange,提问作者Gustav
相关产品推荐
相关产品推荐

