异步/等待的架构设计

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();
    }
}

当然,在我的用户界面中,调用时遵循相同的模式是必要的。

这是:

  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(() => "");
    }
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表达式等待一个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();
}

(理论上,创建的任务有一些细微差别,因此调用者可以添加延续的内容也会有所不同,但在绝大多数情况下,您不会注意到任何差异。)


请注意,您还可以将诸如您描述的消息标记为 async 以添加错误处理。如果您等待该任务,则可以仅将调用包装在 try/catch 块中,而无需添加提供的任务的延续。 - Servy
@Servy:是的,但在那种情况下,它就不符合提供无额外处理的描述了。不过,可以这样做而不更改API确实很方便... - Jon Skeet
2
@valdetero:在这种情况下,它并不完全是“发射并忘记”,你仍然返回了一个“任务(Task)”,可以用来查看操作何时完成、失败等。很难用一句话准确地概括所有内容,最好深入了解await实际上为您做了什么。 - Jon Skeet
@Jon - 谢谢!关于await,我的意思是如果调用者具有线程亲和性(WinForms、Silverlight等),那么他们可以使用textBox.Text = Method().Results,并且它将适用于异步/等待版本(因为没有ConfigureAwait(false)),但在“只返回EFv6给您的任务”的情况下,它会失败并引发跨线程异常。据我所知。 :) - James Manning
1
@JamesManning:不,这对于非异步/等待版本也可以工作,因为“调用”代码仍然会首先等待任务,并且是“他们”的等待将它们带回正确的上下文。他们仍然希望 textBox.Text = await Method() - 但无论 Method 是异步的还是只是从其他地方返回任务都没有关系。 - Jon Skeet
显示剩余6条评论

28

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

不是。异步方法不一定会使用新线程。在这种情况下,由于底层异步方法调用是一个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 包裹它,也不需要使用 await。这样可以使方法更加高效。

你还可以让你的“低级”异步方法使用 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 来“创建异步性”以包装一个本身不是异步的操作,那实际上是将同步代码封装在异步方法中。这将使用线程池线程,但可能会“隐藏”它正在执行此操作的事实,从而使 API 误导用户。通常最好将对 Task.Run 的调用留给最高级别的调用,除非它们确实能够利用异步 IO 或某些其他方式来卸载,否则让底层方法保持同步。 (这并非总是正确的,但通过 Task.Run 将“异步”代码包装在同步代码上,然后通过 async/await 返回,通常是设计有缺陷的标志。)


最后一个方法中的Task.Run()只是一个为了示例目的而创造的例子。在这种情况下,它可以是任何返回TaskTask<string>的方法。我只是想展示方法的嵌套。 - valdetero
2
@valdetero 是的,但是具体的示例实际上是在使用人们不知道的async/await反模式。我想提出这一点,尽管我也提到它不一定是错误的,只是需要谨慎使用。 - Reed Copsey
返回 Task.CompletedTask; - Rakka Rage

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接