写在前面
版本说明:基于 .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 的内部任务(layout、rendering)
* 你 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 也能用
|
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、动画占用机制。