Многопоточность на платформе .NET. Обзор средств

Страница создана Камила Пугачева
 
ПРОДОЛЖИТЬ ЧТЕНИЕ
ВЕСТНИК ПЕРМСКОГО УНИВЕРСИТЕТА
2020                     Математика. Механика. Информатика                            Вып. 2(49)

УДК 004.451.45

Многопоточность на платформе .NET.
Обзор средств
М. В. Шимановская, И. А. Муфтеев, Е. И. Илларионова
Пермский государственный аграрно-технологический университет
Россия, 614990, г. Пермь, ул. Петропавловская, 23
masa-81@mail.ru; 8(342) 2-179-949
       Рассмотрены способы применения параллелизма на платформе .NET на языке программи-
       рования C#. Приведен сравнительный анализ подходов к распределенным вычислениям.
       Представлены числовые показатели по времени, между каждым из способов, а также диа-
       граммы для наглядного представления.
       Ключевые слова: многопоточность; C#; Thread; ThreadPool; Task, PLINQ; Parallel; For;
       Parallel; ForEach.
DOI: 10.17072/1993-0550-2020-2-69-75

   Введение                                         ‒ Прирост в производительности вычисле-
                                                     ний;
      Традиционно программное обеспечение            ‒ Отзывчивость пользовательского интер-
писалось для последовательных вычислений,            фейса (во время выполнения фоновых задач);
алгоритмы реализовывались в виде последо-            ‒ Горизонтальное масштабирование, поз-
вательного потока инструкций, которые вы-            воляющее улучшать производительность за
полняются на центральном процессоре.                 счет применения нескольких ядер процессора.
      Идея параллельных вычислений не нова                 Рассмотрим возможности встроенных
и зародилась в 1960-х гг., ее основателем был        классов и интерфейсов по организации мно-
Эдгар Дейкстрой. Однако реализация таких             гопоточных приложений на языке C#. На
вычислений была осложнена физической реа-            каждый способ будет приводиться пример,
лизацией исполняющего устройства, который            который производит определенные действия
владел только одним потоком управления в             над массивом значений.
один момент времени.                                       Наипростейший пример задействования
      Развитие центральных процессоров в             параллельного вычисления – возведение мас-
2000-х годах шагнуло на ступень эволюции,            сива в степень. Последовательный метод бу-
которая определила ветвь развития в сторону          дет проходить по каждому элементу и выпол-
увеличения процессорных ядер. Сейчас про-            нять действие (листинг 1).
граммисты имеют практически неограничен-
ный потенциал для разработки программного            Листинг 1. Последовательный доступ к массиву
обеспечения с использованием нескольких                1:   var arr = new int[n];
вычислительных ресурсов для решения зада-              2:   for (int i = 0; i < n; i++)
чи, однако до сих пор у большинства появля-            3:   {
                                                       4:       arr[i] = (int)Math.Pow(arr[i],5);
ются трудности с определением параллельно-             5:   }
го алгоритма.
      Эффективное распараллеливание про-                   Такую процедуру возведения массива в
грамм на нескольких вычислительных ресур-            степень довольно просто подвергнуть разде-
сах дает:                                            лению на подзадачи, которые будут занимать-
                                                     ся своею независимой частью.
© Шимановская М. В., Муфтеев И. А., Илларио-
нова Е. И., 2020                                        Класс Thread

                                                69
М. В. Шимановская, И. А. Муфтеев, Е. И. Илларионова

      Класс Thread является самым элемен-             1:   var threads = new
тарным из всех типов пространства имен                     Thread[threadCounts];
                                                      2:   var steps = new Step[threadCounts];
System.Threading. Этот тип определяет набор           3:   for (int i = 0; i < threadCounts;i++)
методов, которые позволяют создавать новые            4:   {
потоки внутри текущего контейнера прило-              5:       steps[i] = new Step(arr, i,
жения, а также приостанавливать, останавли-                threadCounts);
вать и уничтожать определенный поток. Ос-             6:       threads[i] = new Thread(new
                                                           ThreadStart(steps[i].Solve));
новным применением этого метода является              7:   }
запуск фоновых потоков.
      Для того чтобы применить класс Thread,           Класс ThreadPool
необходимо для него заранее подготовить
«почву», а именно разделить массив на такое               Создание потоков требует времени. Ес-
количество частей, чтобы каждая часть была          ли есть различные короткие задачи, подлежа-
доступна отдельному потоку. Количество по-          щие выполнению, можно создать набор пото-
токов рекомендуется создавать в таком коли-         ков заранее и затем просто отправлять соот-
честве, сколько доступно ядер процессора,           ветствующие запросы, когда наступает оче-
если используется технология Hyper-threading        редь для их выполнения. Было бы неплохо,
то количество исполняющих устройств в два           если бы количество этих потоков автоматиче-
раза больше.                                        ски увеличивалось с ростом потребности в
      Для разделения массива создадим класс         потоках и уменьшалось при возникновении
