异步/等待的体系结构

val*_*ero 58 .net c# asynchronous async-await c#-5.0

如果你在架构中的较低级别使用async/await,是否有必要"冒泡"async/await调用一直向上,这是低效的,因为你基本上是为每个层创建一个新线程(异步调用每个层的异步函数,或者它真的不重要,只是取决于您的偏好?

我正在使用EF 6.0-alpha3,以便我可以在EF中使用异步方法.

我的存储库是这样的:

public class EntityRepository<E> : IRepository<E> where E : class
{
    public async virtual Task Save()
    {
        await context.SaveChangesAsync();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我的业务层是这样的:

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public async virtual Task Save()
    {
        await repository.Save();
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,我的UI中的方法在调用时会遵循相同的模式.

这是:

  1. 必要
  2. 表现不利
  3. 只是一个偏好的问题

即使在单独的图层/项目中没有使用它,如果我在同一个类中调用嵌套方法,则同样的问题也适用:

    private async Task<string> Dosomething1()
    {
        //other stuff 
        ...
        return await Dosomething2();
    }
    private async Task<string> Dosomething2()
    {
        //other stuff 
        ...
        return await Dosomething3();
    }
    private async Task<string> Dosomething3()
    {
        //other stuff 
        ...
        return await Task.Run(() => "");
    }
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 58

如果你在架构中的较低级别使用async/await,是否有必要"冒泡"async/await调用一直向上,这是低效的,因为你基本上是为每个层创建一个新线程(异步调用每个层的异步函数,或者它真的不重要,只是取决于您的偏好?

这个问题暗示了一些误解的领域.

首先,每次调用异步函数时都不会创建新线程.

其次,您不需要声明异步方法,只是因为您正在调用异步函数.如果您对已经返回的任务感到满意,只需从没有 async修饰符的方法中返回该任务:

public class EntityRepository<E> : IRepository<E> where E : class
{
    public virtual Task Save()
    {
        return context.SaveChangesAsync();
    }
}

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public virtual Task Save()
    {
        return repository.Save();
    }
}
Run Code Online (Sandbox Code Playgroud)

这样稍微提高效率,因为它不会涉及很少创建状态机 - 但更重要的是,它更简单.

任何异步方法,你有一个await表达式等待a Task或者Task<T>,在方法结束时没有进一步处理,最好不使用async/await写入.所以这:

public async Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return await bar.Quux();
}
Run Code Online (Sandbox Code Playgroud)

写得更好:

public Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return bar.Quux();
}
Run Code Online (Sandbox Code Playgroud)

(理论上,创建的任务有一个非常小的差异,因此呼叫者可以添加延续,但在绝大多数情况下,你不会注意到任何差异.)

  • @valdetero:嗯,在这种情况下,它并不是真正的“即发即弃”-您仍在返回一个`Task`,可用于查看操作何时完成、失败等。将事情准确地总结为一个单一的内容是很棘手的句子 - 最好深入了解 await 实际为您做什么。 (2认同)

Ree*_*sey 28

它是低效的,因为你基本上是为每一层创建一个新线程(异步调用每个层的异步函数,或者它真的不重要,只是取决于你的偏好?

不可以.异步方法不一定使用新线程.在这种情况下,由于底层异步方法调用是IO绑定方法,因此确实不应该创建新线程.

这是:

1. necessary
Run Code Online (Sandbox Code Playgroud)

如果要保持操作异步,则必须"冒泡"异步调用.但这确实是首选,因为它允许您充分利用异步方法,包括在整个堆栈中将它们组合在一起.

2. negative on performance
Run Code Online (Sandbox Code Playgroud)

没有.正如我所提到的,这不会创建新线程.有一些开销,但其中大部分可以最小化(见下文).

3. just a matter of preference
Run Code Online (Sandbox Code Playgroud)

如果你想保持这种异步,那就不是了.您需要这样做才能在堆栈中保持异步.

现在,您可以采取一些措施来改善性能.这里.如果您只是包装异步方法,则不需要使用语言功能 - 只需返回Task:

public virtual Task Save()
{
    return repository.Save();
}
Run Code Online (Sandbox Code Playgroud)

repository.Save()方法已经返回Task- 您不需要等待它只是将它包装回来Task.这将使该方法更有效.

您还可以使用"低级"异步方法ConfigureAwait来防止它们需要调用同步上下文:

private async Task<string> Dosomething2()
{
    //other stuff 
    ...
    return await Dosomething3().ConfigureAwait(false);
}
Run Code Online (Sandbox Code Playgroud)

await 如果您不需要担心调用上下文,这可以显着减少每个开销.这通常是处理"库"代码时的最佳选择,因为"外部" await将捕获UI的上下文.库的"内部"工作通常不关心同步上下文,因此最好不要捕获它.

最后,我要警告你的一个例子:

private async Task<string> Dosomething3()
{
    //other stuff 
    ...
    // Potentially a bad idea!
    return await Task.Run(() => "");
}
Run Code Online (Sandbox Code Playgroud)

如果你正在制作一个异步方法,它在内部Task.Run用于"创建异步",而不是本身异步的东西,那么你有效地将同步代码包装成异步方法.这使用ThreadPool线程,但可以"隐藏"它正在这样做的事实,有效地使API误导.通常最好将调用留给Task.Run最高级别的调用,并让底层方法保持同步,除非它们真正能够利用异步IO或除了之外的某种卸载方式Task.Run.(这并非总是如此,但是"异步"代码包裹在同步代码上Task.Run,然后通过async/await返回通常是设计有缺陷的标志.)

  • @valdetero是的,但具体的例子是有效地使用人们不知道的async/await的反模式.我以为我会指出这一点,虽然我也提到它不一定是错的,只是谨慎使用的东西. (2认同)