我不知道是我做错了什么,还是我在 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) 没有改变上下文。
  • 最佳答案

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

    出于这个原因, 在调用 TaskSchedulerStartNew 时你应该总是指定一个 ContinueWith

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

    之所以陷入僵局,是因为微软犯了同样的错误。查看第 198 行 here :GetContentAsync(由 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 的博客文章,其中展示了一些可能的方法。

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

    关于c# - 在 ContinueWith() 之后,ConfigureAwait(False) 不会更改上下文,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/37020526/

    10-10 07:56