Step, который на каждую часть массива бу-           потребности в освобождении ресурсов.
дет иметь начальный индекс и конечный (ли-                Создавать подобный список потоков
стинг 2).                                           самостоятельно не понадобится. Для
                                                    управления таким списком предусмотрен
            Листинг 2. Класс Step                   класс ThreadPool, который по мере необхо-
                                                    димости уменьшает и увеличивает количе-
  1:   class Step                                   ство потоков в пуле до максимально допу-
  2:   {
                                                    стимого. Значение максимально допусти-
  3:     public int[] Arr { get; set; }
  4:     private int From { get; set; }             мого количества потоков в пуле может из-
  5:     private int To { get; set; }               меняться. В случае двуядерного ЦП оно по
  6:     public Step(int[] arr, int index,          умолчанию составляет 1023 рабочих пото-
       int threadCount)                             ков и 1000 потоков ввода-вывода.
  7:     {                                                Можно указывать минимальное количе-
  8:       Arr = arr;
  9:       var rd = (int)((double)arr.Length        ство потоков, которые должны запускаться сра-
       / threadCount);                              зу после создания пула, и максимальное коли-
 10:       From = index * rd;                       чество потоков, доступных в пуле. Если оста-
 11:       To = index != (threadCount - 1) ?        лись какие-то подлежащие обработке задания, а
       (index + 1) * rd-1 : Arr.Length-1;           максимальное количество потоков в пуле уже
 12:     }
 13:     public void Solve()                        достигнуто, то более новые задания будут по-
 14:     {                                          мещаться в очередь и там ожидать, пока какой-
 15:       for (int i = From; i
Многопоточность на платформе .NET. Обзор средств

          Листинг 4. Класс ThreadPool                    т. е. выполнение только по завершении пере-
   1:   using var mrEvent = new                          числения запроса. Основное различие состоит
        ManualResetEvent(false);                         в том, что PLINQ пытается задействовать сра-
   2:   for (int i = 0; i < processors; i++)             зу все процессоры в системе. Для этого разби-
   3:     ThreadPool.QueueUserWorkItem(                  вается источник данных на сегменты, а затем
   4:       new WaitCallback(x =>
   5:       {
                                                         запрашивается каждый сегмент в отдельном
   6:         var step = x as Step;                      рабочем потоке сразу, используя сразу не-
   7:         step.Solve();                              сколько процессоров. Во многих случаях па-
   8:         if (Interlocked.Decrement(ref              раллельное выполнение значительно сокра-
        pool) == 0)                                      щает время выполнения запроса.
   9:           mrEvent.Set();
  10:       }), steps[i]);
                                                                Благодаря параллельному выполнению
  11:   mrEvent.WaitOne();                               PLINQ позволяет повысить производитель-
                                                         ность некоторых видов запросов по сравне-
   Класс Task                                            нию с устаревшим кодом. Часто для этого до-
                                                         статочно добавить к источнику данных опера-
      В основу TPL (Task Parallel Library) поло-         тор запроса AsParallel (листинг 6). Тем не ме-
жен класс Task. Элементарная единица исполне-            нее, параллелизм может представлять свои
ния инкапсулируется в TPL средствами класса              собственные сложности, и не все операции
Task, а не Thread. Класс Task отличается от клас-        запросов в PLINQ выполняются быстрее. Не-
са Thread тем, что он является абстракцией,              которые запросы при применении паралле-
представляющей "умную" асинхронную опера-                лизма только замедляются. В связи с этим
цию. Массив таких задач в пуле одного прило-             необходимо понимать, как влияют на парал-
жения может исполняться как синхронно на од-             лельные запросы такие аспекты, как упорядо-
ном ядре, так и в режиме многопоточности, в              чение.
зависимости от нагрузки, а в классе Thread ин-
капсулируется поток исполнения.                                     Листинг 6. Запрос PLINQ
      Кроме того, исполнением задач управ-                 1:   var arr = new int[n];
ляет планировщик задач, который работает с                 2:   arr = arr
пулом потоков. Это, например, означает, что                3:     .AsParallel()
                                                           4:     .AsOrdered()
несколько задач могут разделять один и тот                 5:     .Select(x => (int)Math.Pow(x, 5))
же поток. Класс Task (и вся остальная биб-                 6:     .ToArray();
лиотека TPL) определены в пространстве                     7:   arr = arr.Select(x => x).ToArray();
имен System.Threading.Tasks применение это-
го подхода к нашей задаче см. в листинге 5.                Методы класса Parallel
             Листинг 5. Класс Task                             Методы класса Parallel – For и ForEach
      1:var threadCounts =                               являются отличными кандидатами для парал-
    Environment.ProcessorCount;                          лелизации мелких однотипных задач над од-
      2:var arr = new int[n];
      3:var tasks = new Task[threadCounts];
                                                         ной коллекцией. Библиотека TPL предостав-
      4:var steps = new Step[threadCounts];              ляет поддержку распараллеливания циклов
      5:for (int i = 0; i < threadCounts;i++)            посредством явных методов, очень близких
      6: steps[i] = new Step(arr, i,                     своим языковым эквивалентам. Данные мето-
    threadCounts);                                       ды максимально близко имитируют поведе-
      7:for (int i = 0; i < threadCounts;i++)
      8: tasks[i]=Task.Run(steps[i].Solve);
                                                         ние и синтаксис циклов for и foreach.
                                                               Метод Parallel.For() используется только
     9: Task.WaitAll(tasks);
                                                         для сложных циклов, где необходимо органи-
                                                         зовывать прерывания (листинг 7).
   PLINQ
                                                                Листинг 7. Метод Parallel.For()
     Parallel LINQ (PLINQ) – параллельная
                                                           1:   var arr = new int[n];
реализация шаблона LINQ. Запрос PLINQ во                   2:   Parallel.For(0, n, i => arr[i] =
многом напоминает непараллельный запрос                         (int)Math.Pow(arr[i], 5));
LINQ to Objects. Запросы PLINQ, как и после-
довательные запросы LINQ, работают с лю-                      Метод Parallel.ForEach() применим для
бым источником данных IEnumerable в памя-                перебора коллекций из ссылочных элементов,
ти и поддерживают отложенное выполнение,

                                                    71
М. В. Шимановская, И. А. Муфтеев, Е. И. Илларионова

то есть объектов, реализующих какой-либо                 Таблица 1. Занимаемое время обработки (c)
класс (листинг 8).                                               Эл-ов
                                                       Метод               1 млн      10 млн     100 млн
       Листинг 8. Метод Parallel.ForEach()
  1:   var arr = Enumerable                            Sequence            0,00061    0,00604    0,05946
  2:     .Range(0, n)                                  For                 0,00013    0,00129    0,01349
  3:     .Select(x => new Number { X = … })            ForEach             0,00015    0,00128    0,01331
  4:     .ToArray();
  5:   Parallel.ForEach(arr, x => x.X =                PLINQ               0,00013    0,00108    0,01020
       (int)Math.Pow(x.X, 5));                         ThreadPool          0,00012    0,00103    0,01024
                                                       Thread              0,00035    0,00151    0,01052
       Метод Parallel.ForEach() отличается от          Task                0,00011    0,00104    0,01020
Parallel.For() тем, что входной параметр –
ссылка на коллекцию IEnumerable а не число-
вой промежуток, вследствие чего функция
обратного вызова принимает не индекс обра-
батываемого элемента, а сам элемент, поэто-
му, если элемент массива структурный,
например int, то в области видимости обрат-
ного вызова с типом делегата Action бессмыс-
ленно присвоение элементу нового значения.
       Из вышесказанного следует, что для
демонстрации работы этого метода необхо-
димо обернуть каждый элемент массива в
                                                                       Рис. 1. Время обработки
класс с единственным полем – элементом
массива. Для наглядной демонстрации того
                                                            В табл. 2 представлены показатели, отоб-
или иного метода произведены измерения
                                                      ражающие зависимость объема занимаемой
быстродействия каждого метода относительно
                                                      оперативной памяти в мегабайтах, включая
количества элементов. В качестве аппаратуры
                                                      данные (массив) и накладные расходы для со-
взяты два компьютера, с процессорами Intel
                                                      здания потоков от количества элементов в мас-
Core i5 двух поколений – 3-го и 8-го поколения
                                                      сиве.
c тактовой частотой 2,5–3,1 ГГц и 2,8–4,0 ГГц
соответственно. Критериями исследования вы-                 На рис. 2 изображен график.
браны 4 позиции – время работы, занимаемый                  В данном случае самым "прожорливым"
объем памяти метода с подготовленными дан-            оказался метод ForEach.
ными, объем памяти, занимаемой методом, и                   Как было описано ранее, для примене-
сложность кода. Каждый способ повторялся 50           ния метода ForEach необходимо создать мас-
раз для усреднения результатов.                       сив не из структурных элементов, а ссылоч-
       Рассмотрим показатели обработки дан-           ных (классов), поэтому объем памяти одного
ных на процессоре Intel Core i5 8400.                 элемента занимает не 4 байта, как в случае
       В табл. 1 представлены числовые пока-          структуры int, а 32 (помимо числового поля у
затели, отображающие зависимость времени              объекта память занимает служебный блок в
обработки в секундах от количества элемен-            16 байт + 4 байта на числовое поле и вырав-
тов в последовательности. Sequence – метод            нивание памяти до кратного 8 в большую
последовательного доступа к массиву.                  сторону – 32).
       Исходя из полученных показателей вид-             Таблица 2. Занимаемый объем памяти (МБ)
но, что самыми эффективными являются мето-                       Эл-ов
                                                                            1 млн     10 млн     100 млн
ды ThreadPool и Task. Такой результат можно           Метод
объяснить тем, что для обоих пул-потоков вы-             Sequence            3,82      38,15      381,47
деляется программной платформой во время                       For           3,85       38,21     381,56
запуска приложения, что не влечет за собою                   ForEach        30,56      305,24    3052,05
расходов по времени на создание потоков.                      PLINQ          3,85       38,19     381,51
       Для наглядности на рис. 1 изображен              ThreadPool           3,82      38,15      381,47
график. Из графика исключен метод Sequence,               Thread             3,86      38,19      381,52
как самый медленный, дабы не уплотнять по-                    Task           3,82      38,15      381,47

казатели остальных.

                                                 72
Многопоточность на платформе .NET. Обзор средств

                                                      Таблица 4. Занимаемое время обработки (с)
                                                             Эл-ов
                                                     Метод              1 млн      10 млн    100 млн

                                                       Sequence         0,00105   0,01009    0,10031
                                                          For           0,00045   0,00418    0,04214
                                                       ForEach          0,00043   0,00431    0,04381
                                                        PLINQ           0,00045   0,00424    0,04171
                                                      ThreadPool        0,00037   0,00366    0,03797
                                                        Thread          0,00059   0,00377    0,03681
                                                         Task           0,00037   0,00366    0,03653

 Рис. 2. Объем памяти, занимаемый методом и
                   данными
      В табл. 3 представлены показатели,
отображающие зависимость объема занимае-
мой оперативной памяти реализацией парал-
лелизма в килобайтах от количества элемен-
тов в массиве. На рис. 3 изображен график.
      Исходя из показателей, вполне логично,
что последовательный метод не будет исполь-
зовать оперативную память, а методы Task и
ThreadPool, в основе которых лежит пул-
потоков, занимают минимальный объем.                               Рис. 4. Время обработки

 Таблица 3. Используемая память на реализацию         Таблица 5. Занимаемый объем памяти (МБ)
               параллелизма (КБ)
                                                             Эл-ов
       Эл-ов                                                            1 млн      10 млн    100 млн
                1 млн     10 млн    100 млн          Метод
 Метод
  Sequence        0,00      0,00       0,00            Sequence          3,82      38,15     381,47
     For         40,30     66,24      90,24               For             3,84      38,20     381,67
   ForEach       42,88     64,96     267,04             ForEach          30,55     305,24    3052,20
    PLINQ        40,57     40,68      40,81              PLINQ            3,84      38,17     381,49
  ThreadPool      0,80      0,32       0,32           ThreadPool         3,82      38,15     381,47
    Thread       48,32     48,00      48,00             Thread           3,85      38,18     381,50
     Task         0,64      1,02       1,92              Task            3,82      38,15     381,47

  Рис. 3. Объем памяти, занимаемый методом            Рис. 5.Объем памяти, занимаемый методом и
       Для того чтобы подтвердить числовые                             данными
показатели произведены измерения на более            Таблица 6. Используемая память на реализацию
старшей        модели     процессора     In-                       параллелизма (КБ)
tel Core i5 3210m.                                           Эл-ов
                                                                         1 млн     10 млн    100 млн
       В табл. 4–6 представлены показатели с         Метод
зависимостью от количества элементов. К                Sequence           0,00      0,00      0,00
каждой таблице представлены графики (рис.                 For            28,96     50,72     203,04
4–6) отображающие показатели.                           ForEach          31,68     64,41     283,71
                                                         PLINQ           25,04     24,67      24,66
                                                      ThreadPool         0,48       0,32      0,32

                                                73
М. В. Шимановская, И. А. Муфтеев, Е. И. Илларионова

    Thread       32,32      32,00     32,00           нее. Данный критерий позволяет увидеть не
     Task         1,12      1,23       1,44           только объем памяти, занимаемой методом, но и
                                                      зависимость некоторых методов от количества
                                                      элементов в массиве. Методы For и ForEach за-
                                                      нимают больше памяти с ростом количества
                                                      элементов – это получается из-за того, что дан-
                                                      ные методы создают стек задач, которые выпол-
                                                      няются параллельно, возможно, что данный стек
                                                      разбивается пропорционально количеству име-
                                                      ющихся процессорных ядер в системе.
                                                            Сложность кода. Данный критерий –
                                                      субъективный для каждого рода задачи.
                                                            Для задач, в которых необходимо обойти
  Рис. 6. Объем памяти занимаемый методом             массив, подойдут – For, ForEach и PLINQ.
                                                            Для запуска потоков в фоновом режиме с
      Как и следовало ожидать, на более стар-         возможностью синхронизации данных как меж-
шей модели процессора показатели завышены             ду собою, так и между главным потоком подой-
по времени, однако они пропорциональны гра-           дет метод Thread.
фикам, полученным на младшем процессоре.
Объем используемой памяти также пропорцио-               Список литературы
нален и равнозначен, за исключением неболь-
ших погрешностей, на которые влияют как фи-           1. Ахмадулин Р.К. Параллельное программи-
зические характеристики машины, так и про-               рование на языке C#: учеб.-метод. пособие
граммные особенности исполняющей про-                    для студентов. Тюмень: ТИУ, 2016. 37 с.
граммной платформы разных версий .NET.                2. Стивен Клири. Concurrency in C#
                                                         Cookbook: учеб.-метод. пособие для сту-
  Выводы                                                 дентов. 2-е изд. СПб.: O'Reilly Media, 2018.
                                                         254 с.
      В заключение можно отметить субъек-
                                                      3. Террелл Рикардо. Конкурентность и парал-
тивные показатели по четырем критериям и
                                                         лелизм на платформе .NET. Паттерны эф-
выявить наиболее эффективные методы, ко-
                                                         фективного проектирования. СПб.: ТИУ,
торые можно применить для параллельных
                                                         2019. 624 с.
вычислений.
      Время. По данному критерию эффек-               4. Шилдт Герберт. Полное руководство С#
тивными являются методы Task и ThreadPool,               4.0. М.: ООО И.Д. Вильямс, 2011. 1465 с.
                                                      5. Джозеф Албахари. Threading in C#. URL:
причиной этого служит то, что при запуске
                                                         http://www.albahari.com/threading/part3.aspx
этих методов не нужно каждый раз создавать
                                                         Загл.     с   экрана     (дата    обращения:
новые потоки и запускать их, так как про-
                                                         02.10.2019).
граммная платформа делает это заранее, в
                                                      6. .NET Type Internals - From a Microsoft CLR
момент запуска домена приложения.
                                                         Perspective. 2007. URL:
      Занимаемый объем памяти с подго-
товленными данными. Этот критерий вы-                     https://www.codeproject.com/Articles/20481/
                                                          NET-Type-Internals-From-a-Microsoft-CLR-
полнен для того, чтобы показать преимуще-
                                                          Perspecti. Загл. с экрана (дата обращения:
ство методов, которые могут работать не
                                                          13.09.2019).
только с массивом состоящим из элементов
                                                      7. 2002 PODC Influential Paper Award. 2002.
ссылочного типа, но и структурного, в отли-
                                                         URL: http://www.podc.org/influential/2002-
чие от ForEach.
      Объем памяти, занимаемой методом.                  influential-paper/ Загл. с экрана (дата обра-
                                                         щения: 25.09.2019).
Эффективными методами являются Task и
ThreadPool, по той же причине, что описана ра-

Multithreading on the .NET platform.
Product Overview
M. V. Shimanovskaya, I. A. Mufteev, E. I. Illarionova

                                                 74
Многопоточность на платформе .NET. Обзор средств

Perm State Agro-technological University; 23, Petropavlovskaya st., Perm, 614990, Russia
mshim@mail.ru; 8(342) 2-103-413
      The methods of using parallelism on the .NET platform in the C # programming language are con-
      sidered. A comparative analysis of distributed computing approaches is presented. Numerical in-
      dicators of time between each of the methods are presented, as well as diagrams for visual repre-
      sentation.
      Keywords: multithreading; C#, Thread; ThreadPoo; Task; PLINQ; Parallel.For; Paral-
      lel.ForEach.

                                                     75
Вы также можете почитать