在ContinueWith()

时间:2016-05-04 06:55:28

标签: c# .net 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
}
}
  • 异步的HttpClient代码第一次运行良好。
  • 然后,我运行第二个异步代码并使用ContinueWith返回UI上下文,它运行良好。
  • 我再次运行HttClient代码,但它死锁,因为这次ConfigureAwait(false)不会改变上下文。

2 个答案:

答案 0 :(得分:7)

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

总结: StartNewContinueWith只应在您执行dynamic task-based parallelism 时使用(此代码不是)。

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

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

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

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

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

出于这个原因,在致电TaskSchedulerStartNew 时,您应始终指定ContinueWith

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

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

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

以下是异步代码的一些通用指南:

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

如果我们只进行微小的更改(将StartNew替换为Run,将ContinueWith替换为await),则DoAsyncWork 始终在线程池上执行DoWork,并避免死锁(因为await直接使用SynchronizationContext而不是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);
    }
}

然而,拥有基于任务的异步的回调场景总是值得怀疑的,因为任务本身具有回调功能。看起来你正在尝试进行某种异步初始化;我在asynchronous construction上发布了一篇博文,其中展示了一些可能的方法。

即使使用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;
    }
}

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

答案 1 :(得分:0)

不要使用.Result。如果您有任何使用async / await的代码,那么完全忘记它甚至存在。即使我们今天开始工作,你要做的事情也会非常脆弱,以至于它明天不一定会工作。