1

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

4 回答 4

2

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

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

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

于 2015-08-19T13:15:43.577 回答
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));
}

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

于 2015-08-19T13:18:26.070 回答
2

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

几乎没有。

会有任何线程上下文切换吗?

不比平常多。

正如我在博客中所描述的,当 anawait决定让步时,它将首先捕获当前上下文(SynchronizationContext.Current,除非它是null,在这种情况下上下文是TaskScheduler.Current)。然后,当任务完成时,该async方法继续在该上下文中执行。

此对话中的另一个重要元素是,如果可能的话,任务延续会同步执行(同样,在我的博客中进行了描述)。请注意,这实际上并没有记录在任何地方;这是一个实现细节。

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 应用程序,如果您将尽可能多的代码保留在请求上下文之外,则可以免费获得少量并行性,但对于大多数应用程序来说,这无关紧要。

于 2015-08-22T16:28:30.720 回答
0

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

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

这篇文章可能有用异步编程 - 异步性能:了解异步和等待的成本

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

于 2015-08-19T13:06:37.233 回答