C# Task与BackgroundWorker线程安全、任务取消及异常问题咨询
1. for循环迭代变量超出集合长度问题
这个是C#经典的循环变量闭包捕获问题:for循环中定义的迭代变量i是整个循环周期内唯一的变量,你在创建Task的lambda表达式中直接捕获了i,而不是捕获循环执行到当前步骤时i的值。当Task真正开始执行时,for循环往往已经执行完毕,此时i的值已经变成了循环终止值(比如_cars.Count为4时,i最终会自增到4,超出索引上限3),就会出现索引越界的情况,调试时观测到i为5大概率是循环过程中_cars长度动态变化叠加捕获问题导致的。
修复方案:在for循环内部定义局部变量接收当前i的值,lambda捕获该局部变量即可:
for (int i = 0; i < _cars.Count; i++) { // 新增局部变量缓存当前索引 int currentIndex = i; if (_cars[currentIndex].Task != null && _cars[currentIndex].Task.Status != TaskStatus.Running && _cars[currentIndex].Task.Status != TaskStatus.Created) { Console.WriteLine($"Making new task for { _cars[currentIndex].Model }"); _cars[currentIndex].tokenSource.Cancel(); _cars[currentIndex].RefreshToken(); _cars[currentIndex].Task = null; } if (_cars[currentIndex].Task == null) { // 用缓存的currentIndex代替原迭代变量i _cars[currentIndex].Task = new Task(() => new Manufacture.Build().MakeCar(_cars[currentIndex].Model), _cars[currentIndex].cancellationToken); _cars[currentIndex].Task.Start(); } }
如果你使用的是C# 5.0以下版本,foreach的循环变量也存在同样的闭包捕获问题,同样需要在循环内定义局部变量接收car的值再传入lambda。
2. 多个任务同时报错问题
这个问题确实和Random类的特性直接相关:Random的默认构造函数使用系统时钟作为种子,而系统时钟的精度只有10~15ms。你所有Task几乎同时启动,每个Task的MakeCar方法每次循环都重新new Random(),这会导致多个Random实例拿到相同的系统时钟种子,生成的随机数序列完全一致,自然会同时命中chaos>=90的异常触发条件,即使你把概率调低到1%也会同时触发。
修复方案:将Random改为静态实例,加锁访问,避免重复创建:
namespace ThreadTest.Manufacture { class Build { // 全局唯一的Random实例 private static readonly Random _random = new Random(); private static readonly object _lockObj = new object(); public void MakeCar(string car) { try { while (true) { int chaos; // 加锁保证随机数获取线程安全 lock(_lockObj) { chaos = _random.Next(0, 100); } Console.WriteLine($"Building {car}"); Thread.Sleep(2000); if (chaos >= 90) throw new Exception($"Something went wrong with {car}"); } } catch(Exception ex) { Console.WriteLine($"Error: {ex.Message}"); throw; } } } }
3. 任务取消逻辑的问题
你的取消逻辑不符合规范,核心问题是取消令牌没有生效:你给Task传入了cancellationToken,但在MakeCar的执行逻辑中完全没有检查该令牌的取消状态,就算你调用了tokenSource.Cancel(),任务也不会响应取消,会一直执行到抛出异常或者正常结束。
修复方案:在MakeCar的循环中加入取消检查:
// 方法参数传入取消令牌 public void MakeCar(string car, CancellationToken token) { try { // 每次循环都检查是否触发取消 while (!token.IsCancellationRequested) { // 原有业务逻辑 } } catch(Exception ex) { Console.WriteLine($"Error: {ex.Message}"); throw; } }
另外你现有实现还有以下可优化点:
- Timer的Elapsed事件中用空循环等待BackgroundWorker空闲会无意义占用CPU资源,建议将Timer的
AutoReset属性设为false,每次BackgroundWorker执行完成后再手动调用Timer的Start方法。 - Task抛出异常后你没有处理异常观察,未被观察的Task异常会在GC回收时触发
UnobservedTaskException,严重时会导致程序崩溃,建议要么在Task的延续操作中处理异常,要么注册全局的UnobservedTaskException事件处理逻辑。 - 直接判断Task状态的方式不够可靠,建议结合取消令牌和任务延续逻辑来管理任务的生命周期,而不是依赖状态判断。
内容的提问来源于stack exchange,提问作者user1257758

