在ContinueWith()之后,ConfigureAwait(False)不会更改上下文
我不知道我做错了什么或者我在Async库中发现了一个错误,但是在使用continueWith()回到Synchronized上下文后运行一些异步代码时我遇到了一个问题。
更新:代码现在运行
using System; using System.ComponentModel; using System.Net.Http; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication1 { internal static class Program { [STAThread] private static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } public partial class Form1 : Form { public Form1() { InitializeComponent(); MainFrameController controller = new MainFrameController(this); //First async call without continueWith controller.DoWork(); //Second async call with continueWith controller.DoAsyncWork(); } public void Callback(Task task) { Console.Write(task.Result); //IT WORKS MainFrameController controller = new MainFrameController(this); //third async call controller.DoWork(); //IT WILL DEADLOCK, since ConfigureAwait(false) in HttpClient DOESN'T change context } } internal class MainFrameController { private readonly Form1 form; public MainFrameController(Form1 form) { this.form = form; } public void DoAsyncWork() { Task task = Task.Factory.StartNew(() => DoWork()); CallbackWithAsyncResult(task); } private void CallbackWithAsyncResult(Task asyncPrerequisiteCheck) { asyncPrerequisiteCheck.ContinueWith(task => form.Callback(task), TaskScheduler.FromCurrentSynchronizationContext()); } public HttpResponseMessage DoWork() { MyHttpClient myClient = new MyHttpClient(); return myClient.RunAsyncGet().Result; } } internal class MyHttpClient { public async Task RunAsyncGet() { HttpClient client = new HttpClient(); return await client.GetAsync("https://www.google.no").ConfigureAwait(false); } } partial class Form1 { private IContainer components; protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Text = "Form1"; } #endregion } }
- 异步的HttpClient代码第一次运行良好。
- 然后,我运行第二个异步代码并使用ContinueWith返回UI上下文,它运行良好。
- 我再次运行HttClient代码,但它死锁,因为这次ConfigureAwait(false)不会更改上下文。
代码中的主要问题是StartNew
和ContinueWith
。 ContinueWith
是危险的,原因与StartNew
危险相同,正如我在博客中描述的那样。
总结: 如果您正在进行基于动态任务的并行性 (此代码不是这样), 则只应使用StartNew
和ContinueWith
。
实际问题是HttpClient.GetAsync
不使用(相当于) ConfigureAwait(false)
; 它使用ContinueWith
及其默认的调度程序参数(即TaskScheduler.Current
, 而不是 TaskScheduler.Default
)。
更详细地解释……
StartNew
和ContinueWith
的默认调度程序不是 TaskScheduler.Default
(线程池); 它是TaskScheduler.Current
(当前任务调度程序)。 因此,在您的代码中, DoAsyncWork
当前并不总是在线程池上执行DoWork
。
第一次DoAsyncWork
,它将在UI线程上调用,但没有当前的TaskScheduler
。 在这种情况下, TaskScheduler.Current
与TaskScheduler.Default
相同,并且在线程池上调用DoWork
。
然后, CallbackWithAsyncResult
使用在UI线程上运行它的TaskScheduler
调用Form1.Callback
。 因此,当Form1.Callback
调用DoAsyncWork
,它会在UI线程上使用当前的TaskScheduler
(UI任务调度程序)调用。 在这种情况下, TaskScheduler.Current
是UI任务调度程序, DoAsyncWork
最终在UI线程上调用DoWork
。
因此, 在调用StartNew
或ContinueWith
时应始终指定TaskScheduler
。
所以,这是一个问题。 但它实际上并没有导致您看到的死锁,因为ConfigureAwait(false)
应该允许此代码阻止UI而不是死锁。
它陷入僵局,因为微软犯了同样的错误 。 在这里查看第198行: GetContentAsync
(由GetAsync
)使用ContinueWith
而不指定调度程序。 因此,它从您的代码中获取TaskScheduler.Current
,并且在它可以在该调度程序(即UI线程)上运行之前不会完成其任务,从而导致经典的死锁。
你无法解决HttpClient.GetAsync
错误(显然)。 你只需要解决它,最简单的方法是避免使用TaskScheduler.Current
。 永远,如果可以的话。
以下是异步代码的一些一般准则:
- 不要使用
StartNew
。 请改用Task.Run
。 - 不要使用
ContinueWith
。 请改用await
。 - 不要使用
Result
。 请改用await
。
如果我们只进行最小的更改(用Run
替换StartNew
和使用await
DoAsyncWork
),那么DoAsyncWork
总是在线程池上执行DoWork
,并且避免死锁(因为await
直接使用SynchronizationContext
而不是TaskScheduler
):
public void DoAsyncWork() { Task task = Task.Run(() => DoWork()); CallbackWithAsyncResult(task); } private async void CallbackWithAsyncResult(Task asyncPrerequisiteCheck) { try { await asyncPrerequisiteCheck; } finally { form.Callback(asyncPrerequisiteCheck); } }
但是,拥有基于任务的异步的回调场景总是值得怀疑的,因为任务本身具有回调function。 看起来你正在尝试进行某种异步初始化; 我有一篇关于异步构造的博客文章,展示了一些可能的方法。
甚至像这样的基本内容也会比回调(再次,IMO)更好的设计,即使它使用async void
进行初始化:
public partial class Form1 : Form { public Form1() { InitializeComponent(); MainFrameController controller = new MainFrameController(); controller.DoWork(); Callback(controller.DoAsyncWork()); } private async void Callback(Task task) { await task; Console.Write(task.Result); MainFrameController controller = new MainFrameController(); controller.DoWork(); } } internal class MainFrameController { public Task DoAsyncWork() { return Task.Run(() => DoWork()); } public HttpResponseMessage DoWork() { MyHttpClient myClient = new MyHttpClient(); var task = myClient.RunAsyncGet(); return task.Result; } }
当然,这里还存在其他设计问题:即DoWork
在自然异步操作上阻塞, DoAsyncWork
在自然异步操作上阻塞线程池线程。 因此,当Form1
调用DoAsyncWork
,它正在等待在异步操作中被阻止的线程池任务。 Async-over-sync-over-async,即。 您也可以从我关于Task.Run
礼仪的博客系列中Task.Run
。
不要使用.Result
。 如果您有任何使用async / await的代码,那么完全忘记它甚至存在。 即使我们今天开始工作,你要做的事情也会非常脆弱,明天也不一定会工作。