Parallel.For раскручивает неожиданное количество потоков - PullRequest
0 голосов
/ 29 мая 2019

Рассмотрим следующий код:

var options = new ParallelOptions();
var urls = GetListOfUrls();

Parallel.ForEach(urls, options, url => {
    try {
        using (HttpClient client = new HttpClient()) {
            client.Timeout = TimeSpan.FromMinutes(30);
            Task task = client.GetAsync(url);
            task.Wait();
        }
    } catch (Exception exception) {
        Console.WriteLine(exception.Message);
    }

});

Он раскручивает ~ 670 потоков на виртуальной машине с 8 ядрами.Это нормально?Насколько я понимаю, эмпирическое правило для TPL было 25 потоков на ядро, что поместило бы его в диапазон 200 потоков.

PS В приведенном ниже коде GetListOfUrls() возвращает миллион URL-адресов.

enter image description here

enter image description here

Ответы [ 2 ]

1 голос
/ 29 мая 2019

Ваш пример кода написан не с учетом асинхронности.Вам не нужно Parallel.ForEach вообще.HttpClient.GetAsync уже асинхронен, и нет смысла его оборачивать в задачи, связанные с процессором .

private readonly _httpClient = new HttpClient();

var tasks = new List<Task>();
foreach(var url in urls)
{
    var task = DoWork(url);
    tasks.Add(task);
}
await Task.WhenAll(tasks);

foreach(var task in tasks)
{
  if (task.Exception != null)
    Console.WriteLine(task.Exception.Message);
}

public async Task DoWork(string url)
{                
  var json = await _httpClient.GetAsync(url);
  // do something with json
}

Хотя Parallel.ForEach() является более эффективной версией цикла и использует Task.Run() этодействительно должен использоваться только для Связанного с процессором работы (Task.Run Etiquette and Proper Usage) .Вызов URL-адреса не связан с работой ЦП, это работа ввода-вывода (или более технически называемая Работа порта завершения ввода-вывода ).

ВЫ ИСПОЛЬЗУЕТЕ HTTPCLIIДЕСТАБИЛИЗИРУЕТ ВАШЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ

Спасибо за ссылку HttpClient.Вот это да.Исправление кажется таким анти-паттерном.

Хотя это может показаться анти-паттерном, это потому, что предоставленное решение на самом деле является анти-паттерном.HttpClient использует внешние ресурсы для выполнения своей работы, поэтому он должен располагаться (он реализует IDisposable), но в то же время он должен использоваться как одиночный.Это создает проблему, потому что не существует чистого способа избавиться от синглтона, используемого в качестве статического свойства / поля в классе.Однако, поскольку он был написан mirosoft, мы не должны беспокоиться о том, что нам всегда нужно распоряжаться объектами, которые мы создаем, если в документации указано иное.

Поскольку вы указываете, чтони Parallel.ForEach, ни Task.Run не подходят для работы HttpClient, потому что он связан с вводом / выводом, что бы вы порекомендовали?

Async / Await

Я добавил миллионную часть к вопросу

Так что вам нужно ограничить количество параллельных задачитак:

var maximumNumberofParallelOperations = 1;
foreach(var url in urls)
{
    var task = DoWork(url);
    tasks.Add(task);
    while (allTasks.Count(t => !t.IsCompleted) >= maximumNumberofParallelOperations )
    {
      await Task.WhenAny(allTasks);
    }
}
0 голосов
/ 29 мая 2019

Параллель здесь избыточна, поскольку методы HttpClient асинхронны.Я бы предложил называть их асинхронно.В противном случае вы создадите большое количество потоков, которые ничего не делают, кроме ожидания.

Кроме того, используйте один экземпляр HttpClient.Это увеличит число обращений к кешу и уменьшит потребность HttpClient в ускорении потоков для доступа к информации прокси или DNS.

var client = new HttpClient();
var tasks = urls.Select( url => client.GetAsync(url) ).ToList();
await Task.WhenAll(tasks);
var results = tasks.Select( task => task.Result ).ToList();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...