异步等待同步行为

Async-await behaving synchronously

我实现了一小段异步代码,但遇到了一个奇怪的行为。

基本上我想做的是运行一组多个"clients"的初始化过程,我不想在队列中处理它们(有些可能需要时间来处理,其他人可能不会)。我只希望它们全部完成,以便进入下一步。为了避免同时有太多进程 运行ning,我使用了一个信号量(暂时设置为 2)。

我遇到的问题是执行似乎是同步完成的。这是一段代码(缩小:无日志记录,try/catch,等):

public void IntializeReportStructure(DateTimeOffset inReferenceDate)
{
    List<Task> theTaskCollection = new List<Task>();
    foreach (long theClientId in this.GetClientIdCollection())
    {
        Task theTask = this.InitializeClientAsync(theClientId, inReferenceDate.Year, inReferenceDate.Month);
        theTaskCollection.Add(theTask);
    }

    Task.WaitAll(theTaskCollection.ToArray());
}

private async Task InitializeClientAsync(long inClientId, int inReferenceYear, int inReferenceMonth)
{
    await this.Semaphore.WaitAsync();
    await this.InitializeClientReportAsync(inClientId);
    await this.InitializeClientSummaryAsync(inClientId, inReferenceYear, inReferenceMonth);
    this.Semaphore.Release();
}

日志内容如下:

正在等待客户端 41 的信号量。当前计数为 2。
为客户端 41 输入信号量。当前计数为 1。
已为客户端 41 释放信号量。当前计数为 2。
正在等待客户端 12 的信号量。当前计数为 2。
已为客户端 12 输入信号量。当前计数为 1。
已为客户端 12 释放信号量。当前计数为 2。
等待客户端 2 的信号量。当前计数为 2。
为客户端 2 输入信号量。当前计数为 1。
已为客户端 2 释放信号量。当前计数为 2。
正在等待客户端 261 的信号量。当前计数为 2。
已为客户端 261 输入信号量。当前计数为 1。
已为客户端 261 释放信号量。当前计数为 2。
等待客户端 1 的信号量。当前计数为 2。
为客户端 1 输入信号量。当前计数为 1。
已为客户端 1 释放信号量。当前计数为 2。
等待客户端 6 的信号量。当前计数为 2。
为客户端 6 输入信号量。当前计数为 1。
已为客户端 6 释放信号量。当前计数为 2。

如您所见,每个任务只有在前一个任务完成后才会进入信号量步骤。我之前说过进程是 运行 异步的:在循环内(将任务添加到列表时),任务状态是 "RanToCompletion" 并且 IsCompleted 的值是 "true"。我没有在日志中包含时间戳,但我们有一些 "client" 需要 15-20 秒到 运行,同时该过程是 "waiting"。

最后一个细节,"InitializeClientReportAsync"和"InitializeClientSummaryAsync"这两个方法只是做异步获取数据和异步保存数据。那边没什么奇怪的。

有趣的部分 是我可以通过在信号量 WaitAsync 之后添加 "await Task.Delay(1);" 来获得异步结果。

private async Task InitializeClientAsync(long inClientId, int inReferenceYear, int inReferenceMonth)
{
    await this.Semaphore.WaitAsync();
    await Task.Delay(1);
    await this.InitializeClientReportAsync(inClientId);
    await this.InitializeClientSummaryAsync(inClientId, inReferenceYear, inReferenceMonth);
    this.Semaphore.Release();
}

这是日志内容:

正在等待客户端 41 的信号量。当前计数为 2。
正在等待客户端 12 的信号量。当前计数为 1。
正在等待客户端 2 的信号量。当前计数为 0。
正在等待客户端 261 的信号量。当前计数为 0。
等待客户端 1 的信号量。当前计数为 0。
等待客户端 6 的信号量。当前计数为 0。
已为客户端 12 输入信号量。当前计数为 0。
已为客户端 41 输入信号量。当前计数为 0。
已为客户端 12 释放信号量。当前计数为 0。
已为客户端 2 输入信号量。当前计数为 0。
已为客户端 41 释放信号量。当前计数为 0。
已为客户端 261 输入信号量。当前计数为 0。
已为客户端 261 释放信号量。当前计数为 0。
已为客户端 1 输入信号量。当前计数为 0。
已为客户端 1 释放信号量。当前计数为 0。
已为客户端 6 输入信号量。当前计数为 0。
已为客户端 2 释放信号量。当前计数为 1。
已为客户端 6 释放信号量。当前计数为 2。

