Я бы позволил 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)
.
Обновлено в 2020 году, кто-то только что задал хороший вопрос за пределами сайта:
Мне было интересно, в чем причина использования блокировки здесь? Это не обязательно для обработки исключений. Я понимаю, что блокировка используется, потому что List не является потокобезопасным, поэтому реальный вопрос заключается в том, зачем добавлять задачи в список (и нести затраты на блокировку под нагрузкой).
Так как Task.Run отлично отслеживает запущенные задачи, я думаю, что в этом конкретном примере блокировка бесполезна, однако вы помещаете ее туда, потому что в реальной программе наличие задач в списке позволяет нам для например, повторять текущие задачи и завершать задачи без ошибок, если программа получает сигнал завершения от операционной системы.
Действительно, в реальном сценарии мы почти всегда хотим отслеживать задачи, которые мы начинаем с Task.Run
(или любых других объектов Task
, которые находятся «в полете»), по нескольким причинам:
- Для отслеживания исключений задач, которые в противном случае могут быть проглочены без уведомления, если они останутся незамеченными в другом месте.
- Чтобы иметь возможность асинхронно ожидать завершения всех ожидающих задач (например, рассмотрите возможность использования кнопки запуска/остановки пользовательского интерфейса или обработки запроса на запуск/остановку внутри безголовой службы Windows).
- Чтобы иметь возможность контролировать (и регулировать/ограничивать) количество задач, которые мы позволяем выполнять одновременно.
Существуют лучшие механизмы для обработки реальных параллельных рабочих процессов (например, библиотека потоков данных TPL), но я специально включил сюда список задач и блокировку, даже в этом простом примере. Может показаться заманчивым использовать подход «выстрелил и забыл», но это почти никогда не бывает хорошей идеей. По моему собственному опыту, когда я действительно хотел запустить и забыть, я использовал для этого методы async void
(проверьте это) .
person
noseratio
schedule
09.01.2014