Как отправить несколько веб-запросов и обработать их как можно быстрее

#c# #parallel-processing #task #webclient

#c# #параллельная обработка #задача #webclient

Вопрос:

Я работаю над приложением C # winforms, и у меня есть около 84 URL-адресов, которые я хочу проанализировать с помощью html agility pack

для 84 записей требуется 150 секунд, чтобы выполнить задание с помощью приведенного ниже кода.

Мне было интересно, какие варианты у меня есть, чтобы ускорить его выполнение? любая помощь очень ценится!

Ниже приведена моя структура кода для выполнения этой работы

 public class URL_DATA
{
    public string URL { get; set; }
    public HtmlDocument doc  { get; set; }
}

then I call the below function to do the job

 public async Task ProcessUrls(string cookie)
 {
                var tsk = new List<Task>();
                //UrlsToProcess is List<URL_DATA>
                UrlsToProcess.ForEach(async data =>
                {
                    tsk.Add(Task.Run(async () => 
                    {
                      var htmToParse =  await ScrapUtils.GetAgilityDocby(cookie, data.URL);

                        var htmlDoc = new HtmlDocument();
                        htmlDoc.LoadHtml(htmToParse);
                        data.doc = htmlDoc;

                    }));

                });
                await Task.WhenAll(tsk).ConfigureAwait(false);    
   }
  

и, наконец, ниже приведен метод, который я использую для получения строки запроса.

  public static async Task<string> GetAgilityDocby(string cookie, string url)
        {
            using (var wc = new WebClient())
            {
                wc.Proxy = null;// WebRequest.DefaultWebProxy;// GlobalProxySelection.GetEmptyWebProxy();
                wc.Headers.Add(HttpRequestHeader.Cookie, cookie);

                wc.Headers.Add(HttpRequestHeader.UserAgent,
                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36");
                wc.Encoding = Encoding.UTF8;
                test  ;
                return await  wc.DownloadStringTaskAsync(url).ConfigureAwait(false);
            }
        }
  

Ответ №1:

Вы используете ForEach с асинхронным лямбда-выражением. У меня есть подозрение, что это заставляет ваш код выполняться последовательно, а не параллельно, поскольку каждая следующая итерация будет выполнять await.

Итак, что вы можете сделать, чтобы выяснить это наверняка:

  1. Проверьте максимальное время операции для одного URL-адреса, это время должно соответствовать скорости всего процесса (если у вас достаточно пропускной способности, памяти и процессора).
  2. Убедитесь, что ваши операции действительно выполняются параллельно. Например. путем вывода счетчика на консоль. Это не должно быть последовательным и выглядеть достаточно случайным

Вы можете изменить свой код создания задачи на этот, например, попробовать:

 var allTasks = myUrls.Select(url => Task.Run(() => {yourCode})
Task.WhenAll(allTasks);
  

Комментарии:

1. В lambda нет функции ожидания, он просто запускает там новые потоки и ожидает в конце.. Это определенно не самый элегантный способ сделать это, но он должен сработать

2. Я бы проверил это с помощью некоторого протоколирования, чтобы быть уверенным. Возможно, вы правы, и это совсем не проблема, но проверка не помешает 🙂 Знаете ли вы, что нет операции, выполнение которой занимало бы очень много времени?

3. Илья использует метод ScrapUtils. GetAgilityDocby является асинхронным, поэтому мне приходится использовать await, поэтому я пробовал и этот способ, но, похоже, для возврата строки из функции требуется 1,5-7 секунд, и цикл ожидает завершения одного запроса, чтобы начать другой

4. Вместо этого вы можете сделать .Result . Не уверен, что это правильно, но, по крайней мере, вы будете знать, является ли это параллельным

Ответ №2:

Попробуйте увеличить минимальное количество запущенных потоков на

 ThreadPool.SetMinThreads(84,84);
  

Это должно значительно ускорить процесс.

Что касается создания задачи, на которое указал Илья, я бы порекомендовал вам полностью исключить часть Task.Run / AwaitAll и использовать параллельный механизм, который был разработан именно для такого рода задач:

 Parallel.ForEach(UrlsToProcess, data =>
{
    var htmToParse =  ScrapUtils.GetAgilityDocby(cookie, data.URL);

    var htmlDoc = new HtmlDocument();
    htmlDoc.LoadHtml(htmToParse);
    data.doc = htmlDoc;
});
  

Комментарии:

1. Где именно я должен это сделать?

2. В любое время перед вызовом вашей функции. Или в startup-коде вашего приложения. Это нужно вызвать только ОДИН раз, поэтому нет необходимости помещать это в функцию ProcessUrls

3. с параллельным. В результате выполнения каждого цикла происходит немедленное завершение функции, в которой находится цикл, продолжение работы с другим кодом во время выполнения цикла в фоновом режиме. Он запускает все запросы, однако каждый запрос заканчивается 1 на 1, это в основном работает как обычный цикл for, за исключением того, что он выполняется в фоновом режиме.

4. Это странно … потому что механизм parallels определенно блокируется, смотрите Здесь (консольная программа .net): pastebin.com/1PWStQ8C не могли бы вы загрузить свой текущий исходный код, чтобы мы могли изучить?

5. Обнаружена проблема: вы не можете асинхронизировать / ожидать параллельно. Но если вы просто опустите это (отредактировали ответ), то это сработает как по волшебству.