如您所见,进程在进入信号量之前都首先进入信号量。另外,我们意识到客户端“2”是初始化时间最长的一个。这首先是我对代码的期望。

我不知道为什么一个简单的 Task.Delay(1) 会改变我的进程的行为。这没有多大意义。可能我太专注了,所以漏掉了一些明显的东西。

编辑

如评论中所述,问题来自 "async" Entity Framework 代码。我认为这是理所当然的。

为了示例,我简化了底层方法的内容。 "IDbContext"只是我们在Entity Framework的DbContextclass上使用的一个接口。在我们的例子中,异步方法直接来自初始 class.

private async Task InitializeClientAsync(long inClientId, int inReferenceYear, int inReferenceMonth)
{
    await this.Semaphore.WaitAsync();
    await this.SomeTest();
    this.Semaphore.Release();
}

private async Task SomeTest()
{
    using (IDbContext theContext = this.DataAccessProvider.CreateDbContext())
    {
        List<X> theQueryResult = await theContext.Set<X>().ToListAsync<X>();
        foreach (X theEntity in theQueryResult)
        {
            theEntity.SomeProperty = "someValue";
        }

        await theContext.SaveChangesAsync();
    }
}

所以在这里,即使我检索了数百 MB 的实体并更新它们(全部使用 await 和 async 方法),一切都保持完全同步。只有当这个任务完成时,下一个任务才会进入信号量。

我在SaveChangesAsync()之前加了一个Task.Delay(1),下一个Task在第一个task完成之前就进入了信号量。它确认这里的一切都是同步的(Task.Delay 除外)。但是我不能说为什么...

感谢您的所有回答,我想我们能够更深入地了解问题所在:不是真正的异步等待问题,而是更多 Entity Framework 查询阻塞了进程。

我仍然不完全理解为什么他们不 运行 异步。我的猜测是他们应该...我一定会对此进行更多调查。

由于现在的主题有点不同,我想我可以将这个问题标记为已回答。

编辑

在结束问题之前,我想就剩下的问题提供更多细节。

我用 EF6 异步查询 (ToListAsync) 做了一个小例子。在这里,我希望首先在我们的日志中看到 "STARTED",紧接着是 "PENDING",然后在检索到数据后 "FINISHED"。

private static async Task DoSomething()
{
    ILog theLogger = LogManager.GetLogger("test");
    using (Context theContext = new Context())
    {
        theLogger.Info("STARTING");
        Task<List<Transaction>> theTask = theContext.Set<Transaction>().ToListAsync();
        theLogger.Info("PENDING");
        var theResult = await theTask;
    }

    theLogger.Info("FINISHED");
}

2020-04-03 13:35:55,948 [1] INFO  test [(null)] - STARTING
2020-04-03 13:36:11,156 [1] INFO  test [(null)] - PENDING
2020-04-03 13:36:11,158 [1] INFO  test [(null)] - FINISHED

如您所见,"PENDING" 发生在检索数据之后(完成所有工作时)。所以,无论你是否使用异步,结果都是一样的。

我用一个简单的 Task.Delay 而不是查询尝试了相同的示例。

private static async Task DoSomething()
{
    ILog theLogger = LogManager.GetLogger("test");
    using (Context theContext = new Context())
    {
        theLogger.Info("STARTING");
        Task theTask = Task.Delay(20000);
        theLogger.Info("PENDING");
        await theTask;
    }

    theLogger.Info("FINISHED");
}

2020-04-03 13:34:51,858 [1] INFO  test [(null)] - STARTING
2020-04-03 13:34:51,907 [1] INFO  test [(null)] - PENDING
2020-04-03 13:35:21,922 [5] INFO  test [(null)] - FINISHED

这里,一切正常。只有在遇到 await 关键字时才会暂停进程。

有没有人以前遇到过这种行为?正常吗?