没有 I/O 的等待开销是多少

Whats the overhead of await without I/O

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

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

return await SomeAsyncMethod(); 

解决这个问题。

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

编辑:更多信息,此代码无法编译

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();
    }
}

因为 Task 不是协变的,但添加了 await,它会将其转换为正确的 IEnumerable 而不是 List that ToListAsync returns

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);
}

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

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

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

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

您解决此问题的方法的更显着成本是,如果 SynchronizationContext.Current 中有一个值,您需要 post 一个值并等待它安排该工作。如果上下文忙于做其他工作,您可能会等待一段时间,而您 实际上 不需要在该上下文中做任何事情。

这可以通过简单地使用 ConfigureAwait(false) 来避免,同时仍然保持方法 async

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

一些相关的开销,虽然大多数时候,不会有上下文切换 - 大部分成本在为每个异步方法生成的状态机中等待。主要问题是是否有什么东西阻止 运行 同步继续。由于我们讨论的是 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 参数仅用于类型推断 - 遗憾的是,您必须显式写出 return 类型,但至少您不必输入 "input" 也可以手动输入)

示例用法:

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,它应该适用于大多数任务计划程序。

Exactly what impact performance wise will this crate?

差不多none.

Will there be any Thread context switch?

不比正常人多。

正如我在博客中所描述的,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) everywhere in domain code does not feel like a viable solution

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

让我们考虑一下这个调用在您的应用程序中是如何工作的。毫无疑问,您已经获得了一些取决于上下文的入门级代码 - 例如,按钮单击处理程序或 ASP.NET MVC 操作。这会调用相关代码 - 执行 "asynchronous cast" 的域代码。这反过来调用已经使用 ConfigureAwait(false).

的低级代码

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

  1. 低级任务完成。由于这可能是 I/O-based,任务完成代码是线程池线程上的 运行。
  2. 低级任务完成代码恢复执行域级代码。由于未捕获上下文,域级代码(强制转换)在同一个线程池线程上执行,同步
  3. 域级代码到达其方法的末尾,完成了域级任务。此任务完成代码仍在同一线程池线程上 运行。
  4. 域级任务完成代码恢复执行入口级代码。由于入门级需要上下文,因此入门级代码排队到该上下文。在 UI 应用程序中,这会导致线程切换到 UI 线程。

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

  1. 低级任务完成。由于这可能是 I/O-based,任务完成代码是线程池线程上的 运行。
  2. 低级任务完成代码恢复执行域级代码。由于捕获了上下文,因此域级代码(强制转换)排队到该上下文。在 UI 应用程序中,这会导致线程切换到 UI 线程。
  3. 域级代码到达其方法的末尾,完成了域级任务。此任务完成代码在上下文中为运行。
  4. 域级任务完成代码恢复执行入口级代码。入门级需要上下文,上下文已经存在。

所以,这只是上下文切换发生的时间问题。对于 UI 应用程序,最好在线程池上尽可能长时间地保留少量工作,但对于大多数应用程序,如果不这样做,也不会影响性能。同样,对于 ASP.NET 应用程序,如果您将尽可能多的代码保留在请求上下文之外,您可以免费获得少量并行性,但对于大多数应用程序来说,这无关紧要。