在ContinueWith()之后,ConfigureAwait(False)不会更改上下文

Max*_*aga 3 .net c# asynchronous async-await

我不知道我做错了什么或者我在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<HttpResponseMessage> 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<HttpResponseMessage> task = Task<HttpResponseMessage>.Factory.StartNew(() => DoWork());
        CallbackWithAsyncResult(task);
    }

    private void CallbackWithAsyncResult(Task<HttpResponseMessage> 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<HttpResponseMessage> 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
}
}
Run Code Online (Sandbox Code Playgroud)
  • 异步的HttpClient代码第一次运行良好.
  • 然后,我运行第二个异步代码并使用ContinueWith返回UI上下文,它运行良好.
  • 我再次运行HttClient代码,但它死锁,因为这次ConfigureAwait(false)不会更改上下文.

Ste*_*ary 9

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

总结:StartNew并且ContinueWith只应在您执行基于动态任务的并行性(此代码不是)时使用.

实际的问题是,HttpClient.GetAsync不使用(相当于)ConfigureAwait(false); 它使用ContinueWith其默认的调度参数(这就是TaskScheduler.Current,不是 TaskScheduler.Default).

要更详细地解释......

对默认调度StartNewContinueWith不是 TaskScheduler.Default(线程池); 它是TaskScheduler.Current(当前的任务调度程序).所以,在你的代码,DoAsyncWork因为它目前并不能总是执行DoWork的线程池.

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

然后,使用在UI线程上运行它的CallbackWithAsyncResult调用.因此,在调用时,它会在UI线程使用当前(UI任务调度程序)调用.在这种情况下,是UI任务调度程序,并最终调用UI线程.Form1.CallbackTaskSchedulerForm1.CallbackDoAsyncWorkTaskSchedulerTaskScheduler.CurrentDoAsyncWorkDoWork

因此,您应该始终指定TaskScheduler何时调用StartNewContinueWith.

所以,这是一个问题.但它实际上并没有导致您看到的死锁,因为ConfigureAwait(false) 应该允许此代码阻止UI而不是死锁.

它陷入僵局,因为微软犯了同样的错误.在这里查看第198行:( GetContentAsync被调用者GetAsync)使用ContinueWith而不指定调度程序.因此,它TaskScheduler.Current从您的代码中获取,并且在它可以在该调度程序(即UI线程)上运行之前不会完成其任务,从而导致经典的死锁.

你没有办法解决这个问题HttpClient.GetAsync(显然).你只需要解决它,最简单的方法就是避免使用TaskScheduler.Current.永远,如果可以的话.

以下是异步代码的一些一般准则:

  • 不要使用StartNew.请Task.Run改用.
  • 不要使用ContinueWith.请await改用.
  • 不要使用Result.请await改用.

如果我们只进行最小的更改(替换为StartNewwith RunContinueWithwith await),则DoAsyncWork 始终DoWork在线程池上执行,并避免死锁(因为直接await使用SynchronizationContext而不是a TaskScheduler):

public void DoAsyncWork()
{
    Task<HttpResponseMessage> task = Task.Run(() => DoWork());
    CallbackWithAsyncResult(task);
}

private async void CallbackWithAsyncResult(Task<HttpResponseMessage> asyncPrerequisiteCheck)
{
    try
    {
        await asyncPrerequisiteCheck;
    }
    finally
    {
        form.Callback(asyncPrerequisiteCheck);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,拥有基于任务的异步的回调场景总是值得怀疑的,因为任务本身具有回调功能.看起来你正在尝试进行某种异步初始化; 我有一篇关于异步构造的博客文章,展示了一些可能的方法.

即使它async void用于初始化,即使是像这样真正基本的东西也会比回调(再次,IMO)更好的设计:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        MainFrameController controller = new MainFrameController();
        controller.DoWork();

        Callback(controller.DoAsyncWork());
    }

    private async void Callback(Task<HttpResponseMessage> task)
    {
        await task;
        Console.Write(task.Result);

        MainFrameController controller = new MainFrameController();
        controller.DoWork();
    }
}

internal class MainFrameController
{
    public Task<HttpResponseMessage> DoAsyncWork()
    {
        return Task.Run(() => DoWork());
    }

    public HttpResponseMessage DoWork()
    {
        MyHttpClient myClient = new MyHttpClient();
        var task = myClient.RunAsyncGet();
        return task.Result;
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,这里还有其他设计问题:即,DoWork在自然异步操作DoAsyncWork上阻塞,并在自然异步操作上阻塞线程池线程.因此,在Form1调用时DoAsyncWork,它正在等待在异步操作中被阻塞的线程池任务.Async-over-sync-over-async,即.您也可以从我的礼仪博客系列中Task.Run受益.