Фон
У меня есть код, который выполняет пакетную обработку HTML-страницы с использованием контента с одного конкретного хоста. Он пытается сделать большое количество (~ 400) одновременных HTTP-запросов, используя HttpClient
. Я считаю, что максимальное количество одновременных подключений ограничено ServicePointManager.DefaultConnectionLimit
, поэтому я не применяю свои собственные ограничения параллелизма.
После асинхронной отправки всех запросов на HttpClient
с помощью Task.WhenAll
вся пакетная операция может быть отменена с помощью CancellationTokenSource
и CancellationToken
. Ход операции можно просмотреть через пользовательский интерфейс, и можно нажать кнопку, чтобы выполнить отмену.
Проблема
Вызов CancellationTokenSource.Cancel()
блокируется примерно на 5–30 секунд. Это приводит к зависанию пользовательского интерфейса. Есть подозрение, что это происходит из-за того, что метод вызывает код, зарегистрированный для уведомления об отмене.
Что я рассмотрел
- Ограничение количества одновременных задач HTTP-запросов. Я считаю это обходным путем, потому что
HttpClient
уже сам ставит в очередь лишние запросы. - Выполнение вызова метода
CancellationTokenSource.Cancel()
в потоке, отличном от пользовательского интерфейса. Это не сработало; задача фактически не выполнялась, пока не завершилось большинство других. Я думаю, чтоasync
версия метода подойдет, но я не смог ее найти. Кроме того, у меня сложилось впечатление, что метод подходит для использования в потоке пользовательского интерфейса.
Демонстрация
Код
class Program
{
private const int desiredNumberOfConnections = 418;
static void Main(string[] args)
{
ManyHttpRequestsTest().Wait();
Console.WriteLine("Finished.");
Console.ReadKey();
}
private static async Task ManyHttpRequestsTest()
{
using (var client = new HttpClient())
using (var cancellationTokenSource = new CancellationTokenSource())
{
var requestsCompleted = 0;
using (var allRequestsStarted = new CountdownEvent(desiredNumberOfConnections))
{
Action reportRequestStarted = () => allRequestsStarted.Signal();
Action reportRequestCompleted = () => Interlocked.Increment(ref requestsCompleted);
Func<int, Task> getHttpResponse = index => GetHttpResponse(client, cancellationTokenSource.Token, reportRequestStarted, reportRequestCompleted);
var httpRequestTasks = Enumerable.Range(0, desiredNumberOfConnections).Select(getHttpResponse);
Console.WriteLine("HTTP requests batch being initiated");
var httpRequestsTask = Task.WhenAll(httpRequestTasks);
Console.WriteLine("Starting {0} requests (simultaneous connection limit of {1})", desiredNumberOfConnections, ServicePointManager.DefaultConnectionLimit);
allRequestsStarted.Wait();
Cancel(cancellationTokenSource);
await WaitForRequestsToFinish(httpRequestsTask);
}
Console.WriteLine("{0} HTTP requests were completed", requestsCompleted);
}
}
private static void Cancel(CancellationTokenSource cancellationTokenSource)
{
Console.Write("Cancelling...");
var stopwatch = Stopwatch.StartNew();
cancellationTokenSource.Cancel();
stopwatch.Stop();
Console.WriteLine("took {0} seconds", stopwatch.Elapsed.TotalSeconds);
}
private static async Task WaitForRequestsToFinish(Task httpRequestsTask)
{
Console.WriteLine("Waiting for HTTP requests to finish");
try
{
await httpRequestsTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("HTTP requests were cancelled");
}
}
private static async Task GetHttpResponse(HttpClient client, CancellationToken cancellationToken, Action reportStarted, Action reportFinished)
{
var getResponse = client.GetAsync("http://www.google.com", cancellationToken);
reportStarted();
using (var response = await getResponse)
response.EnsureSuccessStatusCode();
reportFinished();
}
}
Выход
Почему отмена блокируется так долго? Кроме того, есть ли что-то, что я делаю неправильно или можно было бы сделать лучше?