【问题标题】:Architecture for async/await异步/等待的架构
【发布时间】:2013-03-08 09:08:16
【问题描述】:

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

【问题讨论】:

    标签: c# .net asynchronous async-await c#-5.0


    【解决方案1】:

    如果您在架构中的较低级别使用 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 表达式等待 TaskTask&lt;T&gt; 的任何异步方法,就在方法的末尾没有进一步处理,最好不使用 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();
    }
    

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

    【讨论】:

    • 请注意,您还可以标记一条消息,例如您描述为async 的消息以添加错误处理。如果您等待任务,您可以将调用包装在 try/catch 块中,而不是向提供的任务添加延续。
    • @Servy:是的,但在这种情况下,它不符合不提供额外处理的描述。在不更改 API 的情况下可以做到这一点很方便,请注意...
    • 所以,试图理解它,我真的只需要使用 async/await 如果同一函数中的某些内容需要返回 async 方法吗?如果是 fire and forget 或最后一个语句,那么就不需要 await 了吗?
    • @valdetero:好吧,在这种情况下,它并不是真正的“一劳永逸”——你仍然返回一个Task,它可以用来查看操作何时完成、失败等。这很棘手用一句话准确地总结事情 - 最好更深入地了解 await 实际为您做了什么。
    • @JamesManning:不,它也适用于非异步/等待版本,因为 调用 代码仍将首先等待任务,它是 他们的 await 这将使他们回到正确的上下文。他们仍然想要textBox.Text = await Method() - 但Method 是异步的还是只是从其他东西返回任务并不重要。
    【解决方案2】:

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

    没有。异步方法不一定使用新线程。在这种情况下,由于底层的异步方法调用是一个 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 返回通常是设计有缺陷的标志。)

    【讨论】:

    • 最后一个方法中的Task.Run() 只是一个人为的示例,用于示例目的。在这种情况下,它将/可能是任何返回 TaskTask&lt;string&gt; 的方法。我只是想展示方法的嵌套。
    • @valdetero 是的,但具体的例子是有效地使用人们不知道的带有 async/await 的反模式。我想我会指出这一点,虽然我也提到它不一定是错的,只是要谨慎使用。
    • 返回Task.CompletedTask;
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-12
    • 2016-07-07
    • 2016-03-25
    • 2017-10-09
    相关资源
    最近更新 更多