在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)不会更改上下文。

代码中的主要问题是StartNewContinueWithContinueWith是危险的,原因与StartNew危险相同,正如我在博客中描述的那样。

总结: 如果您正在进行基于动态任务的并行性 (此代码不是这样), 则只应使用StartNewContinueWith

实际问题是HttpClient.GetAsync不使用(相当于) ConfigureAwait(false) ; 它使用ContinueWith及其默认的调度程序参数(即TaskScheduler.Current而不是 TaskScheduler.Default )。

更详细地解释……

StartNewContinueWith的默认调度程序不是 TaskScheduler.Default (线程池); 它是TaskScheduler.Current (当前任务调度程序)。 因此,在您的代码中, DoAsyncWork当前并不总是在线程池上执行DoWork

第一次DoAsyncWork ,它将在UI线程上调用,但没有当前的TaskScheduler 。 在这种情况下, TaskScheduler.CurrentTaskScheduler.Default相同,并且在线程池上调用DoWork

然后, CallbackWithAsyncResult使用在UI线程上运行它的TaskScheduler调用Form1.Callback 。 因此,当Form1.Callback调用DoAsyncWork ,它会在UI线程使用当前的TaskScheduler (UI任务调度程序)调用。 在这种情况下, TaskScheduler.Current是UI任务调度程序, DoAsyncWork最终在UI线程上调用DoWork

因此, 在调用StartNewContinueWith时应始终指定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的代码,那么完全忘记它甚至存在。 即使我们今天开始工作,你要做的事情也会非常脆弱,明天也不一定会工作。