WPF应用异步缓存数据问题:多BeginExecuteReader完成通知与优化
解决WPF异步缓存任务的等待与优化问题
首先得说,你当前用BeginExecuteReader这种APM(异步编程模型)的方式确实会带来任务跟踪的麻烦——这种旧模式本来就不是为批量异步任务的等待场景设计的,换成现代的TAP(基于任务的异步模式,也就是async/await)会省心太多,而且完全能满足你多线程并行缓存、等待全部完成后更新GUI的需求。
核心问题解决:改用TAP模式实现批量任务等待
第一步:改造缓存方法为异步形式
把原来的APM回调式缓存方法,改成支持async/await的异步方法,同时注意线程安全(因为多个任务会并行修改缓存集合):
using System.Collections.Concurrent; // 假设CachedData里的集合改用线程安全类型,比如ConcurrentBag public static class CachedData { public static ConcurrentBag<AuthObj> AuthObjs { get; } = new ConcurrentBag<AuthObj>(); // 其他缓存集合同理 } public static async Task CacheAuthObjectsAsync(SqlDataReader dbr) { try { // 用异步Read避免阻塞线程 while (await dbr.ReadAsync()) { // 线程安全集合无需手动加锁,直接添加 CachedData.AuthObjs.Add(new AuthObj(dbr)); } } catch (Exception ex) { // 这里保留你的日志、邮件告警逻辑 Logger.Error("缓存Auth对象失败", ex); // 可选:重新抛出异常让上层处理,或者静默处理 throw; } finally { // 异步关闭Reader await dbr.CloseAsync(); } }
第二步:在CacheAllAsync中批量启动并等待任务
用Task.WhenAll来等待所有缓存任务完成,完成后直接更新GUI(因为await会自动切回UI线程,无需手动调用Dispatcher):
public static async Task CacheAllAsync() { var cacheTasks = new List<Task>(); // 示例:启动Auth对象缓存任务 var authConn = Engine.CreateDefaultDBConnection(); await authConn.OpenAsync(); using var authCmd = new SqlCommand("SELECT ....", authConn); using var authReader = await authCmd.ExecuteReaderAsync(); cacheTasks.Add(CacheAuthObjectsAsync(authReader)); // 重复上述逻辑,添加其他10-14个缓存任务 // 比如用户数据缓存 var userConn = Engine.CreateDefaultDBConnection(); await userConn.OpenAsync(); using var userCmd = new SqlCommand("SELECT ....", userConn); using var userReader = await userCmd.ExecuteReaderAsync(); cacheTasks.Add(CacheUserObjectsAsync(userReader)); // 等待所有缓存任务完成 try { await Task.WhenAll(cacheTasks); } catch (AggregateException ex) { // 批量处理所有任务的异常 foreach (var innerEx in ex.InnerExceptions) { Logger.Error("缓存任务失败", innerEx); } } // 所有任务完成后更新GUI UpdateCacheStatusLabel("缓存完成"); // 你的GUI更新方法 }
对原方案的合理性分析与优化建议
原APM方案的问题
你当前用BeginExecuteReader的方案最大问题是:
- 手动跟踪10-15个任务的完成状态非常繁琐,容易出现遗漏(比如忘记处理回调异常导致任务永远标记为未完成);
- APM模式代码可读性差,维护成本高,已经被.NET官方弃用,现在推荐用TAP模式。
多线程方案的优化方向
既然你倾向多线程并行方案,这里给几个关键优化点:
- 线程安全的缓存集合:必须用
System.Collections.Concurrent下的线程安全集合(比如ConcurrentBag<T>、ConcurrentDictionary<TKey, TValue>),避免并发修改导致的集合损坏或数据丢失。如果坚持用普通集合,一定要用lock包裹修改操作,但性能不如线程安全集合。 - 数据库连接池管理:10-15个并发连接完全在默认SQL Server连接池的承载范围内(默认池大小是100),无需担心连接耗尽。如果后续任务量增加,可以调整连接池的
Max Pool Size配置。 - 取消支持:可以给
CacheAllAsync添加CancellationToken参数,在异步方法中传入,支持用户中途取消缓存操作:public static async Task CacheAllAsync(CancellationToken cancellationToken = default) { var authConn = Engine.CreateDefaultDBConnection(); await authConn.OpenAsync(cancellationToken); using var authCmd = new SqlCommand("SELECT ....", authConn); using var authReader = await authCmd.ExecuteReaderAsync(cancellationToken); cacheTasks.Add(CacheAuthObjectsAsync(authReader, cancellationToken)); // ...其他任务 try { await Task.WhenAll(cacheTasks); } catch (OperationCanceledException) { Logger.Info("缓存操作被用户取消"); } } - 异常隔离:每个缓存任务的异常不会影响其他任务(
Task.WhenAll会收集所有异常到AggregateException),你可以针对性地处理每个任务的失败场景,比如某个缓存任务失败后,不影响其他任务的完成。
如果你坚持要用APM模式(不推荐)
如果因为某些原因必须保留BeginExecuteReader,可以用CountdownEvent来跟踪任务完成:
public static async Task CacheAllAsync() { int totalTasks = 15; using var countdown = new CountdownEvent(totalTasks); var tcs = new TaskCompletionSource<bool>(); void CacheCompletedCallback(IAsyncResult result) { try { SqlCommand cmd = (SqlCommand)result.AsyncState; using var dbr = cmd.EndExecuteReader(result); AuthObjectDB.CacheAuthObjects(result); // 你的旧缓存方法 } catch (Exception ex) { Logger.Error("缓存任务失败", ex); } finally { // 任务完成后发出信号 if (countdown.Signal()) { // 所有任务完成,触发Task完成 tcs.TrySetResult(true); } } } // 启动所有任务 for (int i = 0; i < totalTasks; i++) { var cmd = new SqlCommand("SELECT ...."); cmd.Connection = Engine.CreateDefaultDBConnection(); await cmd.Connection.OpenAsync(); cmd.BeginExecuteReader(CacheCompletedCallback, cmd); } // 等待所有任务完成 await tcs.Task; // 更新GUI UpdateCacheStatusLabel("缓存完成"); }
但这种代码的复杂度远高于TAP模式,出错概率也更高,还是强烈建议改用async/await的方案。
内容的提问来源于stack exchange,提问作者Adder
相关产品推荐
相关产品推荐

