wpf 中,一个同步void方法如何调用异步方法
| 方案 | 阻塞 | 异常捕获 | UI 线程安全 | 适用场景 | 备注 |
| ————————————— | ——- | ————- | ——————————— | ————– | —— |
| async void + await | 否 | ✅ | ✅(默认回 UI) | 事件或框架回调 | 第1 |
| Task.Run + await | 否(后台线程) | ✅ | ❌(修改 UI 需 Dispatcher)增加线程开销 | 后台任务 | 第2 |
| Task.Run(task).GetAwaiter().GetResult() | ✅ | ✅ | ❌(UI 需 Dispatcher)
在线程池运行,不会死锁 | 同步调用 async,无死锁 | 第3 |
| 阻塞 GetAwaiter().GetResult() | ✅ | ✅ | ❌(在当前ui线程运行,可能死锁) | 特殊同步调用 | 不推荐 UI |
| 属性包装 + 绑定 | 否 | ✅(依赖异步方法异常处理) | ✅ | MVVM 风格 | 第4 |
| Fire-and-forget + ContinueWith | 否 | ✅ | ❌(需 Dispatcher)
UI 不卡,但代码略啰嗦 | 事件、回调 | 第5 |
将同步 void 改为 async void,事件专用
partial async void OnSelectedCharacterChanged(CharacterAddOrEditViewModel? value)
{
try
{
await LoadVoicesAsync();
}
catch (Exception ex)
{
LogError(ex);
}
SelectedVoice = Voices.FirstOrDefault(v => v.Id == VoiceId);
}
- 优点:
- 最清晰、可以直接
await异步方法 - 异常可直接捕获
- 最清晰、可以直接
- 缺点:
- async void 只适合事件或特殊回调。如果
OnSelectedCharacterChanged是框架调用的partial void,你可能无法改成async void,这种情况需要用方案 A。 - 异常如果未捕获,会在同步调用链上抛出
- async void 只适合事件或特殊回调。如果
使用 Task.Run 包裹
partial void OnSelectedCharacterChanged(CharacterAddOrEditViewModel? value)
{
Task.Run(async () =>
{
try
{
await LoadVoicesAsync();
}
catch (Exception ex)
{
LogError(ex);
}
});
}
public async Task LoadVoicesAsync(CharacterAddOrEditViewModel character)
{
if (character == null) return;
var voices = (await _voiceService.SearchVoicesAsync(new VoiceSearchRequest() { CharacterId = character?.Id })).Items;
Voices.Clear();
foreach (var voice in voices)
{
var vm = _mapper.Map<VoiceSettingsViewModel>(voice);
Voices.Add(vm);
}
}
- 优点:
- 可以在后台线程安全调用异步方法
- 异常捕获
- 缺点:
- Task.Run()开了一个额外线程
- 如果 LoadVoicesAsync 依赖 UI 线程(Dispatcher),需要
Dispatcher.Invoke回到 UI
用ui线程的dispatcher例子 ```csharp
public async Task LoadVoicesAsync(CharacterAddOrEditViewModel character) { if (character == null) return; var voices = (await _voiceService.SearchVoicesAsync(new VoiceSearchRequest() { CharacterId = character?.Id })).Items;
Application.Current.Dispatcher.Invoke(() =>
{
Voices.Clear();
foreach (var voice in voices)
{
var vm = _mapper.Map<VoiceSettingsViewModel>(voice);
Voices.Add(vm);
}
}); } ```
Task.Run(task).GetAwaiter().GetResult()
void OnSomethingChanged()
{
try
{
Task.Run(() => SomeAsyncMethod()).GetAwaiter().GetResult();
}
catch(Exception ex)
{
LogError(ex);
}
}
原理
- 把异步方法放到线程池线程执行。
- 主线程同步阻塞等待完成。
- 避免了 WPF UI 同步上下文死锁。
优点 - 可以捕获异常。
- 避免 WPF 阻塞死锁(比直接 GetAwaiter().GetResult 好)。
缺点 - 仍然阻塞调用线程。
- 增加线程池开销。
- 异步方法如果需要访问 UI,需要 Dispatcher。
- 可读性差。
同步阻塞等待 GetAwaiter().GetResult()
partial void OnSelectedCharacterChanged(CharacterAddOrEditViewModel? value)
{
try
{
LoadVoicesAsync().GetAwaiter().GetResult(); // 阻塞同步等待
}
catch (Exception ex)
{
LogError(ex);
}
}
Task.Run(task)- 会在 线程池线程中执行你的异步任务
task。 - 相当于把任务“搬到后台线程”执行,避免阻塞当前线程的同步上下文(比如 UI 线程)。
- 会在 线程池线程中执行你的异步任务
.GetAwaiter().GetResult()- 阻塞等待任务完成,如果任务抛异常,会把异常重新抛出到调用线程。
- 和
Task.Wait()或Task.Result类似,但GetAwaiter().GetResult()会直接抛原始异常,不会被AggregateException包裹。 优点
| 优点 | 说明 | | ——— | ———————————– | | 可以捕获异常 | 异步方法内部抛出的异常会同步传递给调用方 | | 不会立即死锁 UI | 因为异步任务在线程池线程运行,而不是 UI 同步上下文(WPF 阻塞) | | 简单 | 不用改方法签名,适合同步方法调用异步方法 | 缺点
| 缺点 | 说明 |
|---|---|
| 仍然是阻塞操作 | GetResult() 会阻塞当前线程直到任务完成 |
| 线程开销 | 每次 Task.Run 都会分配线程池线程,如果频繁调用可能影响性能 |
| UI 操作受限 | 如果异步方法内部需要在 UI 线程访问控件(Dispatcher),必须手动调回 Dispatcher,否则会报错 |
| 不适合高频调用 | 因为会频繁创建线程池任务,阻塞调用线程,性能不如纯异步模式 |
| 可读性差 | 逻辑上仍然是“异步同步化”,可能让人迷惑 |
属性包装 + XAML 绑定(MVVM 风格)
public List<Item> Items => LoadItemsAsync().Result;
- 不推荐在 UI 上直接调用
.Result或GetAwaiter().GetResult(),容易死锁。 - 更安全的做法是:
- ViewModel 属性提供集合
- 异步方法提前加载数据
- UI 绑定属性
fire-and-forget 异步调用
_ = LoadVoicesAsync();
public OnSelectedCharacterChanged(CharacterAddOrEditViewModel? value) {
// fire-and-forget 异步加载
_ = LoadVoicesAsync();
}
缺点是无法捕获异常 原因是:LoadVoicesAsync() 会返回一个 Task,_ = 只是丢弃了返回值,并 不等待任务完成。 如果 LoadVoicesAsync() 内部抛异常,异常会进入 未观察任务,默认会触发 TaskScheduler.UnobservedTaskException 或崩溃(视平台而定)。 因此:不能在同步方法里捕获 LoadVoicesAsync 的异常。 异常应该在任务内部处理
partial void OnSelectedCharacterChanged(CharacterAddOrEditViewModel? value)
{
// 启动异步方法,不阻塞同步方法。
// 异常通过 ContinueWith 捕获。
_ = LoadVoicesAsync().ContinueWith(t =>
{
if (t.Exception != null)
{
// 捕获异常
LogError(t.Exception);
}
}, TaskContinuationOptions.OnlyOnFaulted);
SelectedVoice = Voices.FirstOrDefault(v => v.Id == VoiceId);
}
优点:同步方法不阻塞,UI 不卡顿。可以捕获异常。 缺点: 代码略啰嗦,需要在 ContinueWith 里处理异常. 需要自己处理线程安全(如果修改 UI 绑定集合,需要回到 Dispatcher)。