Task.Run 在这里合适吗,包装网络调用?

Is Task.Run appropriate here, wrapping network calls?

我正在研究一个非常流行的 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 吗?

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

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

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 中尝试捕获并使其异步无效 - 我个人不喜欢)

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

This seems to go against the recommendations I've been reading... because Task.Run is wrapping network, not CPU calls.

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

在这种情况下(检查代理服务器的核心 accept/process 逻辑),我们正在查看的代码更像是一个 框架 ,其中 "application" 在里面 HandleClientRequest。 (请注意,此代码托管在控制台或 Win32 服务中,而不是 ASP.NET 中)。

打个比方,ASP.NET会监听连接,会从线程池中取出一个线程来处理请求。这对于 框架 来说是非常自然的。

以下是使用 Task.Run 的一些理由,这些理由可能适用于此特定情况,也可能无效:

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

So how should it work instead, if this is wrong, should it use ThreadPool.QueueUserWorkerItem?

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

So it seems the point of the Task.Run is really just to allow AcceptTcpClientAsync to be called while HandleClientRequest is busy.

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