我不知道是我做错了什么,还是我在 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
}
}
最佳答案
您代码中的主要问题是由于 StartNew
和 ContinueWith
。 ContinueWith
是危险的,原因与 StartNew
is dangerous 相同,正如我在我的博客中所描述的。
总结: StartNew
和 ContinueWith
只应在您执行 dynamic task-based parallelism (此代码不是)时使用。
实际问题是 HttpClient.GetAsync
不使用(相当于) ConfigureAwait(false)
;它使用 ContinueWith
及其默认调度程序参数(即 TaskScheduler.Current
, 而不是 TaskScheduler.Default
)。
要更详细地解释...StartNew
和 ContinueWith
的默认调度器不是 TaskScheduler.Default
(线程池);它是 TaskScheduler.Current
(当前的任务调度程序)。因此,在您的代码中,当前的 DoAsyncWork
并不总是在线程池上执行 DoWork
。
第一次调用 DoAsyncWork
时,它将在 UI 线程上调用,但没有当前 TaskScheduler
。在这种情况下, TaskScheduler.Current
与 TaskScheduler.Default
相同,在线程池上调用 DoWork
。
然后,CallbackWithAsyncResult
使用在 UI 线程上运行的 Form1.Callback
调用 TaskScheduler
。因此,当 Form1.Callback
调用 DoAsyncWork
时,它会在具有当前 TaskScheduler
(UI 任务调度程序)的 UI 线程上调用。在这种情况下,TaskScheduler.Current
是 UI 任务调度程序,DoAsyncWork
最终在 UI 线程上调用 DoWork
。
出于这个原因, 在调用 TaskScheduler
或 StartNew
时你应该总是指定一个 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/