none
c# .net 4.5 - медленная работа нескольких потоков RRS feed

  • Вопрос

  • Добрый день!

    Подскажите, как грамотно реализовать многопоточное приложение?

    В приложении получается циклично информация с веб страницы при помощи WebClient.

    При работе в одном потоке время получения информации около 50мс.

    В нескольких потоках время ответа увеличивается до 400-500 (при 8 потоках).

    Но в то же время, если запускать несколько независимых приложений - скорость так и остается в пределах 50 мс в каждом.

    Пробовал использовать Thread, Task, а так же асинхронное получение даннх со страницы (DownloadStringAsync) - результат тот же.

    Как добиться такой же производительности в многопоточном приложении как и в нескольких независимых приложениях?

    5 сентября 2017 г. 8:36

Ответы

  • Учитывая важные замечания коллег попробуйте запустить более модифицированный код:

    class Program
        {
            static readonly int parTasksCount = 4;
    
            static readonly CountdownEvent CountdownEvent = new CountdownEvent(parTasksCount);
    
            static void Main(string[] args)
            {
                var sp = ServicePointManager.FindServicePoint(new Uri("http://www.xxx.xxx"));
                sp.ConnectionLimit = 4;
    
                var list = new List<Info>();
    
                for (int i = 0; i < parTasksCount; i++)
                {
                    var output = new Info {ThreadId = 0, Items = new List<long>()};
                    list.Add(output);
    
                    var thread = new Thread(GetData);
                    thread.Start(output);
                }
    
                CountdownEvent.Wait();
    
                foreach (var item in list)
                {
                    long sum = 0;
                    foreach (var elapsed in item.Items)
                    {
                        sum += elapsed;
                        Console.WriteLine($"Thread {item.ThreadId} {elapsed} ms");
                    }
    
                    Console.WriteLine($"Average Time: {sum/ item.Items.Count}");
                }
    
                Console.ReadKey();
            }
    
            static void GetData(object output)
            {
                var @out = output as Info;
    
                var client = new WebClient();
                var sw = new Stopwatch();
    
                int r = 8;
                while (r > 0)
                {
                    r--;
                    sw.Start();
                    client.DownloadString("http://www.xxx.xxx");
                    sw.Stop();
                    //Console.WriteLine("Thread " + count + " \t" + sw.ElapsedMilliseconds.ToString() + " ms");
                    @out.Items.Add((int)sw.ElapsedMilliseconds);
                    @out.ThreadId = Thread.CurrentThread.ManagedThreadId;
    
                    sw.Reset();
                }
    
                CountdownEvent.Signal();
            }
    
            private class Info
            {
                public int ThreadId { get; set; }
    
                public List<long> Items { get; set; }
            }
        }
    Не думаю, что результат должен отличаться. Можете поэкспериментировать с parTasksCount и ConnectionLimit, потом отпишитесь.


    Сделаем содержимое сообщества лучше, вместе!

    • Помечено в качестве ответа iljakokorev 5 сентября 2017 г. 17:45
    5 сентября 2017 г. 11:59
    Модератор

