为什么这个 while 循环只运行一次然后冻结直到结果任务完成?Blazor Server c#

Why is this while loop only running once then freezing until the result task is complete? Blazor Server c#

提问人:Bobby Bridgeman 提问时间:11/6/2023 最后编辑:Bobby Bridgeman 更新时间:11/7/2023 访问量:73

问:

我有以下 Blazor Server 代码,该代码调用未等待的异步任务,而是使用 while 循环来检查任务的完成或异常。我遇到的问题是 while 循环似乎只运行一次,然后似乎只是停止。该任务没有出错,也没有完成,但一旦完成,它就会退出 while 循环并按预期继续。

谁能解释一下这里发生了什么?

    private async Task Generate()
    {
        awaitingRequest = true;
        StateHasChanged();

        var result = gPT.SendCommand("system", bot.Description, apiKey, true);

        try
        {
            loopCount = 0;
            while (!result.IsCompleted && !result.IsFaulted)
            {
                //Update the UI after the asynchronous operation
                await Task.Delay(500);               

                loopCount++;

                StateHasChanged();
            }
        } 
        catch (Exception ex)
        {
            
        }

        outputText = gPT.messages[1].content;
        awaitingRequest = false;
        StateHasChanged();
    }

我尝试了各种断点,并将 loopCount 绑定到文本区域控件以进行视觉确认,两者都只是指示它在第二次调用 await Task.Delay(500) 后停止在 while 循环中。我尝试设置ConfigureAwait(false),但没有区别。我基本上希望在gPT.SendCommand方法运行时更新UI。我以前使用过这种方法,效果很好,所以我不知道是什么原因造成的。

C# asp.net .NET 异步 Blazor

评论

3赞 Andrew Williamson 11/6/2023
返回类型为 。将其更改为async voidasync Task
1赞 Andrew Williamson 11/6/2023
我以前被这个抓住了,这是一个很容易犯的错误
1赞 Andrew Williamson 11/6/2023
等等,你的意思是整个方法没有被等待吗?如果是这样,则在发生异常时没有反馈。尝试添加一些错误处理并放置断点以确认您没有收到未经处理的异常
1赞 Bobby Bridgeman 11/6/2023
@AndrewWilliamson 尝试将其更改为 Task,但问题仍然存在。此外,结果任务有自己的错误检查,并且确实完成了所有正常(大约 10 秒后)。while 循环似乎只是挂起,然后在结果任务实际完成时再次开始?
1赞 Bobby Bridgeman 11/6/2023
还为本节添加了 try-catch,但没有捕获任何内容。

答:

1赞 Bobby Bridgeman 11/6/2023 #1

我认为问题是 gPT.SendCommand 虽然是一个异步任务,但没有在自己的线程上运行。使用 Task.Run 解决了这个问题。也许有人可以证实这一点。

 var result = Task.Run(async () => await gPT.SendCommand("system", bot.Description, apiKey, true));

解释

  • 调用该方法。它开始在 UI 线程上运行。Generate()
  • 该方法由 调用。任务在 UI 线程(与父方法相同的线程)上排队。SendCommand()Generate()
  • 该方法经历 while 循环的第一次迭代并调用 ,从而生成线程。Generate()await Task.Delay(500)
  • UI 线程开始处理其队列中的任何其他任务
  • 任务被拾取SendCommand
  • 任务在完成之前不会屈服SendCommand
    • 这表明它正在轮询响应,而不是使用异步 IO
  • 完成后,UI 线程将继续处理其队列中的任务,并返回到该任务Generate

Stephen Toub 在他关于 ConfigureAwait 的文章中描述了类似的情况:

考虑一个库方法,该方法对某些网络下载的结果使用 await。调用此方法并同步阻止等待它完成,例如使用 。Wait() 或 .result 或 .GetAwaiter() 中。GetResult() 关闭返回的 Task 对象。现在考虑一下,如果当前 SynchronizationContext 将可在其上运行的操作数限制为 1 时,如果调用它会发生什么情况,无论是通过前面所示的 MaxConcurrencySynchronizationContext 之类的显式内容,还是通过仅具有一个可以使用的线程的上下文(例如 UI 线程)隐式调用它。因此,您可以在该线程上调用该方法,然后阻止它等待操作完成。该操作将启动网络下载并等待它。由于默认情况下等待 Task 将捕获当前的 SynchronizationContext,因此它会这样做,并且当网络下载完成时,它会将回调回 SynchronizationContext,该回调将调用操作的其余部分。但是,唯一可以处理排队回调的线程当前被代码阻塞阻止,等待操作完成。在处理回调之前,该操作不会完成。僵局!

为什么不改变任何东西?ConfigureAwait(false)

ConfigureAwait更改等待任务的行为。它确定延续('await' 关键字后面的所有内容)是否应在同一同步上下文(UI 线程)上运行,还是在线程池中的任何线程上运行。在以下示例中,第二次调用将在线程池中的默认线程上执行(并引发异常):StateHasChanged()

awaitingRequest = true;
StateHasChanged();

await gPT.SendCommand("system", bot.Description, apiKey, true).ConfigureAwait(false);

StateHasChanged();

在您的情况下,您没有执行任务,因此调用不起作用。awaitConfigureAwait

评论

1赞 Andrew Williamson 11/6/2023
我猜会生成一个连续运行的子任务,当您生成它并在命令持续时间内保持它时,它会接管 UI 同步上下文。应该创建该子任务来防止这种情况,但您的解决方法是下一个最佳解决方案SendCommandConfigureAwait(false)
1赞 Bobby Bridgeman 11/6/2023
谢谢你的建议。我确实试过了,但没有用。我猜理论上,如果 UI 线程是空闲的,即使使用 ConfigureAwait(false),它也可以返回到 UI 线程,这可能是在 Task.Delay 运行时。我对 ConfigureAwait(false) 的理解是它允许异步方法在任何可用线程(而不仅仅是上下文线程)上返回,而 Task.Run 将强制使用新线程。
1赞 Andrew Williamson 11/7/2023
我的意思是在 gpt 调用 ConfigureAwait(false)。SendCommand,而不是它返回的任务。调用代码肯定需要返回到相同的同步上下文,否则在尝试更新 UI 时可能会出现异常。我会在你的回答中添加一个更详细的解释
1赞 MrC aka Shaun Curtis 11/7/2023
这看起来像是在 Task 中运行的同步代码块的情况。它没有像你预期的那样屈服。gPT.SendCommand