【问题标题】:Whats the overhead of await without I/O没有 I/O 的 await 开销是多少
【发布时间】:2015-08-19 12:56:50
【问题描述】:

C# 5 中异步模式的一个缺点是任务不是协变的,即没有 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();
    }
}

因为 Task 不是协变的,而是添加了 await ,它会将其强制转换为正确的 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);
}

【问题讨论】:

  • Will there be any Thread context switch? => 是的
  • 这个博客可能有用。 There Is No Thread

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


【解决方案1】:

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

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

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

【讨论】:

  • 语法糖明智是可悲的,但很好有一个解决方案(我仍然认为他们应该使它成为协变)
  • 顺便说一句,这不会影响数据库查询吗?我想要 DB 调用的线程上下文切换,否则我的线程池可以被填充
  • @Anders 我同意我希望他们有一个ITask 作为 TPL 的中心,而不是一个具体的任务,尽管它是协变的只是众多原因之一更好,但这就是事后诸葛亮的价值。
  • @Anders 你在那个方法中没有做任何需要使用当前上下文的事情,所以除了浪费时间安排在那里运行之外,它什么也做不了。您的实际工作是 IO 工作,而不是 CPU 绑定工作,因此它不会消耗线程池中的大量资源。
  • 是的我知道,但是ConfigureAwait只影响外层任务权,不影响DB任务?顺便说一句,它怎么可能已经执行了
【解决方案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,它应该适用于大多数任务调度程序。

【讨论】:

    【解决方案3】:

    这个板条箱究竟会对性能产生什么影响?

    几乎没有。

    会有线程上下文切换吗?

    不比平常多。

    正如我在博客中所描述的,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 操作。这会调用有问题的代码 - 执行“异步转换”的域代码。这反过来又调用了已经在使用ConfigureAwait(false)的低级代码。

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

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

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

    1. 低级任务完成。由于这可能是基于 I/O 的,因此任务完成代码在线程池线程上运行。
    2. 低级任务完成代码继续执行域级代码。由于上下文被捕获,域级代码(演员表)排队到该上下文。在 UI 应用中,这会导致线程切换到 UI 线程。
    3. 域级代码到达其方法的末尾,从而完成域级任务。此任务完成代码正在上下文中运行。
    4. 域级任务完成代码继续执行入门级代码。入门级需要已经存在的上下文。

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

    【讨论】:

    • 感谢您的意见。我的意思是将它排除在域之外是因为它不应该打扰这些事情(如何执行任务)。它应该只打扰域逻辑。为了可读性,concerenrc 的分离等。但在任务的情况下,我想没有办法绕过它,如果有意义的话,我绝对会使用它。例如,我现在在我的 CQS API 中使用它靠近金属。顺便说一句,这是一个在 ASP.NET 中运行的启用 WebAPI 的 CQS 项目
    • @Anders:是的,ConfigureAwait(false) 实际上的意思是“我关心上下文”。不幸的是,您必须明确声明,而不是将其作为默认值。
    • 我还有一个问题要问你,因为我知道你是任务之王。请在问题底部查看我的编辑。它显示了我的 CQS API 如何使用配置等待。假设 Handle 方法直接从 EF ToListAsync() 返回任务。我的代码不会重新配置它而不关心上下文吗?这很糟糕,因为它不会扩展。还是会配置外部“演员”等待?
    • @Anders:这是配置等待,而不是配置任务。它只能在await 处调用。
    • 是的,但有点不清楚,您将 ConfigureAwait 直接链接到异步方法的结果上。因此,不清楚它实际上是在方法中返回的任务,还是配置的外部等待。它有点不清楚,并且存在巨大差异,因为内部任务可能是 I/O 任务,但外部始终是简单的强制转换任务
    【解决方案4】:

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

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

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

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

    【讨论】:

    • 你不能这样做,因为编译器不会接受它,因为任务不是协变的
    • 为什么需要它们是协变的?
    • @NedStoyanov 因为他想要返回一个比他所提供的任务更少派生的值。
    • 知道了,他没有调用代码,所以我很困惑。 ConfigureAwait(false) 应该有助于避免上下文切换。
    猜你喜欢
    • 2016-06-04
    • 1970-01-01
    • 1970-01-01
    • 2021-07-26
    • 1970-01-01
    • 2011-02-12
    • 2011-11-23
    • 2017-09-27
    • 1970-01-01
    相关资源
    最近更新 更多