【问题标题】:ConfigureAwait(False) doesn't change context after ContinueWith()ConfigureAwait(False) 在 ContinueWith() 之后不会更改上下文
【发布时间】:2016-05-05 00:17:44
【问题描述】:

我不知道是我做错了什么还是我在 Async 库中发现了一个错误,但是当我使用 continueWith() 返回到同步上下文后,我在运行一些异步代码时发现了一个问题。

更新:代码现在运行

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) 没有更改上下文。

【问题讨论】:

  • 如果这是一个演示问题的实际完整示例,那就太好了。不幸的是,事实并非如此,所以我们只能猜测。如果可以,请尝试创建minimal reproducible example
  • 您好,很高兴知道您真正想要实现的目标。由于您为异步操作混合了不同的技术,并且代码有点难以理解。例如 RunAsyncPost 方法被称为 DoHttpWork。

标签: c# .net asynchronous async-await


【解决方案1】:

代码中的主要问题是由于StartNewContinueWithContinueWith 很危险,原因与 StartNew 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 使用在 UI 线程上运行的 TaskScheduler 调用 Form1.Callback。因此,当Form1.Callback 调用DoAsyncWork 时,它会在UI 线程上调用,使用 当前TaskScheduler(UI 任务调度程序)。在这种情况下,TaskScheduler.Current 是 UI 任务调度程序,DoAsyncWork 最终在 UI 线程上调用 DoWork

因此,在调用StartNewContinueWith 时应始终指定TaskScheduler

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

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

您无法修复HttpClient.GetAsync 错误(显然)。你只需要解决它,最简单的方法是避免使用TaskScheduler.Current。永远,如果可以的话。

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

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

如果我们只做最小的更改(用Run 替换StartNew 和用await 替换ContinueWith),那么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 上有一篇博文,其中展示了一些可能的方法。

即使是像这样非常基本的东西也会比回调(再次,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<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 中受益。

【讨论】:

  • 这里发生了很多事情,我在等你发表清晰的分析。 OP 的代码(及其混合的异步机制)似乎已经“明确”设计为创建死锁,我不清楚是否有可能进行“修复”。我可能在单独的答案中包含的唯一一点是,他们似乎相信ConfigureAwait 将事情转移到单独的上下文中,而实际上几乎相反——“如果我们不能回到原来的上下文,那没关系” ,以防发生等待。
  • @Damien_The_Unbeliever:好点;我正要提到这一点,但忘记了这个答案中的所有单词。 :) @op: ConfigureAwait(false) 确实改变上下文。它只通知await 不捕获上下文。特别是,如果任务已经完成,await ...ConfigureAwait(false)不会移动到线程池线程。
  • 谢谢,这是我一直在寻找的,我对修复此代码不感兴趣,这只是一个示例。我想了解它为什么会陷入僵局。当然,更好地学习使用异步工具:)
【解决方案2】:

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

【讨论】:

  • .Result 在 Task.Factory.StartNew(() => DoWork()); 下运行,所以没关系,它将在另一个上下文中。所以 UI 不会等待它。
  • 这与我要做什么无关,我想了解为什么第三次通话会陷入僵局。以及为什么 ConfigureAwait(false) 没有避免这种情况
  • @Max.Moraga - 我不确定你认为ConfigureAwait 做了什么,但你应该知道,它唯一明显的效果与调用它的方法相同。因此,在方法退出之前调用ConfigureAwait(大部分)毫无意义。
  • blog.stephencleary.com/2012/07/dont-block-on-async-code.html 根据这个博客,返回 await client.GetAsync("google.no").ConfigureAwait(false); 的调用应该在不同的上下文中运行 GetAsync,所以没有死锁,但它不会改变上下文,所以整个事情永远冻结了
  • @Max.Moraga - 但由于 return await(当方法中唯一的 await 时)是一种反模式,只会迫使我们毫无意义地创建整个延续机制,您可以重新-在没有 asyncawait 的情况下编写该方法,它将具有相同的语义 - 除了不必为当前方法创建 continuation,它是 that continuation这是唯一受ConfigureAwait 影响的东西。我们方法的调用者和我们方法调用的方法都不受ConfigureAwait 所做的事情的影响。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-06
  • 1970-01-01
  • 2015-11-23
  • 2019-06-14
相关资源
最近更新 更多