我不知道我做错了什么或者我在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
}
}
答案 0 :(得分:7)
代码中的主要问题是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
调用Form1.Callback
并在UI线程上运行它TaskScheduler
。因此,当Form1.Callback
调用DoAsyncWork
时,它会在UI线程上调用当前TaskScheduler
(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上发布了一篇博文,其中展示了一些可能的方法。
即使使用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的代码,那么完全忘记它甚至存在。即使我们今天开始工作,你要做的事情也会非常脆弱,以至于它明天不一定会工作。