WPF 学习笔记(九):多线程与 Dispatcher——UI 线程的真相

写在前面

版本说明:基于 .NET 8 LTS。

本篇是后端工程师最容易踩坑的一篇。在 ASP.NET Core 里,async/await 是"线程池里跑",没有"UI 线程"概念。但在 WPF 里,所有 UI 操作必须在单个 UI 线程上——改个 TextBox.Text 跨线程,直接抛异常。

本篇要把 WPF 的单线程模型、Dispatcher 消息循环、async/await 在 UI 上的特殊行为彻底讲清楚。

本文要回答:

为什么 UI 必须单线程?Dispatcher 到底在做什么?为什么后端的 .ConfigureAwait(false) 在 WPF 里行为不同?


一、为什么 UI 必须单线程

1.1 历史背景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Windows GUI 从 Win32 开始就是 STA 模型
  STA = Single-Threaded Apartment
  每个窗口有一个 UI 线程
  UI 操作只在该线程

为什么这样设计:
  1. 多线程并发改 UI → 状态不一致
     (线程 A 改 Text,线程 B 改 Visibility,渲染线程懵了)
  2. Win32 消息泵本质是"单线程队列"
     (PostMessage → 线程取消息 → 处理)
  3. COM 的 STA 模型(Office、Shell 都是 STA)

WPF 继承了这个约束:
  所有 UI 元素创建在 UI 线程
  所有 UI 操作必须在 UI 线程
  违反 → InvalidOperationException

1.2 异常演示

1
2
3
4
5
6
7
8
// 后台线程改 UI
private async void Button_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(() =>
    {
        myTextBlock.Text = "Hello";   // ❌ 抛异常
    });
}
1
2
3
4
5
6
异常信息:
  "The calling thread cannot access this object because a different thread owns it."

所有继承 DispatcherObject 的对象都有"线程亲和性"
  → 创建它的线程是"owner"
  → 只有 owner 线程能访问

1.3 DispatcherObject 的"线程亲和性"

1
2
3
4
5
6
7
// WPF 几乎所有对象都是 DispatcherObject 派生
public abstract class DispatcherObject
{
    public Dispatcher Dispatcher { get; }
    public bool CheckAccess() { ... }   // 当前线程是否是 owner
    public void VerifyAccess() { ... }  // 不是 owner 抛异常
}
1
2
3
4
5
6
7
8
DependencyObject → DispatcherObject
UIElement → ...
FrameworkElement → ...
Control → ...
Button → ...

→ WPF 几乎所有元素都"绑定到一个 Dispatcher"
→ 跨线程访问 → 异常

二、Dispatcher:UI 线程的消息循环

2.1 Dispatcher 是什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Dispatcher = UI 线程的"消息调度器"
  - 一个线程最多一个 Dispatcher
  - UI 线程的 Dispatcher 处理:
    * Win32 消息(鼠标、键盘)
    * WPF 的内部任务(layoutrendering
    *  Post 的代码

工作流:
  while (true)
  {
    var work = DequeueHighestPriorityItem();
    work.Invoke();
  }

2.2 获取 Dispatcher

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 当前线程的 Dispatcher(如果有的话)
Dispatcher current = Dispatcher.CurrentDispatcher;

// 应用主 UI 线程的 Dispatcher(最常用)
Dispatcher ui = Application.Current.Dispatcher;

// 某个 UI 元素的 Dispatcher
Dispatcher elementDispatcher = myButton.Dispatcher;

// 通常这三种都返回同一个 Dispatcher(主 UI 线程)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
CurrentDispatcher vs Application.Current.Dispatcher 区别:
  CurrentDispatcher:
    → 当前线程的 Dispatcher(如果没就临时创建一个,可能不是你想要的)
    → 后台线程调用会创建临时 Dispatcher(陷阱!)

  Application.Current.Dispatcher:
    → 主 UI 线程的 Dispatcher(永远正确)
    → 推荐用法

  element.Dispatcher:
    → 元素所在线程的 Dispatcher(UI 元素就是主线程)
    → 也推荐

2.3 与 Win32 消息泵的关系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Dispatcher 内部就是 Win32 消息泵的包装:

  GetMessage / PeekMessage(Win32)
  TranslateMessage / DispatchMessage(Win32)
  WPF 接管:
    - 系统消息(鼠标、键盘)→ 路由事件
    - WPF 自定义消息 → Dispatcher 操作队列

  Dispatcher.Run() = 进入消息循环(永不返回,直到 Shutdown)

三、跨线程访问 UI

3.1 Dispatcher.Invoke 系列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 同步执行
Application.Current.Dispatcher.Invoke(() =>
{
    myTextBlock.Text = "Updated";
});

// 带返回值
var result = Application.Current.Dispatcher.Invoke(() =>
{
    return myTextBox.Text;
});

// 异步(BeginInvoke,返回 DispatcherOperation)
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
    myTextBlock.Text = "Updated";
}));

