59

如果您在架构中的较低级别使用 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();
    }
}

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

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

当然,我在 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(() => "");
    }
4

2 回答 2

59

如果您在架构中的较低级别使用 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();
    }
}

稍微更有效,因为它不涉及出于很少的原因创建状态机 - 但更重要的是,它更简单。

任何有一个await表达式等待 aTask的异步方法Task<T>,就在方法的末尾没有进一步处理,最好不使用 async/await 来编写。所以这:

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

最好写成:

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

(理论上,正在创建的任务之间存在非常细微的差异,因此调用者可以添加延续,但在绝大多数情况下,您不会注意到任何差异。)

于 2013-03-19T15:37:04.700 回答
28

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

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

这是:

1. necessary

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

2. negative on performance

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

3. just a matter of preference

如果您想保持异步,则不是。您需要这样做以使堆栈中的事物保持异步。

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

public virtual Task Save()
{
    return repository.Save();
}

repository.Save()方法已经返回 a Task- 您无需等待它,只需将其包装回 a 中即可Task。这将使该方法更加有效。

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

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

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

最后,我会提醒您不要举一个例子:

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

如果您正在创建一个异步方法,该方法在内部Task.Run用于围绕本身不是异步的事物“创建异步”,那么您实际上是将同步代码包装到异步方法中。这使用 ThreadPool 线程,但可以“隐藏”它这样做的事实,从而有效地使 API 产生误导。通常最好将调用留给Task.Run您的最高级别调用,并让底层方法保持同步,除非它们真正能够利用异步 IO 或除Task.Run. (这并不总是正确的,但是“异步”代码通过 包裹在同步代码上Task.Run,然后通过 async/await 返回通常是设计有缺陷的标志。)

于 2013-03-19T15:36:22.417 回答