我看一个代码示例。我很惊讶它是先ConfigureAwait(false)
调用 onhttpClient.GetStringAsync
然后 on sourceStream.WriteAsync
。据我所知ConfigureAwait(false)
,它表明代码应该继续执行而不是在上下文中UI
,而是在任务的上下文中。为什么然后调用它 2 次?
private async void Button_Click(object sender, RoutedEventArgs e)
{
HttpClient httpClient = new HttpClient();
//до этого момента всё выполняется в UI контексте?
string content = await httpClient.GetStringAsync("http://www.microsoft.com").
ConfigureAwait(false);
//после выполнения верхней строчки остальной код который внизу будет выполняться в контексте веррхнего таска?
using (FileStream sourceStream = new FileStream("temp.html", FileMode.Create,
FileAccess.Write, FileShare.None, 4096, useAsync: true))
{
byte[] encodedText = Encoding.Unicode.GetBytes(content);
await sourceStream.WriteAsync(encodedText, 0, encodedText.Length).
ConfigureAwait(false);
//будь дальше какой-то код, в контексте какого потока он выполнялся б?
};
}
看。
ConfigureAwait(false)
确实意味着“我不在乎什么流动SynchronizationContext
'方法的尾部将不会被执行。'也就是说,第一个
ConfigureAwait(false)
可以将方法的“尾部”发送到后台线程。但究竟什么可以,什么不能!如果由于某种原因第一个任务被同步执行(例如,该行已经在缓存中),那么将不会SynchronizationContext
执行到另一个任务的传输,并会在原始上下文中继续执行。如果同时第二
await
个没有配备 constructConfigureAwait(false)
,那么方法的尾部将在原始上下文中再次执行 - 也就是说,在您的情况下,在 UI 的上下文中。因此,对于不与 UI 通信的库方法,实际上有必要添加一个.
await
ConfigureAwait(false)
很明显,添加到每个的
await
有点ConfigureAwait(false)
太懒了。你可以改用这个技巧:一开始就“逃”到线程池,就不用再担心了。这可以通过以下构造来完成:助手类(取自此处):
(取自Stephen Toub
await anything;
的想法)来自同一 Stephen Toub 的关于该主题的额外阅读:ConfigureAwait FAQ(和俄语翻译)——感谢@aepot的提示。
一点理论:
使用关键字时,
await
编译器会做很多有趣的事情,但在这种情况下,我们感兴趣的是记住(实际上,其他上下文也会记住)同步上下文SynchronizationContext
,它旨在在特定的线程中执行代码类型。该类SynchronizationContext
有一个重要的方法Post
,可确保传递的委托将在正确的上下文中执行。现在,我们记得第
await
一个代码之前的代码是在调用线程上执行的,但是当您的方法在 之后恢复时会发生什么await
?事实上,在大多数情况下,它也在调用线程上执行,即使调用线程可能在此期间做了其他事情。为了达到这个效果,当前的上下文被SynchronizationContext
保留(当遇到运算符时会发生这种情况await
)。接下来,当方法恢复时,编译器插入一个调用Post
,以便在记住的上下文中恢复执行。通常,调用此方法的成本相对较高。因此,为了避免开销,.NET不调用Post
如果记住的同步上下文与任务完成时的当前同步上下文匹配。但是,如果同步上下文不同,那么昂贵的Post
. 如果性能是重中之重,或者如果您处理的库代码不关心它在哪个线程上运行,那么承担费用可能没有意义。ConigureAwait(false)
因此,在这种情况下,您应该在等待之前调用该方法。重要的是要了解此方法旨在通知.NET您不关心在哪个线程上恢复执行。如果这个线程不是很重要,比如从池中取出来,那么里面的代码就会继续执行。但是如果线程出于某种原因很重要,那么.NET将更愿意为其他事情释放它,并在从池中获取的线程中继续执行您的方法。关于线程是否重要的决定是基于对当前同步上下文的分析做出的。这是介绍性的,现在让我们稍微现代化您的示例。我们会将负责从站点接收内容的功能
www.microsoft.com
移到一个单独的方法中。请注意,ConigureAwait(false)
这里不再使用它。接下来,让我们稍微更改一下点击事件处理程序:
这里发生了什么以及为什么会发生死锁。
属性调用
Result
会阻塞调用线程,直到异步操作GetContentAsync
完成。所以在
GetContentAsync
使用关键字的方法中,本例中的当前上下文await
将被保存。SynchronizationContext
UI
方法
GetContentAsync
执行后,需要Button_Click
在保存的上下文中恢复方法的工作SynchronizationContext
,但这行不通,因为 主线程因为调用Result
.实际总结:
如果性能是重中之重,或者如果您正在谈论不关心它在哪个线程上运行的库代码
ConigureAwait(false)
,您应该使用.后续调用
ConfigureAwait(false)
不会以任何方式影响同步上下文。该方法仍然在 UI 线程上运行。但是我在我的代码中做同样的事情。这通常是以礼貌的方式完成的。因此,如果
await
删除其中一个 - 构造,该方法不会中断。为了更容易使用 ConfigureAwait(false) 您可以使用
Fody 配置等待
你的代码
什么被编译
在这个例子中,只有一个有任务经验的程序员使用了一种过时的等待方法:
很可能是误导了两种方法的混合。您示例中的 ConfigureAwait 方法根本不需要,它根本不起作用,因为 已经有 await 将为其生成一个单独的异步包装器,该方法将返回 Task ,然后才等待结果。
这一切都会返回给调用异步任务的线程。仅当调用线程当时正在执行其他一些长时间运行的任务时,才有可能出现此问题。然后,为了节省时间,我们会使用 ConfigureAwait,这听起来像是“已经返回?占用任何空闲线程,忘记上下文并继续工作”,但这在此类代码中基本上是不可能的。实际上有 1 个任务,我们正在等待它完成。
你的代码应该是这样的:
这是 Microsoft 考试 70-843 书中第 26 页中描述的类似情况。这是比 Internet 上的文章可靠得多的来源,其中所有内容都收集在堆中以及 async\await 和 ConfigureAwait() 中:
或者在哈布雷
聚苯乙烯
我将尝试用教练、足球运动员和球的例子来解释。球在哪里流动。教练正在与球员交谈(同步执行)。教练可以踢球然后说跑。所以他可以为所有 11 名球员提供 11 个球流。但在您的代码中,它执行以下操作:
这种情况需要 ConfigureAwait(false) :