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

gro*_*kky 11 c# asynchronous entity-framework async-await entity-framework-core

我使用ASP.NET的核心,和EF核心,拥有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();
}
Run Code Online (Sandbox Code Playgroud)

问题是同步SaveChanges().

我总是"一直异步",但这里不可能.我可以重新设计LogAndAudit(),LogAndAuditAsync()但这不是DRY,我需要更改十几个不属于我的主要代码.

关于这个主题还有很多其他问题,所有问题都是一般的,复杂的,充满争议.在这个具体案例中,我需要知道最安全的方法.

那么,在SaveChanges()没有死锁的情况下,如何安全地同步调用异步方法呢?

Mar*_*age 9

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

public override int SaveChanges {
    LogAndAuditAsync().GetAwaiter().GetResult();
    return base.SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)

这将确保抛出的异常LogAndAuditAsync不会显示为AggregateExceptionin SaveChanges.而是传播原始异常.

但是,如果代码在特殊同步上下文上执行,在执行异步同步(例如ASP.NET,Winforms和WPF)时可能会死锁,那么您必须更加小心.

每次LogAndAuditAsync使用代码时,await它都会等待任务完成.如果此任务必须在当前被调用阻止的同步上下文上执行,LogAndAuditAsync().GetAwaiter().GetResult()则会出现死锁.

为避免这种情况,您需要添加.ConfigureAwait(false)到所有await呼叫中LogAndAuditAsync.例如

await file.WriteLineAsync(...).ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)

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

如果这不可能,您的最后一个选项是在任务池调度程序上启动新任务:

Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();
Run Code Online (Sandbox Code Playgroud)

这仍将阻止同步上下文,但LogAndAuditAsync将在任务池调度程序上执行而不是死锁,因为它不必进入被阻止的同步上下文.


gro*_*kky 2

有很多方法可以实现异步同步,每种方法都有其缺陷。但我需要知道对于这个特定的用例来说哪个是最安全的

答案是使用 Stephen Cleary 的“线程池黑客”

Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();
Run Code Online (Sandbox Code Playgroud)

原因是在该方法内,只执行更多的数据库工作,没有执行其他任何操作。不需要原始的同步上下文 - 在 EF Core 中,DbContext您不需要访问 ASP.NET Core 的HttpContext

因此,最好将操作卸载到线程池,并避免死锁。