【问题标题】:Is Task.Run appropriate here, wrapping network calls?Task.Run 在这里合适吗,包装网络调用?
【发布时间】:2017-08-22 09:45:54
【问题描述】:

我正在研究一个非常流行的 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 的重点似乎只是允许在 HandleClientRequest 忙碌时调用 AcceptTcpClientAsync

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

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

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

【问题讨论】:

  • 您调用的两个方法都是异步的,因此返回 should 任务。为什么不创建这些任务的数组并调用 Task.WaitAll(...)?请注意,您可能希望在返回的 Task 对象上调用 .ConfigureAwait(...) ,否则您可能会有 very long wait :)
  • P.S. Stephen 的博客说您不应该在异步方法的实现中使用 Task.Run,​​但建议您可以使用 Task.Run 来调用异步方法。
  • 为什么你觉得你必须用Task.Run 包装这些异步调用?你为什么不等待他们中的任何一个?
  • 记住这不是我的代码,我试图了解它是否是最好的方法。

标签: c# asynchronous async-await threadpool


【解决方案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 中尝试捕获并将其设置为 async void - 我个人不喜欢)

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

【讨论】:

    【解决方案2】:

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

    一般来说,Task.Run 不应该用于 I/O。但是,该建议是针对应用程序代码的,即使有几个例外。

    在这种情况下(检查代理服务器的核心接受/处理逻辑),我们正在查看的代码更像是一个框架,其中“应用程序”位于HandleClientRequest 内部. (请注意,此代码托管在控制台或 Win32 服务中,而不是 ASP.NET 中)。

    为了实现并行,ASP.NET 将侦听连接并从线程池中获取一个线程来处理请求。这对于 框架 来说是非常自然的。

    以下是使用Task.Run 的一些原因,对于这种特殊情况可能有效也可能无效:

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

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

    绝对不是! Task.Run 是使用线程池的合适方式。 (除非您拥有一支非常聪明的团队并且您可以显示出可衡量的绩效差异)。

    因此,Task.Run 的重点似乎只是允许在 HandleClientRequest 忙时调用 AcceptTcpClientAsync。

    Task.Run 的用法充其量仍然值得怀疑,因为任务应该迟早要等待。按照目前的情况,HandleClientRequestAcceptTcpClientAsyncOnConnectionAccepted 的任何异常都将被静默删除。如果HandleClientRequestOnConnectionAccepted 都有顶级try 块,那没关系,但是AcceptTcpClientAsync 的任何异常都会导致整个代理服务器停止工作。而且该方法可以抛出异常,尽管这种情况很少见。

    【讨论】:

      猜你喜欢
      • 2016-09-04
      • 2014-12-21
      • 2014-08-09
      • 2014-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-17
      相关资源
      最近更新 更多