Все ответы

  • А вам как нужно? Одну и ту же страницу в несколько потоков? Если да, то много поточность тут не поможет, так как задача не распараллеливается. Если разные страницы, то тут уже можно параллельно запустить несколько экземпляров WebClient в разных потоках.

    Сделаем содержимое сообщества лучше, вместе!

    5 сентября 2017 г. 8:52
    Модератор
  • Так же смотрим тут:

    https://docs.microsoft.com/en-us/dotnet/framework/network-programming/managing-connections


    This posting is provided "AS IS" with no warranties, and confers no rights.

    5 сентября 2017 г. 9:01
    Модератор
  • Благодарю за ответ!

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

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

    5 сентября 2017 г. 9:09
  • У меня (упрощенно) такой код:

    static int n;
            static void Main(string[] args)
            {
                n = 1;
                for (int i = 0; i < 8; i++)
                {
                    Task t = new Task(GetData);
                    t.Start();
                }
                Console.ReadKey();
            }
    
            static void GetData()
            {
                int count = n;
                n++;
                WebClient client = new WebClient();
                System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
                while (true)
                {
                    sw.Start();
                    client.DownloadString("http://example.com?param=" + count.ToString());
                    sw.Stop();
                    Console.WriteLine("Thread " + count + " \t" + sw.ElapsedMilliseconds.ToString() + " ms");
                    sw.Reset();
                }
            }

    Так вот, в нем получение в каждом потоке медленнее, чем восемь запущенных однопоточных приложений в сумме.

    5 сентября 2017 г. 9:21
  • Это вполне ожидаемо. Создание задачи требует определенных ресурсов. На выгоду при распараллеиливании загрузки файлов, занимающей 50 мс, надеятся не приходится.

    К тому же, перед измерением производительности, следует убедиться что программа корректно работает. Вы используете одну и ту же переменную (n) из нескольких потоков без всякой синхронизации, что не правильно (вы проверяли, что программа в результате скачивает?)

    5 сентября 2017 г. 9:37
  • Это вполне ожидаемо. Создание задачи требует определенных ресурсов. На выгоду при распараллеиливании загрузки файлов, занимающей 50 мс, надеятся не приходится.

    К тому же, перед измерением производительности, следует убедиться что программа корректно работает. Вы используете одну и ту же переменную (n) из нескольких потоков без всякой синхронизации, что не правильно (вы проверяли, что программа в результате скачивает?)

    Даже если сделать 8 отдельных методов без обращения к одной переменной (хотя обращение к переменной только в начале, разве оно влияет на производительность?), производительность падает так, что такое чуство, что операции выполняются последовательно.

    И все же вопрос - почему не стоит рассчитывать на выгоду при распараллеливании загрузки занимающей 50 мс? ОС как то же работает "с выгодой"? Несколько однотипных копий приложения (грузящих разные страницы) не замедляют работу друг друга.

    5 сентября 2017 г. 11:39
  • Учитывая важные замечания коллег попробуйте запустить более модифицированный код:

    class Program
        {
            static readonly int parTasksCount = 4;
    
            static readonly CountdownEvent CountdownEvent = new CountdownEvent(parTasksCount);
    
            static void Main(string[] args)
            {
                var sp = ServicePointManager.FindServicePoint(new Uri("http://www.xxx.xxx"));
                sp.ConnectionLimit = 4;
    
                var list = new List<Info>();
    
                for (int i = 0; i < parTasksCount; i++)
                {
                    var output = new Info {ThreadId = 0, Items = new List<long>()};
                    list.Add(output);
    
                    var thread = new Thread(GetData);
                    thread.Start(output);
                }
    
                CountdownEvent.Wait();
    
                foreach (var item in list)
                {
                    long sum = 0;
                    foreach (var elapsed in item.Items)
                    {
                        sum += elapsed;
                        Console.WriteLine($"Thread {item.ThreadId} {elapsed} ms");
                    }
    
                    Console.WriteLine($"Average Time: {sum/ item.Items.Count}");
                }
    
                Console.ReadKey();
            }
    
            static void GetData(object output)
            {
                var @out = output as Info;
    
                var client = new WebClient();
                var sw = new Stopwatch();
    
                int r = 8;
                while (r > 0)
                {
                    r--;
                    sw.Start();
                    client.DownloadString("http://www.xxx.xxx");
                    sw.Stop();
                    //Console.WriteLine("Thread " + count + " \t" + sw.ElapsedMilliseconds.ToString() + " ms");
                    @out.Items.Add((int)sw.ElapsedMilliseconds);
                    @out.ThreadId = Thread.CurrentThread.ManagedThreadId;
    
                    sw.Reset();
                }
    
                CountdownEvent.Signal();
            }
    
            private class Info
            {
                public int ThreadId { get; set; }
    
                public List<long> Items { get; set; }
            }
        }
    Не думаю, что результат должен отличаться. Можете поэкспериментировать с parTasksCount и ConnectionLimit, потом отпишитесь.


    Сделаем содержимое сообщества лучше, вместе!

    • Помечено в качестве ответа iljakokorev 5 сентября 2017 г. 17:45
    5 сентября 2017 г. 11:59
    Модератор
  • Огромное списибо!

    Так задержек нет.

    Могли бы Вы еще сказать, что за код:

    var sp = ServicePointManager.FindServicePoint(new Uri("http://www.xxx.xxx"));
                sp.ConnectionLimit = 4;

    Т.е. я понял что он влияет на скорость работы, но не совсем понимаю как.

    Или получается, что даже при работе обычного WebClient имеется "встроенное правило" не открывать более одного коннекта, и из за этого я получаю задержки (в моем примере)?

    5 сентября 2017 г. 14:57
  • "Даже если сделать 8 отдельных методов без обращения к одной переменной (хотя обращение к переменной только в начале, разве оно влияет на производительность?), производительность падает так, что такое чуство, что операции выполняются последовательно."

    Речь не о том, что это влияет на производительность, а о том, что перед измерением производительности нужно убедиться в корректности кода (да и решаемой задачи). Что вы пытаетесь выяснить? Влияние распараллеливания на скорость загрузки файлов? Для этого надо мерить суммарное время, а не время загрузки одного файла, иначе эксперимент не имеет смысла.

    Время загрузки одного файла при параллельной загрузке, определенно, может быть больше, чем при последовательной, в определенных ситуациях. Во-первых, параллельность не приходит бесплатно. Если потоков больше, чем ядер, на переключение контекстов потоков расходуется процессорное время. Во вторых, при параллельной загрузке, во время скачивания одного файла может вклиниваться другой поток со своим файлом, и в итоге время на скачивание одного файла становится больше (хотя общее время на скачивание группы файлов может быть и меньше). При параллельно загрузке как бы теряется понятие "время на скачивание одного файла", время становится "разделяемым".

    "И все же вопрос - почему не стоит рассчитывать на выгоду при распараллеливании загрузки занимающей 50 мс?"

    Потому что 50 мс - слишком малое время, чтобы заметить выигрыш. Тупо погрешность измерения может перебивать результат. Распаралеливать имеет смысл что-то, что длится заметное время, нет?

    Говоря о выигрыше при многопоточности, следует понимать одну вещь: параллельность полезна, если потоки не конкурируют за общие ресурсы. Бессмысленно гонять 100 потоков на двухъядерном процессоре - на переключение контекстов потеряем больше, чем на выполнение самих задач. Этот же принцип можно распространить и на сеть - если пропускная способность своего канала является лимитирующим фактором, распараллеливание не даст никакого выигрыша; если нет - может и даст. Для исследования можно попробовать вот такую программу:

    	static string[] urls = new string[] {                         
                "http://example.ru/file1.zip",
                "http://example.ru/file2.zip",
                "http://example.ru/file3.zip"
            };
    
            static int n;
            static object sync = new object();
            static System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    
            static void Main(string[] args)
            {
                WebClient client = new WebClient();
                using (client)
                {
                    //предварительно выполним длительную операцию,
                    //чтобы исключить влияние инициализации CLR на последующие вычисления...
                    byte[] data = client.DownloadData("http://example.com/file.png");
                }
    
                n = 0;
                Console.WriteLine(" *** Последовательная загрузка ***");
                sw.Start();
    
                //последовательно загрузим три файла...
                GetData();
                GetData();
                GetData();
    
                n = 0;
                Console.WriteLine();
                Console.WriteLine(" *** Параллельная загрузка ***");
                sw.Reset();
                sw.Start();
                
                //запустим три параллельных потока на загрузку тех же файлов...
                for (int i = 0; i < 3; i++)
                {
                    Task t = new Task(GetData);
                    t.Start();
                }
                Console.ReadKey();
            }
    
            static void GetData()
            {
                int count;
    
                lock (sync)
                {
                    count = n;
                    n++;
                }
    
                WebClient client = new WebClient();
    
                Console.WriteLine("Downloading " + count + ", t=" + sw.ElapsedMilliseconds.ToString() + " ms");
                byte[] data = client.DownloadData(urls[count]);
                Console.WriteLine("Downloaded " + count + ", t=" + sw.ElapsedMilliseconds.ToString() + " ms");
                
            }

    Я сам сейчас не могу получить вменяемые результаты, так как у WiFi-соединения скорость слишком нестабильная. Но предварительно: при загрузке файлов около 20 КБ выигрыша не наблюдается, около мегабайта - небольшой выигрыш есть.


    • Изменено VadimTagil 5 сентября 2017 г. 17:18
    5 сентября 2017 г. 17:14
  • "Могли бы Вы еще сказать, что за код:" - это код из статьи которую указал Ilya Tumanov, основная загвоздка была в нем. Видимо вы не читали статью по ссылке :)

    Сделаем содержимое сообщества лучше, вместе!

    5 сентября 2017 г. 17:25
    Модератор