异步/等待的体系结构

本文关键字:体系结构 等待 异步 | 更新日期: 2023-09-27 18:04:46

如果您在体系结构的较低级别使用 async/await,是否有必要一直"冒泡"异步/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(() => "");
    }

异步/等待的体系结构

如果您在体系结构的较低级别使用 async/await,是否有必要一直"冒泡"异步/await 调用,它是否效率低下,因为您基本上是为每个层创建一个新线程(异步调用每个层的异步函数,还是它并不重要,只是取决于您的偏好?

这个问题暗示了几个误解领域。

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

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

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表达式等待TaskTask<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();
}

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

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

不。 异步方法不一定使用新线程。 在这种情况下,由于基础异步方法调用是 IO 绑定方法,因此实际上不应创建新线程。

是这样的:

1. necessary

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

2. negative on performance

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

3. just a matter of preference

如果要保持异步,则不会。 您需要这样做才能在整个堆栈中保持异步。

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

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

repository.Save()方法已经返回了一个Task - 你不需要等待它只是为了把它包装回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返回通常是设计有缺陷的标志。