Ответ 1
Я позволил бы Framework управлять потоками и не создавал бы никаких дополнительных потоков, если тесты профилирования не показывают, что мне это может понадобиться. Особенно, если вызовы внутри HandleConnectionAsync
в основном связаны с вводом-выводом.
В любом случае, если вы хотите освободить вызывающий поток (диспетчер) в начале HandleConnectionAsync
, есть очень простое решение. Вы можете перейти на новый поток из ThreadPool
с помощью await Yield()
. Это работает, если ваш сервер работает в среде выполнения, в которой не установлен какой-либо контекст синхронизации в начальном потоке (консольное приложение, Служба WCF), которая обычно используется для TCP-сервера.
Следующие примеры иллюстрируют это (код взят из здесь). Обратите внимание, что основной цикл 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
в Task.Run
, потому что я все еще хочу воспользоваться асинхронными API-интерфейсами внутри HandleConnectionAsync
и использовать там 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)
.