如何安全地从EF的非异步SaveChanges方法中调用异步方法?

13

我正在使用ASP.NET Core和EF Core,它们具有SaveChangesSaveChangesAsync

在保存到数据库之前,在我的DbContext中,我执行一些审计/日志记录:

public async Task LogAndAuditAsync() {
    // do async stuff
}

public override int SaveChanges {
    /*await*/ LogAndAuditAsync();      // what do I do here???
    return base.SaveChanges();
}

public override async Task<int> SaveChangesAsync {
    await LogAndAuditAsync();
    return await base.SaveChanges();
}
问题在于同步的SaveChanges()方法。
我一直遵循“全程异步”,但这里不可能实现。我可以重新设计LogAndAudit()LogAndAuditAsync(),但这样就不符合DRY原则了,而且我还需要更改其他十几个主要代码段,这些代码并不属于我。
关于这个问题有很多其他的问题,并且所有这些问题都是普遍而复杂的,并充满争议。在这种特殊情况下,我需要知道最安全的方法
那么,在SaveChanges()中,如何在不发生死锁的情况下安全同步地调用异步方法?

你尝试过使用 await Task.Run(() => { ... }); 吗? - H. Herzl
@H.Herzl 是的,那个方法可以用。但问题不在于我不能编译或运行它,而是在这种特定情况下,我需要知道什么是最安全的方式,以避免死锁的可能性。同步和异步的错误使用很容易发生。 - grokky
绕过这个问题,是否可以使用队列来进行日志/审计操作,然后日志/审计操作自然地异步进行,这样你就根本不需要使用异步/等待了? - Menahem
@Menahem 这是一个不错的想法,但我们的系统不使用队列。 - grokky
也许现在是它该做的时候了 :) - Menahem
2个回答

11

从非异步方法调用异步方法最简单的方式是使用 GetAwaiter().GetResult()

public override int SaveChanges {
    LogAndAuditAsync().GetAwaiter().GetResult();
    return base.SaveChanges();
}

这将确保在LogAndAuditAsync中引发的异常不会作为SaveChanges中的AggregateException出现。相反,原始异常会被传播。

然而,如果代码在可能死锁的特殊同步上下文中执行(例如ASP.NET、Winforms和WPF),那么你必须更加小心。

每当LogAndAuditAsync中的代码使用await时,它都会等待任务完成。如果该任务必须在当前由调用LogAndAuditAsync().GetAwaiter().GetResult()阻塞的同步上下文上执行,则会发生死锁。

为了避免这种情况,你需要在LogAndAuditAsync中的所有await调用中添加.ConfigureAwait(false)。例如:

await file.WriteLineAsync(...).ConfigureAwait(false);

请注意,在此await之后,代码将继续在同步上下文之外执行(在任务池调度程序上)。

如果不可能,请选择在任务池调度程序上启动一个新任务:

Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();

这仍将阻塞同步上下文,但 LogAndAuditAsync 将在任务池调度程序上执行,并且不会出现死锁,因为它不必进入被阻塞的同步上下文。


我知道这种语法,但为什么在EF Core和ASP.NET Core中持久化时它是最安全的方法?有许多同步异步的方法,这只是其中之一。 - grokky

2
有很多方法可以进行异步同步,每种方法都有其注意事项。但我需要知道哪种方法对于这个特定的用例来说是最安全的。

答案是使用Stephen Cleary的"线程池hack"

Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();

原因在于,在该方法中,只执行了更多的数据库工作,没有其他操作。原始的同步上下文是不需要的——在EF Core的DbContext中,您不应该需要访问ASP.NET Core的HttpContext!因此,最好将操作卸载到线程池中,并避免死锁。

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