Асинхронным запросам API требуется больше времени, чтобы вернуть результаты с несколькими сотнями циклов — является ли подключение к Интернету узким местом?

#c# #async-await #task

#c# #async-await #задача

Вопрос:

У меня возникли некоторые проблемы с поиском узкого места в приведенном ниже коде. Цель кода — отправить 400 запросов API асинхронно, чтобы получить данные как можно быстрее.

Проблема в том, что когда я запускаю один и тот же код синхронно, каждый запрос занимает около 3 секунд, но когда я запускаю его асинхронно, первые ответы занимают около 3 секунд, но время отклика постепенно увеличивается, а последние занимают более 20 секунд.

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

Я застрял на этой производительности или есть способ заставить эту программу работать быстрее?

 using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Http;

namespace ConsoleApplication___Alpha_Async_Download
{
    class Program
    {
        public static int iterations = 400;

        public static string[] content = new string[iterations];
        public static string[] URL = new string[iterations];

        public static int counter = 0;
        public static int[] count = new int[iterations];

        public static TimeSpan[] IndividualDownloadTime = new TimeSpan[iterations];
        public static DateTime[] IndividualDownloadTimeStampB = new DateTime[iterations];
        public static DateTime[] IndividualDownloadTimeStampA = new DateTime[iterations];

        public static HttpClient client = new HttpClient();

        static void Main(string[] args)
        {
            ServicePointManager.DefaultConnectionLimit = int.MaxValue;

            Uri uri = new Uri("URL");
            ServicePoint sp = ServicePointManager.FindServicePoint(uri);
            sp.ConnectionLimit = int.MaxValue;

            Console.WriteLine("Press ENTER to download data.");
            ConsoleKeyInfo info = Console.ReadKey();
            if (info.Key == ConsoleKey.Enter)
            {
                Console.Clear();
                Data();
            }
            Console.ReadLine();
        }

        public static void Data()
        {
            Console.WriteLine("Downloading data...");
            Console.WriteLine();

            DateTime DownloadTimeStampA = DateTime.Now;

            System.Collections.Generic.List<Task> tasks = new System.Collections.Generic.List<Task>();
            for (int a = 0; a < iterations; a  )
            {
                var local = a;
                tasks.Add(Task.Run(async () =>
                {
                    await DownloadDataAsync(local);
                }));
            }

            Task.WaitAll(tasks.ToArray());

            Console.WriteLine();
            DateTime DownloadTimeStampB = DateTime.Now;
            TimeSpan DownloadTime = DownloadTimeStampB - DownloadTimeStampA;
            Console.WriteLine("Download Time: "   DownloadTime.Minutes   ":"   DownloadTime.Seconds   ":"   DownloadTime.Milliseconds);
            Console.WriteLine();
        }

        static async Task DownloadDataAsync(int c)
        {
            counter = counter   1;
            count[c] = counter;

            char[] delimiters = new char[] { ',' };
            StreamReader URLs = new StreamReader(@"C:UsersOwnerDocumentsURL_List.csv");
            string URLs2 = URLs.ReadLine();
            string[] list = URLs2.Split(',');
            List<string> URL = new List<string>(list);

            IndividualDownloadTimeStampA[c] = DateTime.Now;

            try
            {
                content[c] = await client.GetStringAsync(URL[c]);
            }
            catch (Exception ex)
            {
                Console.WriteLine(count[c]   ". Error Message: "   ex);
            }

            Console.WriteLine();
            IndividualDownloadTimeStampB[c] = DateTime.Now;
            IndividualDownloadTime[c] = IndividualDownloadTimeStampB[c] - IndividualDownloadTimeStampA[c];
            Console.WriteLine(count[c]   "   Start: "   IndividualDownloadTimeStampA[c].ToString("HH:mm:ss")   "   Finish: "   IndividualDownloadTimeStampB[c].ToString("HH:mm:ss")   "   Duration: "   IndividualDownloadTime[c].Minutes   ":"   IndividualDownloadTime[c].Seconds   ":"   IndividualDownloadTime[c].Milliseconds);
        }
    }
}
  

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

1. Вы перенасыщаете ThreadPool , выполняя 400 задач одновременно. Увеличенная задержка связана с нехваткой доступных потоков в пуле.

2. Я запустил программу, используя » postman-echo.com/delay/3 «в качестве URL-адреса-заполнителя и программа запустилась за 6,5 секунд, даже со встроенной задержкой в 3 секунды. Поэтому я не уверен, почему другой API занимает намного больше времени. Я связался с другим поставщиком API, и они сказали, что их задержка составляет микросекунды, поэтому я не думаю, что это проблема и с их стороны. Я предполагаю, что это связано с тем фактом, что API-заполнитель не передает столько данных. Но что я могу сделать, чтобы ускорить это?

3. Удалите Task.Run s. DownloadAsync уже является задачей — даже задачей на основе ввода-вывода — вам не нужно включать потоки, используемые Task.Run.

4. Спасибо за предложение, я внес изменения, но, к сожалению, улучшения производительности не произошло. Это чище, хотя и без Task.Run.

5. Возможно, мне нужно обновить свой процессор? У меня четырехъядерный процессор с гиперпоточностью. Увеличит ли скорость 16-ядерного процессора с hyper-threading в четыре раза?

Ответ №1:

Как отмечали другие, проблема связана с Task.Run , что не требуется в коде, привязанном к вводу-выводу. Заменить:

 var local = a;
tasks.Add(Task.Run(async () =>
{
  await DownloadDataAsync(local);
}));
  

с:

 tasks.Add(DownloadDataAsync(a));
  

Кроме того, в DownloadDataAsync есть много странного поведения; похоже, что он читает CSV-файл один раз за запрос. Было бы лучше просто загрузить его один раз и передавать определенный URL для каждого вызова DownloadDataAsync . Также существует некоторое копирование в / из массивов и списков, которое не вызывает этой проблемы, но неэффективно и ненужно.

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

1. Отличное предложение, я внес первое изменение, но, к сожалению, улучшения производительности не произошло. Для запуска все еще требовалось около 28 секунд. Я обязательно внесу предложение и относительно CSV-файла.

Ответ №2:

Во-первых, позвольте мне сказать, что ваша конфигурация для ServicePointManager.DefaultConnectionLimit слишком агрессивен, я понимаю, вы пытались это сделать, чтобы повысить производительность, но вы должны установить разумное ограничение для этого параметра, иначе вы столкнетесь с другими потенциальными проблемами в стеке tcp / ip.

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

ThreadPool.SetMinThreads(100, 100); ThreadPool.SetMaxThreads(200, 200);

Эти настройки приведут к тому, что ThreadPool позволит быстрее создавать больше потоков и будет иметь больше портов асинхронного завершения, ограничивая также количество максимальных потоков, которые будут пытаться выполнять параллельную работу в любое время, прямо сейчас вы ограничиваетесь, и именно поэтому вы не получаете лучшей производительности, в частности, из-за настройки maxThreads, вот как объясняется в .Сетевая документация:

ThreadPool.Метод SetMaxThreads(Int32, Int32)

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

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

1. Спасибо за предложение, к сожалению, хотя улучшения производительности не произошло. Я последовал вашему совету и опустил ServicePointManager. DefaultConnectionLimit равным 40. Спасибо за совет!

2. если увеличение числа одновременно выполняющих работу потоков не помогает, скорее всего, проблема не в клиенте, а на стороне сервера, вероятно, там существует ограничение на количество одновременных вызовов с одного IP, это типичный сценарий для служб по предотвращению DoS-атак, ограничениями могут быть количество одновременных подключений или пропускная способность на IP-адрес. проверьте это.