3.2 Invoke vs BeginInvoke vs InvokeAsync

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Invoke(action):
  - 同步阻塞
  - 等到 action 执行完才返回
  - ⚠️ 危险:从 UI 线程自己调 → 可能死锁

BeginInvoke(action):
  - 异步,把 action 加入队列立即返回
  - 返回 DispatcherOperation(可等待)
  - 老接口(沿用自 WinForms)

InvokeAsync(action):
  - 异步,返回 Task<T>
  - 现代 API(推荐)
  - 支持 await
1
2
3
4
5
// 推荐写法
await Application.Current.Dispatcher.InvokeAsync(() =>
{
    myTextBlock.Text = "Updated";
});

3.3 死锁陷阱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 在 UI 线程调 Invoke → 死锁
private void Button_Click(...)
{
    // 当前已经在 UI 线程
    Dispatcher.Invoke(() =>   // 试图"同步切到 UI 线程"
    {
        // 但 UI 线程被外层阻塞着(Invoke 是同步的)
        // → 永远等不到执行
        // → 死锁
        DoSomething();
    });
}

// ✅ 用 BeginInvoke / InvokeAsync(异步)
Dispatcher.BeginInvoke(() => DoSomething());

// ✅ 或检查是否已经在 UI 线程
if (!Dispatcher.CheckAccess())
    Dispatcher.Invoke(...);
else
    ...  // 直接执行

四、async/await 在 WPF

后端工程师的关键调整点。

4.1 SynchronizationContext

1
2
3
4
5
6
7
8
9
async/await 的"上下文捕获"机制:
  await 一个 Task 前,记录当前 SynchronizationContext
  await 完成后,回到原上下文继续

不同环境的 SynchronizationContext:
  ASP.NET Core:null(无上下文)→ 任意线程池线程继续
  ASP.NET 经典:AspNetSynchronizationContext → 请求线程继续
  WPF:DispatcherSynchronizationContext → UI 线程继续
  WinForms:WindowsFormsSynchronizationContext → UI 线程继续

4.2 WPF 的 await 行为

1
2
3
4
5
6
private async void Button_Click(object sender, RoutedEventArgs e)
{
    var result = await SomeAsyncOperation();
    // ← 这里回到 UI 线程继续
    myTextBlock.Text = result;   // ✅ 安全
}
1
2
3
4
5
6
7
8
9
执行流:
  1. Button_Click 在 UI 线程执行
  2. await SomeAsyncOperation()
     → 记录 SynchronizationContext(= UI 线程)
     → 线程池线程跑 SomeAsyncOperation
  3. 完成后,Post 回 UI 线程
  4. myTextBlock.Text = result 在 UI 线程

这就是为什么"后端 await 模式"在 WPF 也能用

4.3 ConfigureAwait(false)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ❌ WPF 中常见反模式
private async Task LoadAsync()
{
    var data = await httpClient.GetAsync(url);
    // 后续回到 UI 线程
    var json = await data.Content.ReadAsStringAsync();
    // 又回到 UI 线程(多余的切换)
}

