使用多async/await实现文件压缩复制时UI冻结且仅处理首个项的问题排查
问题根源与解决方案分析
你的代码出现UI冻结且仅处理第一个项的问题,主要有两个核心原因,下面逐一拆解并给出修复方案:
一、UI冻结的原因:同步阻塞操作占用UI线程
虽然你用了async/await,但TaskOperationBackupOffline里的大部分操作(比如归档创建、文件添加、目录操作)都是同步阻塞方法,而且这些方法默认在UI线程执行(因为你没有把它们放到后台线程),直接卡住了UI消息循环,导致界面冻结。
另外,直接在任务中调用dgwStation.Items.Refresh()属于跨线程访问UI元素,虽然async方法在await后会回到UI上下文,但前面的同步阻塞代码已经占用了UI线程,无法及时更新界面。
二、仅处理第一个项的原因:共享归档实例导致线程冲突
看起来ArchivioB1是一个全局类成员,多个并行任务同时操作同一个实例,会导致严重的线程安全问题:第一个任务打开归档后,后续任务的归档操作会被干扰(比如无法重复打开归档、覆盖操作状态),最终只有第一个任务能正常执行。
针对性修复方案
1. 将同步阻塞操作移到后台线程
把所有耗时的同步操作(归档、目录创建等)包装到Task.Run中,让它们在ThreadPool线程执行,释放UI线程。同时封装UI更新逻辑,确保在UI线程执行。
2. 为每个任务创建独立的归档实例
每个并行任务必须使用自己的ArchivioB1实例,避免共享资源导致的冲突。
3. 优化取消令牌检查与路径处理
在关键步骤前检查取消令牌,确保能及时响应用户取消;用Path.Combine替代手动拼接路径,避免分隔符错误。
修复后的代码示例
按钮点击事件(无需大改,保持原有任务调度逻辑)
var row_list = GetDataGridRows(dgwStation); tokenSource = new CancellationTokenSource(); var tasks = new List<Task<string>>(); foreach (DataGridRow single_row in row_list) { var row = single_row.GetIndex(); tasks.Add(TaskOperationBackupOffline(row, tokenSource.Token)); } btBackup.IsEnabled = false; btCancel.IsEnabled = true; await Task.WhenAll(tasks);
修改后的TaskOperationBackupOffline方法
private async Task<string> TaskOperationBackupOffline(int rowindex, CancellationToken ct) { try { // 先获取当前项数据,避免后台线程直接访问UI绑定集合 var currentItem = StationItemsList[rowindex]; string source = currentItem.ControlPath; string target = currentItem.Destination; string filename = currentItem.StationName; // 更新UI:检查源目录状态 UpdateItemState(rowindex, Chiavi.dict["backupon_controlorigin"].ToString()); // 检查源目录是否存在(可在UI线程执行) if (!Directory.Exists(source)) { UpdateItemState(rowindex, Chiavi.dict["backupstate_source"].ToString()); generalerror = true; return "source error"; } // 更新UI:检查目标目录状态 UpdateItemState(rowindex, Chiavi.dict["backupon_controldestinationdata"].ToString()); // 后台创建目标目录 await Task.Run(() => { ct.ThrowIfCancellationRequested(); if (!Directory.Exists(target)) { Directory.CreateDirectory(target); } }, ct); ct.ThrowIfCancellationRequested(); // 更新UI:准备归档状态 UpdateItemState(rowindex, Chiavi.dict["backupon_preparation"].ToString()); // 生成归档文件名(用Path.Combine避免路径错误) string timestamp = $"{DateTime.Now.Year}-{voidGeneral.ConversioneDecimale(DateTime.Now.Month)}-{voidGeneral.ConversioneDecimale(DateTime.Now.Day)}_{voidGeneral.ConversioneDecimale(DateTime.Now.Hour)}_{voidGeneral.ConversioneDecimale(DateTime.Now.Minute)}_{voidGeneral.ConversioneDecimale(DateTime.Now.Second)}"; string archivePath = Path.Combine(target, $"{timestamp}-{filename}.zip"); ct.ThrowIfCancellationRequested(); // 更新UI:创建ZIP状态 UpdateItemState(rowindex, Chiavi.dict["backupon_createZIP"].ToString()); // 后台执行归档操作,每个任务使用独立的ArchivioB1实例 await Task.Run(() => { using (var archivio = new ArchivioB1()) // 假设ArchivioB1实现IDisposable,否则直接实例化 { ct.ThrowIfCancellationRequested(); archivio.FileName = archivePath; archivio.OpenArchive(FileMode.Create); archivio.Comment = rowindex.ToString(); archivio.OnOverallProgress += ArchivioB1_OnOverallProgress; archivio.BaseDir = source; ct.ThrowIfCancellationRequested(); archivio.AddFiles("*.*"); ct.ThrowIfCancellationRequested(); archivio.CloseArchive(); } }, ct); UpdateItemState(rowindex, "Complete"); return "Complete"; } catch (OperationCanceledException) { UpdateItemState(rowindex, "Cancelled"); tbkStato.Text = Chiavi.dict["backupstate_cancelled"].ToString(); return "Cancelled"; } catch (Exception ex) { UpdateItemState(rowindex, ex.Message); generalerror = true; return "Error"; } } // 封装UI更新逻辑,确保在UI线程执行 private void UpdateItemState(int rowindex, string state) { if (dgwStation.Dispatcher.CheckAccess()) { StationItemsList[rowindex].State = state; dgwStation.Items.Refresh(); // 如果用ObservableCollection绑定,可去掉此调用,自动刷新 } else { dgwStation.Dispatcher.Invoke(() => { StationItemsList[rowindex].State = state; dgwStation.Items.Refresh(); }); } }
额外优化建议
- 如果
StationItemsList是ObservableCollection<T>,更新State后会自动触发UI刷新,可去掉dgwStation.Items.Refresh()调用,提升性能。 ArchivioB1_OnOverallProgress事件处理程序如果涉及UI更新,也要用Dispatcher.Invoke确保在UI线程执行。
内容的提问来源于stack exchange,提问作者Paolo
相关产品推荐
相关产品推荐

