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

C# 基于任务与基于线程的看门狗-但需要异步,c#,multithreading,async-await,task,C#,Multithreading,Async Await,Task,我们正在使用看门狗来确定连接的系统是否仍然存在 在前面的代码中,我们直接使用TCP,并在单独的线程中处理看门狗。现在使用的是一种新服务,它使用gRPC提供数据 为此,我们尝试对任务使用异步接口,但基于任务的看门狗将失败 我编写了一个小程序,对代码进行了抽象并说明了问题。您可以在基于任务的看门狗和基于线程的看门狗之间切换,方法是用/注释掉第18行 演示包含导致问题的以下代码: async Task gRPCSendAsync(CancellationToken cancellationToken

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

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

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

我编写了一个小程序,对代码进行了抽象并说明了问题。您可以在基于任务的看门狗和基于线程的看门狗之间切换,方法是用
/
注释掉第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工作要做,则会发出失败信号

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

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

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

但是使用线程限制了我们只能使用同步方法。无法从线程中调用异步方法。我创建了另一个,它显示在第一个
await
之后的所有代码都将排队到线程bool,这样
CallAsync().Wait()
将无法工作。(顺便说一句,这个问题要处理得多。)

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

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

也许我完全错了,创建一个基于任务的看门狗应该做得非常不同

思想


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

以下是如何从包中使用Stephen Cleary的类,以便将异步工作流约束到专用线程:

await Task.Factory.StartNew(() =>
{
    AsyncContext.Run(async () =>
    {
        await DoTheWatchdogAsync(watchdogCts.Token);
    });
}, TaskCreationOptions.LongRunning);
AsyncContext.Run
的调用将被阻止,直到提供的异步操作完成。由
DoTheWatchdogAsync
创建的所有异步延续将由当前线程上的
AsyncContext
进行内部处理。在上面的示例中,当前线程不是
ThreadPool
线程,因为在构建包装器
任务时使用了标记。您可以通过查询属性来确认这一点


如果您愿意,您可以使用传统的
线程
构造函数,而不是非传统的
任务.Factory.StartNew
+
长时间运行

您是否考虑过线程池按需创建,例如调用
ThreadPool.SetMinThreads(50,10)在程序开始时?作为旁注,比较通过调用
UtcNow
获得的
DateTime
s不是测量持续时间的可靠方法,因为它取决于可进行非确定性调整的系统时钟。“我建议改用一种新的方法。”西奥多·佐利亚斯是的,我这样做了,但这不是真正的解决办法。这只会将问题稍微转移到未来。此外,并不是每个使用过的CPU都能提供64个线程,所以这将是线程转换成本和线程数量之间的一条走钢丝绳。@TheodorZoulias是的,我知道。我们的高效代码使用秒表,但这并不重要,我试图创建一个简单的演示来说明问题。@TheodorZoulias我们目前使用的是
Nito.AsyncEx
,原因还有其他,没有理由反对
上下文
。我以前没有试过,因为我不知道。在阅读了一些文档以了解它的功能之后,我将尝试一下。谢谢你的提示,大部分时间。这是迄今为止我看到的最好的希望。但问题是我必须删除
.configurewait(false)使其工作。如果使用
.ConfigureAwait调用第43行和第44行(false)它不工作。此刻我不明白为什么这不起作用。继续…@SebastianSchumann yeap,说得好。任何
ConfigureAwait(false)
都会将延续发送到
线程池
,因此您应该删除所有延续,以便留在
AsyncContext
。由于@StephenCleary的解释,我接受了这个答案,因为使用
AsyncContext
是我们的最佳选择。Thx.我应该注意,
AsyncContext
解决方案仅在顶层有效。例如,如果方法
grpcsendsync
gRPCReceiveAsync
依赖
ThreadPool
线程进行内部工作,那么使用
AsyncContext
可能不会给您带来任何好处。