什么是没有I / O等待的开销

时间:2015-08-19 12:56:50

标签: c# casting async-await context-switch covariant

C#5中异步模式的一个缺点是任务不是协变,I.E没有ITask<out TResult>

我注意到我的开发人员经常这样做

return await SomeAsyncMethod(); 

来解决这个问题。

这个箱子对性能有何影响?没有I / O或线程产量它只会等待并将其转换为正确的协变,在这种情况下异步框架会做什么?会有任何线程上下文切换吗?

编辑:更多信息,此代码不会编译

public class ListProductsQueryHandler : IQueryHandler<ListProductsQuery, IEnumerable<Product>>
{
    private readonly IBusinessContext _context;

    public ListProductsQueryHandler(IBusinessContext context)
    {
        _context = context;
    }

    public Task<IEnumerable<Product>> Handle(ListProductsQuery query)
    {
        return _context.DbSet<Product>().ToListAsync();
    }
}

beacuse任务不是协变但添加等待它会将其强制转换为IEnumerable而不是ToListAsync返回的List

ConfigureAwait(false)域代码中的任何地方都不是一个可行的解决方案,但我肯定会将它用于我的低级方法,如

public async Task<object> Invoke(Query query)
{
    var dtoType = query.GetType();
    var resultType = GetResultType(dtoType.BaseType);
    var handler = _container.GetInstance(typeof(IQueryHandler<,>).MakeGenericType(dtoType, resultType)) as dynamic;
    return await handler.Handle(query as dynamic).ConfigureAwait(false);
}

4 个答案:

答案 0 :(得分:2)

解决此问题的方法成本越显着,如果SynchronizationContext.Current中有值,则需要向其发布值并等待它安排该工作。如果上下文忙于完成其他工作,那么当您实际需要在该上下文中执行任何操作时,您可能会等待一段时间。

只需使用ConfigureAwait(false)即可避免这种情况,同时仍保留方法async

一旦删除了使用同步上下文的可能性,那么async方法生成的状态机的开销不应高于添加延续时需要提供的开销。明确。

答案 1 :(得分:2)

一些开销相关联,虽然大多数情况下,不会有上下文切换 - 大部分成本都在为等待的每个异步方法生成的状态机中。主要问题是如果有什么东西阻止继续同步运行。而且由于我们正在讨论I / O操作,实际I / O操作的开销往往会相形见绌 - 同样,主要的例外是Winforms使用的同步上下文。

为了减少这种开销,你可以自己做一个辅助方法:

public static Task<TOut> Cast<TIn, TOut>(this Task<TIn> @this, TOut defaultValue)
  where TIn : TOut
{
    return @this.ContinueWith(t => (TOut)t.GetAwaiter().GetResult());
}

defaultValue参数仅用于类型推断 - 遗憾的是,您必须明确地写出返回类型,但至少您不必手动输入“输入”类型)

样本用法:

public class A
{
    public string Data;
}

public class B : A { }

public async Task<B> GetAsync()
{
    return new B { Data = 
     (await new HttpClient().GetAsync("http://www.google.com")).ReasonPhrase };
}

public Task<A> WrapAsync()
{
    return GetAsync().Cast(default(A));
}

如果需要,您可以尝试进行一些调整,例如:使用TaskContinuationOptions.ExecuteSynchronously,这应该适用于大多数任务调度程序。

答案 2 :(得分:1)

  

这个箱子对性能的影响究竟是什么?

几乎没有。

  

是否有任何线程上下文切换?

不过是正常的。

正如我在博客中描述的那样,when an await decides to yield, it will first capture the current context (SynchronizationContext.Current, unless it is null, in which case the context is TaskScheduler.Current). Then, when the task completes, the async method resumes executing in that context.

此对话中的另一个重要元素是task continuations execute synchronously if possible(再次,在我的博客中描述)。请注意,这实际上并未在任何地方记录;它是一个实施细节。

  

ConfigureAwait(false)域代码中的任何地方都不是一个可行的解决方案

可以在您的域代码中使用ConfigureAwait(false)(出于语义原因,我实际上建议您这样做),但在这种情况下,它可能不会在性能方面产生任何差异。这就是为什么......

让我们考虑一下这个调用在您的应用程序中是如何工作的。毫无疑问,您获得了一些依赖于上下文的入门级代码 - 例如,按钮单击处理程序或ASP.NET MVC操作。这会调用有问题的代码 - 执行&#34;异步转换&#34;的域代码。这反过来调用已经使用ConfigureAwait(false)的低级代码。

如果您在域代码中使用ConfigureAwait(false),则完成逻辑将如下所示:

  1. 低级别任务完成。由于这可能是基于I / O的,因此任务完成代码在线程池线程上运行。
  2. 低级任务完成代码继续执行域级代码。由于未捕获上下文,域级代码(强制转换)在同一个线程池线程上执行,同步
  3. 域级代码到达其方法的末尾,从而完成域级任务。此任务完成代码仍在同一个线程池线程上运行。
  4. 域级任务完成代码继续执行入门级代码。由于入门级需要上下文,因此入门级代码将排队到该上下文。在UI应用程序中,这会导致线程切换到UI线程。
  5. 如果您在您的域代码中使用ConfigureAwait(false),则完成逻辑将如下所示:

    1. 低级别任务完成。由于这可能是基于I / O的,因此任务完成代码在线程池线程上运行。
    2. 低级任务完成代码继续执行域级代码。由于捕获了上下文,域级代码(转换)排队到该上下文。在UI应用程序中,这会导致线程切换到UI线程。
    3. 域级代码到达其方法的末尾,从而完成域级任务。此任务完成代码正在上下文中运行。
    4. 域级任务完成代码继续执行入门级代码。入门级需要上下文,该上下文已经存在。
    5. 所以,这只是上下文切换何时发生的问题。对于UI应用程序,尽可能长时间地在线程池上保留少量工作是很好的,但对于大多数应用程序而言,如果你不这样做,它将不会损害性能。同样,对于ASP.NET应用程序,如果您在请求上下文中保留尽可能多的代码,则可以免费获得少量并行性,但对于大多数应用程序而言,这无关紧要。

答案 3 :(得分:0)

编译器生成一个状态机,在调用方法时保存当前SynchornizationContext并在await之后恢复它。

因此可能存在上下文切换,例如当您从UI线程调用此函数时,await之后的代码将切换回在UI线程上运行,这将导致上下文切换。

这篇文章可能很有用Asynchronous Programming - Async Performance: Understanding the Costs of Async and Await

在您的情况下,在等待之后粘贴ConfigureAwait(false)以避免上下文切换可能很方便,但是async方法的状态机仍将生成。最后,与您的查询费用相比,await的费用可以忽略不计。