You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的方案最大问题是:

  1. 手动跟踪10-15个任务的完成状态非常繁琐,容易出现遗漏(比如忘记处理回调异常导致任务永远标记为未完成);
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:09:03