WinForm中SynchronizationContext未被捕获的原因解析
private async void button1_Click(object sender, EventArgs e) { await foo().ConfigureAwait(true); textBox1.Text += "\r\n Current thread: " + Thread.CurrentThread.ManagedThreadId; textBox1.Invalidate(); } private async Task foo() { textBox1.Text += "Current thread: " + Thread.CurrentThread.ManagedThreadId; HttpClient client = new HttpClient(); await await client.GetStringAsync("https://jsonplaceholder.typicode.com/todos/1").ContinueWith<Task>(async (p) => { await Task.Delay(100_000).ConfigureAwait(true); textBox1.Text += "\r\n Current thread SecondContinueWith: " + Thread.CurrentThread.ManagedThreadId; } ).ConfigureAwait(true); textBox1.Text += "\r\n Current thread: " + Thread.CurrentThread.ManagedThreadId; }
运行后textBox1输出内容:
Current thread: 1
Current thread SecondContinueWith: 14
Current thread: 1
Current thread: 1
原本预期ContinueWith中的续接会捕获WindowsFormsSynchronizationContext并在UI线程执行,但实际非调试模式下显示非UI线程ID,调试模式下直接抛出InvalidOperationException,说明续接运行在线程池线程中。为何续接没有捕获上下文?哪里遗漏了?为什么会在非UI线程执行?
注:在续接中显式调用SynchronizationContext.SetSynchronizationContext时,续接会按预期在UI线程执行。
核心原因:ContinueWith默认不捕获当前同步上下文
ContinueWith是传统TPL(任务并行库)的API,它默认不会自动捕获当前的SynchronizationContext,除非调用时显式指定TaskScheduler.FromCurrentSynchronizationContext()作为参数。而await是异步/等待模式的语法糖,会自动捕获当前上下文(UI线程的WindowsFormsSynchronizationContext),并在后续代码中恢复到该上下文执行。
你的代码中,client.GetStringAsync完成后,ContinueWith的回调会在线程池线程中执行(未指定UI线程任务调度器)。此时回调内部的await Task.Delay(100_000).ConfigureAwait(true)尝试恢复上下文,但当前线程的SynchronizationContext已为null(线程池线程无同步上下文),所以后续代码依然在线程池线程执行,导致访问UI控件时要么显示非UI线程ID,要么在调试模式下触发跨线程访问控件的异常。
为什么显式设置上下文能生效?
当你在ContinueWith的回调中显式调用SynchronizationContext.SetSynchronizationContext设置为UI线程的同步上下文后,后续的await ... ConfigureAwait(true)就能捕获到这个上下文,从而将后续代码调度到UI线程执行。
正确的写法(推荐用await替代ContinueWith)
异步代码中应尽量避免混用ContinueWith和await,改用纯await写法,它会自动处理上下文捕获和恢复:
private async Task foo() { textBox1.Text += "Current thread: " + Thread.CurrentThread.ManagedThreadId; HttpClient client = new HttpClient(); // 用await替代ContinueWith await client.GetStringAsync("https://jsonplaceholder.typicode.com/todos/1"); await Task.Delay(100_000).ConfigureAwait(true); textBox1.Text += "\r\n Current thread after delay: " + Thread.CurrentThread.ManagedThreadId; textBox1.Text += "\r\n Current thread: " + Thread.CurrentThread.ManagedThreadId; }
如果一定要使用ContinueWith,需要显式指定UI线程的任务调度器:
await client.GetStringAsync("https://jsonplaceholder.typicode.com/todos/1") .ContinueWith(async (p) => { await Task.Delay(100_000).ConfigureAwait(true); textBox1.Text += "\r\n Current thread SecondContinueWith: " + Thread.CurrentThread.ManagedThreadId; }, TaskScheduler.FromCurrentSynchronizationContext()) // 指定UI线程调度器 .Unwrap() // 解开嵌套的Task<Task> .ConfigureAwait(true);
内容的提问来源于stack exchange,提问作者Ivan Shumakov

