【问题标题】:Task based vs. thread based Watchdog - but async needed基于任务与基于线程的看门狗 - 但需要异步
【发布时间】:2020-07-30 13:11:55
【问题描述】:

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

在前面的代码中,我们直接使用 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,它显示第一个await 之后的所有代码都将排队到线程bool,这样CallAsync().Wait() 将无法工作。 (顺便说一句。这个问题得到了更多的处理here。)

我们有很多异步代码可以在这些时间紧迫的操作中使用。

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

也许我完全错了,创建基于任务的看门狗应该以非常不同的方式完成。

想法

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

【问题讨论】:

  • 您是否考虑过increasing the minimum number of threads 线程池按需创建,例如在程序开始时调用ThreadPool.SetMinThreads(50, 10);
  • 附带说明,比较通过调用UtcNow 获得的DateTimes 不是衡量持续时间的可靠方法,因为它取决于可能受到非确定性调整的系统时钟.我建议改用Stopwatch
  • @TheodorZoulias 是的,我做到了——但这不是真正的解决方案。这只会将问题稍微转移到未来。此外,并非每个使用的 CPU 都提供 64 个线程,因此这将是线程切换成本和线程数之间的走钢丝。
  • @TheodorZoulias 是的,我知道。我们的生产代码使用的是秒表,但这并不重要,我尝试创建一个简单的演示来说明问题。
  • @TheodorZoulias 我们目前出于其他原因使用Nito.AsyncEx,没有理由反对Context。我之前没有尝试过,因为我不知道它。我会试一试——在阅读了一些文档以了解它的作用之后。谢谢你的提示。

标签: c# multithreading async-await task


【解决方案1】:

您可以通过以下方式使用 Nito.AsyncEx.Context 包中的 Stephen Cleary 的 AsyncContext 类,以便将异步工作流约束到专用线程:

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

AsyncContext.Run 的调用将阻塞,直到提供的异步操作完成。 DoTheWatchdogAsync 创建的所有异步延续将由当前线程上的AsyncContext 在内部处理。在上面的示例中,当前线程不是ThreadPool 线程,因为在构造包装器Task 中使用了标志TaskCreationOptions.LongRunning。您可以通过查询属性Thread.CurrentThread.IsThreadPoolThread 来确认这一点。

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

【讨论】:

  • 主要是。这是迄今为止我看到的最好的希望。 I tried it. 但问题是我必须删除 .ConfigureAwait(false); 才能使其工作。如果使用 .ConfigureAwait(false); 调用第 43 行和第 44 行,则它不起作用。目前我不明白为什么这不起作用。待续......
  • @SebastianSchumann 是的,好点。任何ConfigureAwait(false) 都会将继续发送到ThreadPool,因此您应该删除所有这些以留在AsyncContext
  • 由于@StephenCleary 的解释,我接受了这个答案,因为使用AsyncContext 是我们拥有的最佳选择。谢谢。
  • 我应该注意到AsyncContext 解决方案仅在顶层有效。例如,如果 gRPCSendAsyncgRPCReceiveAsync 方法的内部工作依赖于 ThreadPool 线程,那么您可能不会从使用 AsyncContext 中获得任何好处。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
  • 2011-12-04
  • 1970-01-01
  • 2017-04-22
  • 2012-04-09
相关资源
最近更新 更多