如何等待BackgroundWorker全部任务完成后再启动新后台任务
最初遇到UI阻塞问题:现有两个需要配合其他代码按顺序执行的外部进程,每个进程需执行n次,进程运行时会导致UI冻结。为解决该问题,最开始在代码中使用BackgroundWorker对象,将第一个进程ProcessA、第二个进程ProcessB分别放入BackgroundWorker中执行。
启动应用执行整体任务时,ProcessA运行速度异常快并输出结果(暂未明确运行过快的原因),输出结果校验后正确。ProcessA共需执行n个步骤,每个步骤都会新建一个BackgroundWorker实例在后台执行对应任务。
按照逻辑,第二个进程必须在第一个进程全部执行完成后才能启动,但实际遇到的问题是:第一个进程的任务完成事件未按预期触发,第二个进程会在第一个进程未完成时提前启动,对应实现代码如下:
public void ButtonClick(object sender, RoutedEventArgs e) { for (int i = 0; i < steps + 1; i++) { backgroundworker = new BackgroundWorker(); backgroundworker.DoWork += new DoWorkEventHandler(BackgroundWorker_DoWork); backgroundworker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted; backgroundworker.RunWorkerAsync(Tuple.Create(mypath, i)); } While(!FirstProcessFinished) Thread.Sleep(1000); RunSecondProcess(mypath); } protected void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { // 运行第一个进程的相关代码 } protected void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { Counter++; IncrementProgressBar(); Thread.Sleep(); Console.WriteLine("Finished"); if (counter == Steps + 1) { FirstProcessFinished = true; } }
核心疑问:如何在n次backgroundWorker_Completed回调全部触发后,正确执行ProcessB?
问题更新
测试了基于Task的实现方案后出现新问题:第二个任务无法执行,第一个任务也无法走完流程,对应测试代码如下:
private async void Work(string path) { try { // 启动所有A任务 var aTasks = new List<Task>(); for (int i = 0; i < steps; i++) { aTasks.Add(new Task(() => RunA(path, i))); } // 等待所有任务结束 await Task.WhenAll(aTasks); IncrementProgressBar(); // 启动B任务 await Task.Run(() => RunB(path)); IncrementProgressBar(); } catch (Exception e) { Console.WriteLine(e); throw; } } private async Task RunA(string path, int index) { // 启动实际工作(新任务避免冻结UI) await Task.Run(() => WorkA(path, index)); // 回到UI线程更新状态 IncrementProgressBar(); Console.WriteLine($"Task A{index:00} Finished"); }
疑问:这段代码的问题出在哪里?
原BackgroundWorker方案失效原因
核心问题是UI线程死锁:RunWorkerCompleted回调依赖UI线程的消息循环调度才能执行,但你在UI线程上写了while(!FirstProcessFinished) { Thread.Sleep(1000); }的阻塞循环,直接把UI线程占死,回调永远无法触发,FirstProcessFinished标志永远不会被设为true,ProcessB要么永远等不到执行,要么因为逻辑漏洞提前启动。BackgroundWorker是比较老旧的异步实现模型,逻辑容易出错,更推荐使用async/await+Task的方案实现同类需求。
测试版Task方案失效原因
你用new Task(() => RunA(path, i))创建的是冷任务,这类任务创建后不会自动启动,必须手动调用Start()方法才会执行。你把未启动的任务加入列表后直接await Task.WhenAll(aTasks),会永远等不到任务完成,流程直接卡死。
正确实现方案
直接使用Task.Run创建自动启动的热任务,用async/await做异步等待,全程不阻塞UI线程,代码如下:
// 按钮点击事件作为顶层事件处理器,使用async void是合规写法 private async void ButtonClick(object sender, RoutedEventArgs e) { var triggerBtn = sender as Button; triggerBtn.IsEnabled = false; // 防止重复点击触发任务 try { // 初始化进度条 progressBar.Maximum = steps + 1; progressBar.Value = 0; // 批量启动ProcessA的所有步骤任务 var processATasks = Enumerable.Range(0, steps) .Select(async stepIndex => { // 后台执行单步逻辑,不占用UI线程 await Task.Run(() => ExecuteProcessAStep(mypath, stepIndex)); // await后自动回到UI线程,可直接更新UI控件 progressBar.Value++; Console.WriteLine($"ProcessA 第{stepIndex}步执行完成"); }) .ToList(); // 异步等待所有ProcessA步骤完成,不会阻塞UI await Task.WhenAll(processATasks); Console.WriteLine("ProcessA全部执行完成,开始启动ProcessB"); // 后台执行ProcessB await Task.Run(() => ExecuteProcessB(mypath)); progressBar.Value++; Console.WriteLine("ProcessB执行完成"); } catch (Exception ex) { MessageBox.Show($"任务执行出错:{ex.Message}"); } finally { triggerBtn.IsEnabled = true; } } /// <summary> /// ProcessA单步执行逻辑,禁止在此方法内直接操作UI控件 /// </summary> private void ExecuteProcessAStep(string path, int stepIndex) { // 迁移原BackgroundWorker_DoWork中ProcessA的对应逻辑即可 } /// <summary> /// ProcessB执行逻辑,禁止在此方法内直接操作UI控件 /// </summary> private void ExecuteProcessB(string path) { // 写入ProcessB的执行逻辑即可 }
实现注意事项
- 绝对不要在UI线程上使用
Thread.Sleep、Wait()、Result这类阻塞式调用,会堵死UI消息循环,既会造成UI冻结,也会导致依赖UI线程调度的回调无法执行 - 不要使用
new Task()创建任务,直接用Task.Run()获取自动启动的热任务,避免遗漏启动造成死锁 - 后台执行的逻辑中禁止直接操作UI控件,所有UI更新操作放在
await之后的代码段即可,WPF会自动处理跨线程调度 - 如果需要限制ProcessA的并发执行数量(比如最多同时跑3个步骤避免资源占满),可以用
SemaphoreSlim做并发控制,不需要自行实现计数器或阻塞等待。
内容的提问来源于stack exchange,提问作者JuniorDev13