// ✅ 库代码用 ConfigureAwait(false)
public async Task<string> FetchAsync()
{
    var data = await httpClient.GetAsync(url).ConfigureAwait(false);
    var json = await data.Content.ReadAsStringAsync().ConfigureAwait(false);
    // 全程在后台线程
    return json;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ConfigureAwait(false) 的语义:
  - 不捕获 SynchronizationContext
  - 后续在任意线程池线程继续
  - 性能更好(避免 UI 线程切换)
  - 但后续不能直接访问 UI

什么时候用:
  - 库代码(不关心调用方是 UI 还是控制台)
  - 不需要访问 UI 的步骤

什么时候不用:
  - UI 层代码(要访问 UI)
  - WPF ViewModel 的命令(最后要更新 UI)

4.4 完整模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public partial class MainViewModel : ObservableObject
{
    [RelayCommand]
    private async Task LoadDataAsync()
    {
        IsLoading = true;
        try
        {
            // 后台线程获取数据(用 ConfigureAwait(false) 优化)
            var data = await _service.GetDataAsync().ConfigureAwait(false);

            // 回 UI 线程更新 ObservableCollection
            await Application.Current.Dispatcher.InvokeAsync(() =>
            {
                Items.Clear();
                foreach (var item in data) Items.Add(item);
            });
        }
        finally
        {
            // IsLoading 是 INPC,绑定更新要 UI 线程
            // 但 await 之前的 ConfigureAwait(false) 让我们不在 UI 线程
            // 切回去
            await Application.Current.Dispatcher.InvokeAsync(() => IsLoading = false);
        }
    }
}
1
2
3
4
观察:
  - await 默认回 UI 线程(什么都不写最简单)
  - 优化才用 ConfigureAwait(false)
  - 后台线程改 UI 必须用 Dispatcher

五、DispatcherTimer vs Task.Delay

两个看起来都能"延迟",但完全不同。

5.1 DispatcherTimer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var timer = new DispatcherTimer
{
    Interval = TimeSpan.FromSeconds(1)
};
timer.Tick += (s, e) =>
{
    // 在 UI 线程触发
    myTextBlock.Text = DateTime.Now.ToString();
};
timer.Start();
1
2
3
4
5
6
7
8
9
DispatcherTimer:
  - 在 UI 线程触发 Tick
  - 优先级由 Dispatcher 控制(默认 Background)
  - 不精确(受 UI 繁忙程度影响)
  - 可以直接访问 UI

适用:
  - 定期刷新 UI(每秒更新时钟)
  - 不需要精确定时

5.2 Task.Delay

1
2
3
await Task.Delay(1000);
// 1 秒后继续(线程池线程)
// 注意:在 WPF 中 await 后默认回 UI 线程
1
2
3
4
5
6
7
8
9
Task.Delay:
  - 基于 Task 的延迟
  - 不占用 UI 线程
  - 配合 async/await 用
  - await 后回到原 SynchronizationContext

适用:
  - 异步流程里的延迟
  - 不需要"定时触发"

5.3 区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
场景:每秒更新 UI

DispatcherTimer:
  - 简单
  - UI 卡顿时丢帧(不会触发)
  - UI 线程触发

Task.Delay + 循环:
  - 略复杂
  - 后台线程触发(不卡 UI)
  - 配合 Dispatcher 更新 UI

推荐:
  - 普通定时器(时钟、UI 刷新)用 DispatcherTimer
  - 异步流程内延迟用 Task.Delay

六、跨线程访问 UI 的正确姿势

6.1 IProgress

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private async Task ProcessAsync()
{
    var progress = new Progress<int>(p =>
    {
        // 回调在 UI 线程(Progress<T> 自动捕获 SynchronizationContext)
        progressBar.Value = p;
    });

    await Task.Run(() =>
    {
        for (int i = 0; i <= 100; i++)
        {
            ((IProgress<int>)progress).Report(i);
            Thread.Sleep(50);
        }
    });
}
1
2
3
4
5
Progress<T>:
  - 创建时捕获 SynchronizationContext
  - Report 在 UI 线程触发回调
  - 是"进度通知"的标准模式
  - 比手动 Dispatcher.Invoke 干净

6.2 CancellationToken

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private async Task ProcessAsync(CancellationToken token)
{
    await Task.Run(async () =>
    {
        for (int i = 0; i < 1000; i++)
        {
            token.ThrowIfCancellationRequested();
            await DoWorkAsync(i);
        }
    }, token);
}

// 调用
var cts = new CancellationTokenSource();
buttonCancel.Click += (s, e) => cts.Cancel();
await ProcessAsync(cts.Token);

6.3 完整模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[RelayCommand]
private async Task LongOperationAsync(CancellationToken token)
{
    var progress = new Progress<int>(p =>
    {
        ProgressValue = p;
    });

    try
    {
        var result = await Task.Run(() => DoHeavyWork(progress, token), token);
        Result = result;
    }
    catch (OperationCanceledException)
    {
        // 用户取消
    }
}

private int DoHeavyWork(IProgress<int> progress, CancellationToken token)
{
    for (int i = 0; i <= 100; i++)
    {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(50);
        progress.Report(i);
    }
    return 42;
}

七、BackgroundWorker 的现代替代

老代码用 BackgroundWorker,新代码用 Task.Run + Progress + CancellationToken。

7.1 BackgroundWorker(旧)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var worker = new BackgroundWorker
{
    WorkerReportsProgress = true,
    WorkerSupportsCancellation = true
};

worker.DoWork += (s, e) =>
{
    // 后台线程
    for (int i = 0; i <= 100; i++)
    {
        if (worker.CancellationPending) { e.Cancel = true; return; }
        worker.ReportProgress(i);
        Thread.Sleep(50);
    }
};

worker.ProgressChanged += (s, e) =>
{
    // UI 线程
    progressBar.Value = e.ProgressPercentage;
};

worker.RunWorkerCompleted += (s, e) =>
{
    // UI 线程
    if (e.Cancelled) return;
    MessageBox.Show("Done");
};

worker.RunWorkerAsync();

7.2 现代等价(推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private async Task RunWithProgressAsync(CancellationToken token)
{
    var progress = new Progress<int>(p => progressBar.Value = p);

    try
    {
        await Task.Run(() =>
        {
            for (int i = 0; i <= 100; i++)
            {
                token.ThrowIfCancellationRequested();
                ((IProgress<int>)progress).Report(i);
                Thread.Sleep(50);
            }
        }, token);

        MessageBox.Show("Done");
    }
    catch (OperationCanceledException) { }
}
1
2
3
4
5
现代优势:
  - 更简洁
  - 异常处理自然(try-catch)
  - 取消用标准 CancellationToken
  - 没有事件订阅的内存泄漏

八、Dispatcher 优先级

Dispatcher 处理任务有优先级。

8.1 优先级枚举

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
DispatcherPriority 枚举(从高到低):
  Send        (最高,同步执行)
  Normal
  Background
  Input       (键盘鼠标输入)
  Loaded
  Render
  DataBind
  Send        (最低)
  Inactive
  ApplicationIdle
  ContextIdle
  Background
  ...

8.2 用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 等到 UI 渲染完再执行
await Application.Current.Dispatcher.InvokeAsync(() =>
{
    // 在所有渲染任务后
}, DispatcherPriority.Loaded);

// 等到 Application Idle(空闲)
await Application.Current.Dispatcher.InvokeAsync(() =>
{
    // 应用空闲时执行
}, DispatcherPriority.ApplicationIdle);

8.3 让 UI 响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ❌ 长循环阻塞 UI
private void ProcessData()
{
    for (int i = 0; i < 1000000; i++)
    {
        DoWork(i);   // 阻塞 UI 线程
    }
}

// ✅ 切到后台线程
private async Task ProcessDataAsync()
{
    await Task.Run(() =>
    {
        for (int i = 0; i < 1000000; i++) DoWork(i);
    });
}

// ✅ 或者用 Dispatcher.Yield(不推荐,仅调试)
for (int i = 0; i < 1000000; i++)
{
    DoWork(i);
    if (i % 1000 == 0)
        await Dispatcher.Yield(DispatcherPriority.Background);
}

九、实战:可取消的进度条后台任务

9.1 ViewModel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public partial class MainViewModel : ObservableObject
{
    private CancellationTokenSource _cts;

    [ObservableProperty]
    private int _progress;

    [ObservableProperty]
    private bool _isRunning;

    [ObservableProperty]
    private string _status = "就绪";

    [RelayCommand]
    private async Task StartAsync()
    {
        _cts = new CancellationTokenSource();
        IsRunning = true;
        Status = "处理中...";

        var progress = new Progress<int>(p => Progress = p);

        try
        {
            var result = await Task.Run(() => DoWorkAsync(progress, _cts.Token), _cts.Token);
            Status = $"完成:{result}";
        }
        catch (OperationCanceledException)
        {
            Status = "已取消";
            Progress = 0;
        }
        finally
        {
            IsRunning = false;
        }
    }

    [RelayCommand]
    private void Cancel()
    {
        _cts?.Cancel();
    }

    private int DoWorkAsync(IProgress<int> progress, CancellationToken token)
    {
        int total = 100;
        for (int i = 0; i <= total; i++)
        {
            token.ThrowIfCancellationRequested();
            Thread.Sleep(50);
            progress.Report(i);
        }
        return 42;
    }
}

9.2 XAML

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<Window.DataContext>
    <local:MainViewModel/>
</Window.DataContext>

<StackPanel Margin="20">
    <ProgressBar Value="{Binding Progress}" Maximum="100" Height="25" Margin="0,0,0,8"/>

    <TextBlock Text="{Binding Status}" Margin="0,0,0,8"/>

    <StackPanel Orientation="Horizontal">
        <Button Content="开始" Command="{Binding StartCommand}" Width="80" Margin="0,0,8,0"
                IsEnabled="{Binding IsRunning, Converter={StaticResource InverseBool}}"/>
        <Button Content="取消" Command="{Binding CancelCommand}" Width="80"
                IsEnabled="{Binding IsRunning}"/>
    </StackPanel>
</StackPanel>

9.3 知识点覆盖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
✅ Task.Run 后台执行
✅ IProgress<T> 进度通知(自动 UI 线程)
✅ CancellationToken 取消
✅ RelayCommand 异步命令
✅ IsRunning 状态绑定按钮启用
✅ 异常处理(OperationCanceledException)

观察:
  - 任务期间 UI 完全响应(可拖动、可取消)
  - 进度条平滑更新(每 50ms 一次)
  - 取消立即响应(每次循环检查 token)

十、与 ASP.NET Core 的关键对比

后端工程师必读。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
                          ASP.NET Core             WPF
─────────────────────────────────────────────────────────────
SynchronizationContext   null(无)                DispatcherSynchronizationContext
默认 await 后续          任意线程池线程             UI 线程
ConfigureAwait(false)    无意义(已无上下文)       显著优化(避免 UI 切换)
线程模型                 每请求独立                 单 UI 线程
跨线程访问               无"线程亲和"对象           UI 元素有亲和性
进度报告                 IProgress<T>              IProgress<T>(自动 UI 切换)
取消                     CancellationToken          CancellationToken(同样)
Timer                    System.Threading.Timer    DispatcherTimer(UI 线程)

关键差异:
  1. ASP.NET Core 没有 UI 线程概念
     → 后端代码可以任意线程跑
     → ConfigureAwait(false) 几乎无意义

  2. WPF 的 await 默认回 UI 线程
     → 简单(不写 ConfigureAwait 就回 UI)
     → 但要小心"过度切换"(库代码用 ConfigureAwait(false) 优化)

  3. 后台代码改 UI 必须用 Dispatcher
     → 这条 ASP.NET Core 工程师最容易忘

10.1 经典迁移错误

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 后端思维(在 ASP.NET Core 没问题)
public async Task<string> GetAsync()
{
    var data = await httpClient.GetStringAsync(url);
    return data.Trim();
}

// 直接搬到 WPF ViewModel 没问题(await 自动回 UI)
public async Task<string> GetAsync()
{
    var data = await httpClient.GetStringAsync(url);
    return data.Trim();
}

// ❌ 但如果换 Task.Run 不 await
public string Get()
{
    var data = httpClient.GetStringAsync(url).Result;  // ❌ 死锁
    return data.Trim();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.Result 在 WPF 死锁原因:
  - .Result 阻塞 UI 线程
  - httpClient 内部要回到 UI 线程(SynchronizationContext)
  - 但 UI 线程被阻塞
  - → 死锁

ASP.NET Core 不死锁:
  - 无 SynchronizationContext
  - httpClient 不需要回到原线程
  - .Result 直接拿后台结果

修复:
  - 用 await(推荐)
  - 或 ConfigureAwait(false)(库代码)

十一、常见陷阱

11.1 async void

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// ❌ async void 异常会崩溃
private async void DoWork()
{
    await Task.Delay(1000);
    throw new Exception();   // 应用崩溃
}

// ✅ async Task
private async Task DoWorkAsync()
{
    await Task.Delay(1000);
    throw new Exception();   // 异常被 Task 捕获
}

// 例外:事件处理器(必须是 async void)
private async void Button_Click(...)
{
    await DoWorkAsync();
}

11.2 忘了 await

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ❌ fire-and-forget
[RelayCommand]
private void Save()
{
    _service.SaveAsync();   // 没 await,异常丢失
}

// ✅
[RelayCommand]
private async Task SaveAsync()
{
    await _service.SaveAsync();
}

11.3 后台线程改 ObservableCollection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ❌
await Task.Run(() =>
{
    Items.Add(new Item());   // 异常
});

// ✅ 切回 UI 线程
await Application.Current.Dispatcher.InvokeAsync(() =>
{
    Items.Add(new Item());
});

// ✅ 或启用集合同步
BindingOperations.EnableCollectionSynchronization(Items, _lock);

11.4 在构造函数里 await

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ❌ 构造函数不能 async
public MainViewModel()
{
    await LoadDataAsync();   // 编译错误
}

// ✅ 异步初始化模式
public MainViewModel()
{
    Loaded = LoadDataAsync();
}

public Task Loaded { get; }

// 调用方
await viewModel.Loaded;

11.5 CurrentDispatcher 陷阱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ❌ 在后台线程调
Task.Run(() =>
{
    Dispatcher.CurrentDispatcher.Invoke(...);
    // CurrentDispatcher 在后台线程会"创建临时 Dispatcher"
    // 不是 UI 线程的 Dispatcher
});

// ✅ 用 Application.Current.Dispatcher
Task.Run(() =>
{
    Application.Current.Dispatcher.Invoke(...);
});

十二、小结

本文深入 WPF 的多线程模型:

  • UI 单线程硬约束的历史与必要性
  • Dispatcher 的角色(消息循环 + 任务调度)
  • DispatcherObject 的线程亲和性
  • Invoke / BeginInvoke / InvokeAsync 的差异与死锁陷阱
  • SynchronizationContext 与 WPF 的 await 行为
  • ConfigureAwait(false) 在 WPF 的语义
  • DispatcherTimer vs Task.Delay
  • IProgress + CancellationToken 模式
  • BackgroundWorker 的现代替代
  • Dispatcher 优先级
  • 实战:可取消进度条后台任务
  • 与 ASP.NET Core 的关键差异
1
2
3
4
记住三句话:
  1. UI 单线程是硬约束——后台线程改 UI 必须用 Dispatcher
  2. await 默认回 UI 线程(DispatcherSynchronizationContext),库代码用 ConfigureAwait(false) 优化
  3. 后端工程师最常踩的坑:.Result 在 WPF 会死锁(ASP.NET Core 不会)

下一篇进入 WPF 的图形与动画——保留模式渲染、Timeline、Storyboard、动画占用机制

Licensed under CC BY-NC-SA 4.0