基于任务与基于线程的看门狗——但需要异步

Task based vs. thread based Watchdog - but async needed

我们正在使用看门狗来确定连接的系统是否仍然存在。

在前面的代码中我们直接使用了TCP并且在一个单独的线程中处理看门狗。现在使用了一项新服务,它使用 gRPC 提供数据。

为此,我们尝试对任务使用异步接口,但基于任务的看门狗会失败。

我写了一个小的DEMO,抽象了代码并说明了问题。您可以通过使用 //.

注释掉第 18 行来在基于任务的看门狗和基于线程的看门狗之间切换

该演示包含导致问题的代码:

async Task gRPCSendAsync(CancellationToken cancellationToken = default) => await Task.Yield();
async Task gRPCReceiveAsync(CancellationToken cancellationToken = default) => await Task.Yield();

var  start = DateTime.UtcNow;
await gRPCSendAsync(cancellationToken).ConfigureAwait(false);
await gRPCReceiveAsync(cancellationToken).ConfigureAwait(false);
var end = DateTime.UtcNow;

if ((end - start).TotalMilliseconds >= 100)
    // signal failing

如果在 Task.Run 中使用此代码,如果应用程序在其他任务中有很多 cpu 工作要做,它将发出失败信号。

如果使用专用线程,看门狗会按预期工作,不会引发任何问题。

我确实理解这个问题:等待之后的所有代码可能(如果尚未完成或不包含“真正的”等待)排队到线程池。但是线程池还有其他事情要做,所以完成该方法的时间太长了。

是的,简单的答案是:使用线程。

但是使用线程限制了我们只能使用同步方法。无法从线程中调用异步方法。我创建了另一个 sample that shows that all code after first await will be queued to thread bool so that CallAsync().Wait() will not work. (Btw. that issue is much more handled here.)

我们有很多异步代码可以在这种时间关键型操作中使用。

所以问题是:有没有什么方法可以使用带有 async/await 的任务来执行该操作?

也许我完全错了,创建基于任务的看门狗应该完全不同。

想法

我在考虑System.Threading.Timer,但是异步发送和异步接收的问题无论如何都会导致这个问题。

以下是如何使用 Stephen Cleary 的 AsyncContext class from the Nito.AsyncEx.Context 包,以便将异步工作流限制到专用线程:

await Task.Factory.StartNew(() =>
{
    AsyncContext.Run(async () =>
    {
        await DoTheWatchdogAsync(watchdogCts.Token);
    });
}, TaskCreationOptions.LongRunning);

AsyncContext.Run 的调用将阻塞,直到提供的异步操作完成。 DoTheWatchdogAsync 创建的所有异步延续将由 AsyncContext 在当前线程上进行内部处理。在上面的示例中,由于标志 TaskCreationOptions.LongRunning used in the construction of the wrapper Task. You could confirm this by querying the property Thread.CurrentThread.IsThreadPoolThread.

,当前线程不是 ThreadPool 线程

如果您愿意,可以使用传统的 Thread 构造函数而不是有些非常规的 Task.Factory.StartNew+LongRunning.