C#异步库方法应该调用await吗?

11

异步库方法调用是否应该使用await?例如,假设我有一个数据服务库方法,它可以访问名为“repository”的Entity Framework 6数据上下文。据我所见,我有两种定义此方法的方式:

public static async Task<IEnumerable<Blogs>>
    GetAllBlogsAsync(EfDataContext db)
{
    return await db.Blogs
        .OrderByDescending(b => b.Date)
        .SelectAsync();
}

或者没有 async/await 修饰符的情况下

public static Task<IEnumerable<Blogs>>
    GetAllBlogsAsync(EfDataContext db)
{
    return db.Blogs
        .OrderByDescending(b => b.Date)
        .SelectAsync();
}
在应用程序终点,即MVC控制器操作中,对于任何一种方法,调用方式都相同。
public async Task<ActionResult> Blogs()
{
    var blogs = await BlogService.GetAllBlogs(_blogRepository);
    return View(blogs);
}

当然,这种情况可能会更加复杂,应用程序调用一系列异步方法。每个链中的方法都应该调用await吗?还是只应该在调用链末尾只有一个await语句,这会产生什么区别呢?


我不确定你所说的“调用链”是什么意思,我猜这取决于你如何定义这些方法。而类型将指导你编写正确的代码。 - Gábor Bakos
我所说的“调用链”只是指应用程序端点可能会调用库中的异步方法,而该库又会调用另一个库中的异步方法。应用程序调用Lib1.MethodX,该方法调用Lib2.MethodY,Lib2.MethodY调用Lib3.MethodZ,以此类推。 - Neilski
我真的不明白你为什么要问这个问题。你认为多个await会做什么? - Aron
1
我不知道,所以才问。我想我的担忧是调用另一个await方法是否会增加任何开销,或者是否会引入其他线程复杂性,例如死锁。事实上,我似乎可以使用async/await定义底层库,也可以不使用,这让我感到困扰,因为我不明白这可能会产生什么样的差异。 - Neilski
我认为在这两种情况下,你的调用应该是BlogService.GetAllBlogsAsync。 - user848765
显示剩余2条评论
4个回答

2

在将可处理的对象传递给异步方法时要小心。

using (var db = new EfDataContext())
{
    return BlogService.GetAllBlogs(db);
}

如果在Task执行查询完成之前处理DataContext,则可能会收到ObjectDisposedException(或任何其他类型,从访问已处理的上下文引起)。如果您可以确保在处理上下文之前等待任务完成,则最好仅返回Task(无需使用async),因为它将涉及少一步。
编辑:虽然大多数情况下是在using块中使用可处理对象,但对于所有类型的引用,在任务完成之前更改对象状态也是如此。例如,以下示例一旦调用await TestAsync()将抛出DivideByZeroException。
public class MyClass
{
    public int Value { get; set; }
}

public static async Task<int> DivideAsync(MyClass myClass)
{
    await Task.Yield();
    return 2 / myClass.Value;
}

public static Task<int> TestAsync()
{
    MyClass myClass = new MyClass { Value = 4 };
    Task<int> result = DivideAsync(myClass);
    myClass.Value = 0;
    return result;
}

在我看来,如果您要更改任务将使用的对象的状态,最好的做法是等待任务。


好的,谢谢。希望我能避免这个问题,因为我正在使用AutoFac依赖注入在MBV控制器的构造函数中进行新的存储库,并使用InstancePerRequest()处理程序。 - Neilski

1
如果您返回内部的Task,则会失去对可能发生在内部方法中的异常进行净化或处理的能力。
例如,您可能希望使用特殊类型为您的库包装所有异常:
public static async Task<IEnumerable<Blogs>>
    GetAllBlogsAsync(EfDataContext db)
{
    try
    {
        // If a fault occurs in the linq query, the 
        // exception is raised by the 'await' statement
        return await db.Blogs
            .OrderByDescending(b => b.Date)
            .SelectAsync();
    }
    catch (Exception ex)
    {
        throw new BlogLibraryException("This blog appears to be broken.", ex);
    }
}

如果您立即返回Task,则无法执行初始异常,直到等待任务。

只要避免调用Task.WaitTask.GetResultTask.Result,就不会有死锁的风险。启动任务的开销很小,特别是如果您只是将工作委托给数据库进行“真正的异步”(系统边界上的IO)。


等等,try-catch-throw不是反模式吗?为什么你还要捕获你无法处理的错误呢? - Adrian Salazar
3
因为有些人在使用该库时,希望能够以与库本身未引起的异常不同的方式处理BlogLibraryExceptions。 - Maximilian Riegler

1

异步库方法调用应该使用等待吗?

简短的回答:是。

长的回答:是,除非你担心服务器应用程序中的内存波动,而且该方法除了调用另一个方法并等待之外没有其他操作。

删除asyncawait是一种优化,但非常微小。就像大多数优化一样,如果不非常小心,它可能会引起问题。

在您的示例中:

return db.Blogs
    .OrderByDescending(b => b.Date)
    .SelectAsync();

这大致相当于:
var blogs = db.Blogs;
var query = blogs.OrderByDescending(b => b.Date);
var task = query.SelectAsync();

其中只有最后一个是真正的异步调用。在这种情况下,如果您确定BlogsOrderByDescending不会抛出异常,那么安全地省略asyncawait


如果我的数据层(EF)有ToListAsyn(),我会在每个层上使用await db.ToListAsync().。我的业务逻辑类似于await GetAsync(),并且我在每个层上都使用了await。这样做的方式正确吗? - Eldho
1
@Eldho:如果这是你的问题,你应该始终使用异步。 - Stephen Cleary
谢谢,现在我对我的异步使用是真实的。 - Eldho
在阅读了支持和反对在库函数中使用await的论点后,我有些犹豫。例如,John Hanna的回答可能是技术上最实用和正确的。然而,总的来说,如果你还没有完全掌握这些机制以及它们真正的作用,使用它可能比不使用更安全。 - Neilski

1

我不会在那里使用async

但我会在库中使用它。

如果你正在尾调用返回任务的方法,那么不使用async是:

  1. 更快。

  2. 更简单。

  3. 可内联。

  4. 同样容易调试(您真的会因为堆栈跟踪不来自await点而感到困惑吗?)

  5. 不太可能出现异常。

  6. 如果异常是从SelectAsync而不是GetAllBlogsAsync抛出的,那也没什么大不了的

  7. 如果有任何更改使其更具吸引力,则可以轻松替换为async形式。

最后一点更重要。如果进行这种更改是一件大事,那么过早地悲观await可能值得谨慎,但实际上并非如此。

async 的开销很低,但为什么要写代码来引入一些开销呢?

这并不意味着我会用“否”来回答“C#异步库方法应该调用 await 吗?”。在 async 不是尾调用返回任务的方法时,我肯定会这样做。但我几乎总是使用 .ConfigureAwait(false),因为我通常不希望从调用代码传递上下文到我正在等待的 await 中,这可能会导致死锁。


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