Task.Run在这里是否合适,包装网络电话?

时间:2017-03-29 04:38:10

标签: c# asynchronous async-await threadpool

我正在研究一种非常流行的c#代理服务器。在其中有代码,例如(我故意隐藏代理服务器身份,因为我觉得很难讲故事,如果确实是错误的话)。

private void OnConnectionAccepted(TcpClient cl)
{
     ....
     Task.Run(async () =>
            {
               await HandleClientRequest();

            });
     Task.Run(async () =>
        {
            TcpClient cl = await listener.AcceptTcpClientAsync();
            OnConnectionAccepted(cl);
        });
}

HandleClientRequest执行某些异步网络调用,例如从客户端读取请求并将其重复发送到服务器,就像您希望代理一样。 listener.AcceptTcpClientAsync只是等待来自浏览器的下一个连接。

所以看起来Task.Run的重点只是允许在AcceptTcpClientAsync忙碌时调用HandleClientRequest

这似乎违背了我一直在阅读的建议,例如。 http://blog.stephencleary.com/2013/10/taskrun-etiquette-and-proper-usage.html因为Task.Run正在包装网络,而不是CPU调用。

那么它应该如何工作,如果这是错误的,它应该使用ThreadPool.QueueUserWorkerItem吗?

顺便说一句,上下文通常是一个控制台应用程序。

2 个答案:

答案 0 :(得分:5)

  

这似乎违背了我一直在阅读的建议......因为Task.Run包装网络,而不是CPU调用。

通常,Task.Run不应用于I / O.但是,该建议仅适用于应用程序代码,甚至有几个例外。

在这种情况下(检查代理服务器的核心接受/处理逻辑),我们正在查看的代码更像是一个框架,其中“application”位于{{1 }}。 (请注意,此代码托管在Console或Win32服务中,而不在ASP.NET中)。

要绘制并行,ASP.NET将侦听连接,并从线程池中获取一个线程来处理请求。这对框架来说非常自然。

以下是使用HandleClientRequest的一些原因,这些原因可能适用于此特定情况,也可能不适用:

  • 某些.NET网络调用以同步部分开头 - 特别是HTTP代理和DNS查找是同步完成的。这是非常不幸的,但出于向后兼容的原因,我们坚持使用它。因此,即使异步网络API也可以部分同步。如果Task.Run使用这些API,则将其包装在HandleClientRequest中是有意义的。
  • 代码可能不需要当前的Task.Run。但是,这似乎不是这种情况。
  • 所有异步方法都开始同步执行。如果SynchronizationContext在开始实际工作之前做了一些“管家”,那么将它包装在HandleClientRequest中可能是有益的,这样听众就不会被阻止。
  • 如果同步执行,递归异步代码可以填充堆栈。例如,如果第二个Task.Run不存在且一次连接的批次,那么堆栈可能会溢出。我认为这是第二个Task.Run的目的,因为我无法想到它会有任何其他目的。
  

那么它应该如何工作,如果这是错误的,它应该使用ThreadPool.QueueUserWorkerItem吗?

绝对不是! Task.Run是使用线程池的适当方式。 (除非你有一个非常聪明的团队,否则你可以显示出可测量的性能差异。)

  

所以看起来Task.Run的目的只是允许在HandleClientRequest忙的时候调用AcceptTcpClientAsync。

Task.Run的用法充其量仍然值得怀疑,因为迟早要等待 的任务。目前的情况是,Task.RunHandleClientRequestAcceptTcpClientAsync的所有例外情况都会被默默删除。如果OnConnectionAcceptedHandleClientRequest都有顶级OnConnectionAccepted块,那就没问题,但try的任何异常都会导致整个代理服务器停止工作。并且该方法可以抛出异常,尽管很少见。

答案 1 :(得分:1)

我建议将这一点重构为一个接受连接的方法和另一个处理传入数据的方法。最后,第三个处理失败的任务。

public async Task Acceptor(TcpListener listener)
{
  for (;;)
  {
    TcpClient client = await listener.AcceptTcpClientAsync();
    OnConnectionAccepted(client).ContinueWith(task => { errorHandling }, OnlyOnFaulted);
    // some logic which will stop this loop if necessary
  }
}

private async Task OnConnectionAccepted(TcpClient client)
{
  await HandleClientRequest(client);
  // .. further logic
}
当侦听器不再接受新连接时,

“Acceptor”将抛出。当无法处理某个特定连接时,将调用“errorHandling”。 (您也可以考虑使用OnConnectionAccepted中的try catch并使其异步无效 - 我个人不喜欢这样做)

Stephen是对的,几乎没有任何使用Task.Run的情况。这也是因为async / await和Task.Run在不同的抽象级别上工作。除非您正在编写某种类型的库async / await应该总是足够。