如何为数据操作类实现最佳异步方法?
实现异步数据加载与更新(不阻塞UI + 进度显示)
首先,咱们要抓住几个核心原则:用原生异步IO方法而非线程池包装同步方法、通过IProgress<T>安全更新UI进度、正确处理异步异常。下面一步步修改你的代码:
1. 改造SourceElement的异步方法
SqlDataAdapter本身提供了FillAsync和UpdateAsync这些原生异步方法,这是真正的非阻塞IO操作,比用Task.Run包裹同步Fill/Update高效得多。我们可以直接利用这些方法,同时加入进度报告支持:
public sealed class SourceElement : IDisposable { private readonly SqlDataAdapter _adapter; public SourceElement(SourceManager parentManager, string tableName) { ParentManager = parentManager; TableName = tableName; _adapter = new SqlDataAdapter($"SELECT * FROM [{TableName}];", ParentManager.ConnectionString); _adapter.FillSchema(ParentManager.MainDataSet, SchemaType.Mapped, TableName); } public void Dispose() => _adapter?.Dispose(); public string TableName { get; } private SourceManager ParentManager { get; } public void Load() => _adapter.Fill(ParentManager.MainDataSet, TableName); // 异步加载,支持进度报告 public async Task LoadAsync(IProgress<int> progress = null) { // 使用原生FillAsync,真正异步IO await _adapter.FillAsync(ParentManager.MainDataSet, TableName); // 加载完成后报告进度(这里每个元素算1个进度单位) progress?.Report(1); } public void Update() => _adapter.Update(ParentManager.MainDataSet.Tables[TableName]); // 异步更新,返回受影响行数,支持进度报告 public async Task<int> UpdateAsync(IProgress<int> progress = null) { int affectedRows = await _adapter.UpdateAsync(ParentManager.MainDataSet.Tables[TableName]); // 报告受影响行数作为进度细节 progress?.Report(affectedRows); return affectedRows; } }
2. 改造SourceManager的异步方法
这里要处理多个SourceElement的异步操作,同时计算整体进度。你可以选择串行执行(更安全,避免DataSet并发问题)或者并行执行(效率更高,适合独立表操作):
串行版本(推荐,无并发风险)
public sealed class SourceManager : IDisposable { // 原构造函数、Dispose、属性保持不变... public void LoadElements() => Elements.ForEach(element => element.Load()); public async Task LoadElementsAsync(IProgress<int> progress = null) { int totalElements = Elements.Count; int completedCount = 0; foreach (var element in Elements) { await element.LoadAsync(); completedCount++; // 计算百分比进度并报告 int percent = (completedCount * 100) / totalElements; progress?.Report(percent); } } public void UpdateAll() => Elements.ForEach(element => element.Update()); public async Task UpdateAllAsync(IProgress<int> progress = null) { int totalElements = Elements.Count; int completedCount = 0; foreach (var element in Elements) { await element.UpdateAsync(); completedCount++; int percent = (completedCount * 100) / totalElements; progress?.Report(percent); } } }
并行版本(适合独立表,效率更高)
如果你的每个SourceElement操作的是DataSet中不同的表,那么并行执行是安全的,能加快整体速度:
public async Task LoadElementsAsync(IProgress<int> progress = null) { int totalElements = Elements.Count; int completedCount = 0; // 用WhenAll并行执行所有加载任务 var tasks = Elements.Select(async element => { await element.LoadAsync(); // 用Interlocked保证并发下计数正确 int currentCompleted = Interlocked.Increment(ref completedCount); int percent = (currentCompleted * 100) / totalElements; progress?.Report(percent); }); await Task.WhenAll(tasks); }
3. UI层调用(不阻塞+进度显示)
在WinForms中,Progress<T>类是神器——它会自动把进度回调调度到创建它的线程(也就是UI线程),所以你不需要手动调用Invoke来更新控件。同时要注意:UI事件处理方法用async void,其他异步方法返回Task,并且一定要捕获异常:
public partial class Form1 : Form { private SourceManager sourceManager; public Form1() { InitializeComponent(); sourceManager = new SourceManager("Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;"); } protected override async void OnLoad(EventArgs e) { base.OnLoad(e); // 初始化进度控件(假设你有progressBar1和lblProgress两个控件) progressBar1.Maximum = 100; progressBar1.Value = 0; lblProgress.Text = "准备加载..."; // 创建Progress实例,回调会自动在UI线程执行 var loadProgress = new Progress<int>(percent => { progressBar1.Value = percent; lblProgress.Text = $"加载中:{percent}%"; }); try { await sourceManager.LoadElementsAsync(loadProgress); lblProgress.Text = "加载完成!"; } catch (Exception ex) { MessageBox.Show($"加载失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); lblProgress.Text = "加载失败"; } } public async void SaveAll() { var saveProgress = new Progress<int>(percent => { progressBar1.Value = percent; lblProgress.Text = $"保存中:{percent}%"; }); try { await sourceManager.UpdateAllAsync(saveProgress); lblProgress.Text = "保存完成!"; } catch (Exception ex) { MessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); lblProgress.Text = "保存失败"; } } public async void SaveData(string tableName) { var element = sourceManager.Elements.Find(e => e.TableName.Equals(tableName)); if (element == null) { lblProgress.Text = $"未找到表:{tableName}"; return; } // 这里进度报告受影响行数 var saveProgress = new Progress<int>(affectedRows => { lblProgress.Text = $"保存{tableName}:{affectedRows}行已更新"; }); try { await element.UpdateAsync(saveProgress); lblProgress.Text = $"{tableName}保存完成!"; } catch (Exception ex) { MessageBox.Show($"保存{tableName}失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); lblProgress.Text = $"{tableName}保存失败"; } } }
关键注意事项
- 不要用
Task.Run包装同步方法:FillAsync/UpdateAsync是原生异步IO,不会占用线程池线程,比Task.Run(() => element.Load())高效得多。 IProgress<T>的正确使用:必须在UI线程创建Progress<T>实例,这样回调才会自动在UI线程执行,避免跨线程操作控件的异常。- 异常处理:所有异步操作都要放在
try/catch中,否则未捕获的异常会导致程序崩溃。 - DataSet线程安全:DataSet本身不是线程安全的,但如果每个
SourceElement操作的是不同的DataTable,并行执行是安全的;如果操作同一个表,必须串行。
内容的提问来源于stack exchange,提问作者Shehab
相关产品推荐
相关产品推荐

