我会让框架管理线程并且不会创建任何额外的线程,除非分析测试表明我可能需要这样做。特别是如果HandleConnectionAsync 内部的调用主要是 IO 绑定的。
不管怎样,如果你想在HandleConnectionAsync开头释放调用线程(dispatcher),有一个非常简单的解决方案。 您可以使用await Yield() 从ThreadPool 跳转到一个新线程。 如果您的服务器在初始线程上没有安装任何同步上下文的执行环境中运行(控制台应用程序, WCF 服务),通常是 TCP 服务器的情况。
以下说明了这一点(代码最初来自here)。请注意,while 主循环不会显式创建任何线程:
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Program
{
object _lock = new Object(); // sync lock
List<Task> _connections = new List<Task>(); // pending connections
// The core server task
private async Task StartListener()
{
var tcpListener = TcpListener.Create(8000);
tcpListener.Start();
while (true)
{
var tcpClient = await tcpListener.AcceptTcpClientAsync();
Console.WriteLine("[Server] Client has connected");
var task = StartHandleConnectionAsync(tcpClient);
// if already faulted, re-throw any error on the calling context
if (task.IsFaulted)
await task;
}
}
// Register and handle the connection
private async Task StartHandleConnectionAsync(TcpClient tcpClient)
{
// start the new connection task
var connectionTask = HandleConnectionAsync(tcpClient);
// add it to the list of pending task
lock (_lock)
_connections.Add(connectionTask);
// catch all errors of HandleConnectionAsync
try
{
await connectionTask;
// we may be on another thread after "await"
}
catch (Exception ex)
{
// log the error
Console.WriteLine(ex.ToString());
}
finally
{
// remove pending task
lock (_lock)
_connections.Remove(connectionTask);
}
}
// Handle new connection
private async Task HandleConnectionAsync(TcpClient tcpClient)
{
await Task.Yield();
// continue asynchronously on another threads
using (var networkStream = tcpClient.GetStream())
{
var buffer = new byte[4096];
Console.WriteLine("[Server] Reading from client");
var byteCount = await networkStream.ReadAsync(buffer, 0, buffer.Length);
var request = Encoding.UTF8.GetString(buffer, 0, byteCount);
Console.WriteLine("[Server] Client wrote {0}", request);
var serverResponseBytes = Encoding.UTF8.GetBytes("Hello from server");
await networkStream.WriteAsync(serverResponseBytes, 0, serverResponseBytes.Length);
Console.WriteLine("[Server] Response has been written");
}
}
// The entry point of the console app
static async Task Main(string[] args)
{
Console.WriteLine("Hit Ctrl-C to exit.");
await new Program().StartListener();
}
}
或者,代码可能如下所示,没有await Task.Yield()。请注意,我将async lambda 传递给Task.Run,因为我仍然希望从HandleConnectionAsync 中的异步API 中受益并在其中使用await:
// Handle new connection
private static Task HandleConnectionAsync(TcpClient tcpClient)
{
return Task.Run(async () =>
{
using (var networkStream = tcpClient.GetStream())
{
var buffer = new byte[4096];
Console.WriteLine("[Server] Reading from client");
var byteCount = await networkStream.ReadAsync(buffer, 0, buffer.Length);
var request = Encoding.UTF8.GetString(buffer, 0, byteCount);
Console.WriteLine("[Server] Client wrote {0}", request);
var serverResponseBytes = Encoding.UTF8.GetBytes("Hello from server");
await networkStream.WriteAsync(serverResponseBytes, 0, serverResponseBytes.Length);
Console.WriteLine("[Server] Response has been written");
}
});
}
更新,基于评论:如果这将是库代码,则执行环境确实未知,并且可能具有非默认同步上下文。在这种情况下,我宁愿在池线程(没有任何同步上下文)上运行主服务器循环:
private static Task StartListener()
{
return Task.Run(async () =>
{
var tcpListener = TcpListener.Create(8000);
tcpListener.Start();
while (true)
{
var tcpClient = await tcpListener.AcceptTcpClientAsync();
Console.WriteLine("[Server] Client has connected");
var task = StartHandleConnectionAsync(tcpClient);
if (task.IsFaulted)
await task;
}
});
}
这样,在StartListener 中创建的所有子任务都不会受到客户端代码的同步上下文的影响。所以,我不必在任何地方显式调用Task.ConfigureAwait(false)。
2020年更新,刚刚有人在场外问了个好问题:
我想知道在这里使用锁的原因是什么?这不是
异常处理所必需的。我的理解是锁是
使用是因为 List 不是线程安全的,因此真正的问题
这就是为什么将任务添加到列表中(并在
加载)。
由于 Task.Run 完全能够跟踪它的任务
开始,我的想法是,在这个具体的例子中,锁是
没用,但是你把它放在那里,因为在一个真正的程序中,有
例如,列表中的任务允许我们迭代当前
如果程序接收到,则运行任务并干净地终止任务
来自操作系统的终止信号。
事实上,在现实生活场景中,我们几乎总是希望跟踪以Task.Run(或任何其他“正在运行”的Task 对象)开始的任务,原因如下:
- 跟踪任务异常,否则 might be silently swallowed 如果在其他地方未观察到。
- 能够异步等待所有待处理任务的完成(例如,考虑启动/停止 UI 按钮或处理启动/停止无头 Windows 服务内部的请求)。
- 为了能够控制(和限制/限制)我们允许同时运行的任务数量。
有更好的机制来处理现实生活中的并发工作流(例如,TPL 数据流库),但我确实在此处包含了任务列表和锁定,即使在这个简单的示例中也是如此。使用即发即弃的方法可能很诱人,但这几乎从来都不是一个好主意。根据我自己的经验,当我确实想要一劳永逸时,我使用了async void 方法(检查this)。