Jon*_*eet 33 c# task async-await
我正在为Google Cloud API编写客户端库,这些库具有相当常见的异步帮助程序重载模式:
目前我们正在使用异步方法,但是:
(await foo.Bar().ConfigureAwait(false)).TransformToBaz()括号很烦人.使用两个语句可以提高可读性,但这意味着我们不能使用表达式身体方法.ConfigureAwait(false)- 这在某种程度上可以通过工具解决,但它仍然有点气味Task<TResult>.ContinueWith听起来是个好主意,但我读过Stephen Cleary的博客文章推荐反对它,原因看似合理.我们正在考虑Task<T>为此添加扩展方法:
潜在的延伸方法
public static async Task<TResult> Convert<TSource, TResult>(
this Task<TSource> task, Func<TSource, TResult> projection)
{
var result = await task.ConfigureAwait(false);
return projection(result);
}
Run Code Online (Sandbox Code Playgroud)
然后我们可以非常简单地从同步方法中调用它,例如
public async Task<Bar> BarAsync()
{
var fooRequest = BuildFooRequest();
return FooAsync(fooRequest).Convert(foo => new Bar(foo));
}
Run Code Online (Sandbox Code Playgroud)
甚至:
public Task<Bar> BarAsync() =>
FooAsync(BuildFooRequest()).Convert(foo => new Bar(foo));
Run Code Online (Sandbox Code Playgroud)
它看起来如此简单和有用,我有点惊讶没有已经可用的东西.
作为我使用它来使表达式方法工作的一个例子,在Google.Cloud.Translation.V2代码中我有两种方法来翻译纯文本:一个接受一个字符串,一个接受多个字符串.单字符串版本的三个选项是(在参数方面有所简化):
常规异步方法
public async Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage)
{
GaxPreconditions.CheckNotNull(text, nameof(text));
var results = await TranslateTextAsync(new[] { text }, targetLanguage).ConfigureAwait(false);
return results[0];
}
Run Code Online (Sandbox Code Playgroud)
表达式异步方法
public async Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
(await TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.ConfigureAwait(false))[0];
Run Code Online (Sandbox Code Playgroud)
使用Convert的表达式同步方法
public Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.Convert(results => results[0]);
Run Code Online (Sandbox Code Playgroud)
我个人更喜欢最后一个.
我知道这会改变验证的时间 - 在最后的例子中,传递一个null值text将立即抛出一个ArgumentNullException而传递一个null值targetLanguage将返回一个故障任务(因为TranslateTextAsync将异步失败).这是我愿意接受的差异.
我应该注意调度或性能的差异吗?(我们仍在构建两个状态机,因为该Convert方法将创建一个.使用Task.ContineWith可以避免这种情况,但是在博客文章中提到了所有问题.该Convert方法可能会被更改为ContinueWith谨慎使用.)
(我有点想在CodeReview上发布这个,但我怀疑答案中的信息将更有用,除了这是否是一个好主意.如果其他人不同意,我很乐意移动它.)
Ste*_*ary 23
转换await的结果最终会在优先级方面令人讨厌
我通常更喜欢引入局部变量,但正如您所指出的那样,这会阻止表达式方法.
我们偶尔会忘记
ConfigureAwait(false)- 这在某种程度上可以通过工具解决
由于您正在使用库并且应该ConfigureAwait(false)
在任何地方使用,因此使用强制使用的代码分析器可能是值得的
ConfigureAwait.这是一个ReSharper插件和一个VS插件.不过,我自己也没试过.
Task<TResult>.ContinueWith听起来是个好主意,但我读过Stephen Cleary的博客文章推荐反对它,原因看似合理.
如果您使用过ContinueWith,则必须明确指定
TaskScheduler.Default(这ContinueWith相当于
ConfigureAwait(false)),并且还要考虑添加标记,例如
DenyChildAttach.IMO很难记住如何使用ContinueWith
正确而不是记住ConfigureAwait(false).
另一方面,虽然ContinueWith是一种低级,危险的方法,但如果你正确使用它,那么它可以为你提供较小的性能改进.特别是,使用该state参数可以为您节省委托分配.这是TPL和其他Microsoft库通常采用的方法,但是对于大多数库而言,IMO会降低可维护性.
它看起来如此简单和有用,我有点惊讶没有已经可用的东西.
Convert你建议的方法非正式地存在Then.斯蒂芬并没有这么说,但我认为这个名字Then来自JavaScript世界,承诺是等同于任务的(它们都是Futures).
在旁注中,斯蒂芬的博客文章将这个概念带到了一个有趣的结论.Convert/ Then是bind未来的monad,因此它可用于实现LINQ-over-futures.Stephen Toub也
为此发布了代码(相当过时,但很有趣).
我曾经想过几次添加Then到我的AsyncEx库中,但每次都没有进行切割,因为它与刚刚完全一样await.它唯一的好处是允许方法链接解决优先级问题.我认为它出于同样的原因在框架中不存在.
也就是说,实现自己的Convert方法肯定没有错
.这样做将避免使用括号/额外局部变量并允许表达式身体方法.
我知道这会改变验证的时间
这是我担心躲避asyncawait的原因之一/(我的博客文章涉及更多原因).
在这种情况下,我认为这两种方式都没问题,因为"设置请求的简短同步工作"是一个先决条件检查,IMO并不重要抛出愚蠢的异常(因为它们不应该被捕获) .
如果"简短的同步工作"更复杂 - 如果它可以抛出,或者可以合理地抛出一个人从现在开始重构它 - 那么我会使用async/ await.您仍然可以使用Convert以避免优先级问题:
public async Task<TranslationResult> TranslateTextAsync(string text, string targetLanguage) =>
await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage)
.Convert(results => results[0])
.ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)