同步方法 调用异步方法

Posted by Andy Blog on December 12, 2025

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。
    • 异常如果未捕获,会在同步调用链上抛出

使用 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);
    }
}
  1. Task.Run(task)
    • 会在 线程池线程中执行你的异步任务 task
    • 相当于把任务“搬到后台线程”执行,避免阻塞当前线程的同步上下文(比如 UI 线程)。
  2. .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 上直接调用 .ResultGetAwaiter().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)。