在ContinueWith()之后,ConfigureAwait(False)不会更改上下文 [英] ConfigureAwait(False) doesn't change context after ContinueWith()
问题描述
我不知道我做错了什么还是在异步库中发现了一个错误,但是当我使用continueWith()返回到同步上下文后,在运行一些异步代码时遇到了一个问题.
I don't know if I am doing something wrong or I found a bug in the Async library, but I have seen an issue when running some async code after I came back to the Synchronized context with 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)不会更改上下文,因此它死锁了.
- 永远不要使用
StartNew
.请使用Task.Run
. - 永远不要使用
ContinueWith
.请使用await
. - 永远不要使用
Result
.请使用await
. - Don't ever use
StartNew
. UseTask.Run
instead. - Don't ever use
ContinueWith
. Useawait
instead. - Don't ever use
Result
. Useawait
instead.
推荐答案
代码中的主要问题是由于StartNew
和ContinueWith
. ContinueWith
具有危险性,其原因与 StartNew
具有危险性,正如我在博客中所描述的.
The main problem in your code is due to StartNew
and ContinueWith
. ContinueWith
is dangerous for the same reasons that StartNew
is dangerous, as I describe on my blog.
摘要:仅在执行 StartNew
和ContinueWith
rel ="noreferrer">基于任务的动态并行处理 (此代码不是).
In summary: StartNew
and ContinueWith
should only be used if you're doing dynamic task-based parallelism (which this code is not).
实际 问题是HttpClient.GetAsync
不使用ConfigureAwait(false)
(等效于).它使用ContinueWith
及其默认的调度程序参数(TaskScheduler.Current
,不是 TaskScheduler.Default
).
The actual problem is that HttpClient.GetAsync
doesn't use (the equivalent of) ConfigureAwait(false)
; it's using ContinueWith
with its the default scheduler argument (which is TaskScheduler.Current
, not TaskScheduler.Default
).
要详细解释...
StartNew
和ContinueWith
的默认调度程序为 not TaskScheduler.Default
(线程池);它是TaskScheduler.Current
(当前的任务计划程序).因此,在您的代码中,DoAsyncWork
就像目前一样,不是总是在线程池上执行DoWork
.
The default scheduler for StartNew
and ContinueWith
is not TaskScheduler.Default
(the thread pool); it's TaskScheduler.Current
(the current task scheduler). So, in your code, DoAsyncWork
as it currently is does not always execute DoWork
on the thread pool.
第一次调用DoAsyncWork
时,它将在UI线程上被调用,但是没有当前的TaskScheduler
.在这种情况下,TaskScheduler.Current
与TaskScheduler.Default
相同,并且在线程池上调用DoWork
.
The first time DoAsyncWork
is called, it will be called on the UI thread but without a current TaskScheduler
. In this case, TaskScheduler.Current
is the same as TaskScheduler.Default
, and DoWork
is called on the thread pool.
然后,CallbackWithAsyncResult
用TaskScheduler
调用Form1.Callback
,该Form1.Callback
在UI线程上运行.因此,当Form1.Callback
调用DoAsyncWork
时,将在UI线程上使用当前的TaskScheduler
(UI任务计划程序)调用它.在这种情况下,TaskScheduler.Current
是UI任务调度程序,而DoAsyncWork
最终在UI线程上调用DoWork
.
Then, CallbackWithAsyncResult
invokes Form1.Callback
with a TaskScheduler
that runs it on the UI thread. So, when Form1.Callback
calls DoAsyncWork
, it is called on the UI thread with a current TaskScheduler
(the UI task scheduler). In this case, TaskScheduler.Current
is the UI task scheduler, and DoAsyncWork
ends up calling DoWork
on the UI thread.
由于这个原因,在调用StartNew
或ContinueWith
时应始终指定TaskScheduler
.
所以,这是一个问题.但这实际上并不会导致您看到死锁,因为ConfigureAwait(false)
应该允许此代码仅阻塞UI而不是死锁.
So, this is a problem. But it's not actually causing the deadlock you're seeing, because ConfigureAwait(false)
should allow this code to just block the UI instead of deadlocking.
僵局是因为Microsoft 犯了同样的错误.检出第198行
It's deadlocking because Microsoft made the same mistake. Check out line 198 here: GetContentAsync
(which is called by GetAsync
) uses ContinueWith
without specifying a scheduler. So, it's picking up the TaskScheduler.Current
from your code, and will not ever complete its task until it can run on that scheduler (i.e., the UI thread), causing the classic deadlock.
(显然)您无法采取任何措施来修复HttpClient.GetAsync
错误.您只需要解决它,最简单的方法就是避免使用TaskScheduler.Current
.如果可以的话.
There's nothing you can do to fix the HttpClient.GetAsync
bug (obviously). You'll just have to work around it, and the easiest way to do that is to avoid having a TaskScheduler.Current
. Ever, if you can.
以下是异步代码的一些一般准则:
Here's some general guidelines for asynchronous code:
如果我们只做最小的更改(用Run
替换StartNew
,用await
替换ContinueWith
),则DoAsyncWork
总是在线程池上执行DoWork
,并且避免死锁(因为await
直接使用SynchronizationContext
而不是TaskScheduler
):
If we just do minimal changes (replacing StartNew
with Run
and ContinueWith
with await
), then DoAsyncWork
always executes DoWork
on the thread pool, and the deadlock is avoided (since await
uses the SynchronizationContext
directly and not a 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);
}
}
但是,具有基于任务的异步的回调方案总是有问题的,因为任务本身具有回调的功能.看起来您正在尝试进行某种异步初始化;我在异步构造上有一篇博客文章,其中显示了一个几种可能的方法.
However, it's always questionable to have a callback scenario with Task-based asynchrony, because Tasks themselves have the power of callbacks within them. It looks like you're trying to do a sort of asynchronous initialization; I have a blog post on asynchronous construction that shows a few possible approaches.
即使这样的最基本的东西也比回调(再次是IMO)更好,即使它使用async void
进行初始化:
Even something really basic like this would be a better design than callbacks (again, IMO), even though it uses async void
for initialization:
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
时,它正在等待被异步操作阻止的线程池任务.就是异步.您可能还会从我的关于Task.Run
礼仪的博客系列中受益.
Of course, there's other design problems here: namely, DoWork
is blocking on a naturally-asynchronous operation, and DoAsyncWork
is blocking a thread pool thread on a naturally-asynchronous operation. So, when Form1
calls DoAsyncWork
, it's awaiting a thread pool task that's blocked on an asynchronous operation. Async-over-sync-over-async, that is. You may also benefit from my blog series on Task.Run
etiquette.
这篇关于在ContinueWith()之后,ConfigureAwait(False)不会更改上下文的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!