Чарівництво Parallel.ForEach. Короткий огляд

У цій частині ми розглянемо внутрішній механізм роботи паралельного циклу Parallel.ForEach. Вам може здаватися, що розуміння внутрішнього механізму паралельного циклу при створенні програм вам не до чого, однак автор наводить кілька доказів для того що б ви більш глибоко вникли в цей механізм.
По-перше ви дізнаєтеся, тонкощі використання OtlDataManager і зможете більш глибоко застосовувати ці знання в ваших додатках.По-друге – це цікава тема :)І в третіх використання Parallel.ForEach набагато більш краще рішення, ніж використання доморощеного багатопотокового коду, так як OTL більш гнучка.

Давайте почнемо знайомство зі страшною діаграмою. Картина нижче показує внутрішній механізм OtlDataManager. У майбутньому ми будемо часто звертатися до цій діаграмі, але поки Ви можете благополучно пропустити її

OtlDataManagerВражає, чи не так. Але давайте зосередимося на коді.
Parallel.ForEach(1, 1000)
  .Execute(
    procedure (const elem: integer)
    begin
    end);

Цей простий код виконується в циклі від 1 до 1000 на всіх доступних ядрах паралельно і виконує просту процедуру, яка не містить робочого коду. При першому погляді, код нічого не робить – але всередині механізму розпаралелювання він робить роботу в дуже складній манері.Метод ForEach створює новий об’єкт TOmniParallelLoop . Цей об’єкт, який координує паралельні завдання, і є вихідним провайдером, який знає, як отримати доступ до значення перерахування, яке в даний момент перераховується (від 1 до 1000 в цьому прикладі)OtlDataManager містить чотирьох інших вихідних провайдера – один для кожного типу даних, який можна передати до методу ForEach (докладніше ми розглянемо це далі). Як пише автор “Якби була б потреба розширити ForEach з новим джерелом перерахування, я повинен був би тільки додати трохи простих методів до модуля OtlParallel і написати новий вихідний провайдер “.

class function Parallel.ForEach(low, high: integer; step: integer):
  IOmniParallelLoop<integer>;
begin
  Result := TOmniParallelLoop<integer>.Create(
  CreateSourceProvider(low, high, step), true);
end; { Parallel.ForEach }

Зрештою, викликається InternalExecuteTask. Цей метод відповідальний за створення і старт паралельних задач циклу.InternalExecuteTask спочатку створює менеджер даних і прив’язаний до вихідного постачальнику (порівняйте це з картиною вище – є один вихідний постачальник і один менеджер даних).Потім він створює відповідне число завдань і викликає певний для задачі метод делегата від кожного.Цей делегат обгортають Ваш паралельний код і надає йому належний вхід (і іноді вихід) в створену задачу.
procedure TOmniParallelLoopBase.InternalExecuteTask(
  taskDelegate: TOmniTaskDelegate);
var
  dmOptions    : TOmniDataManagerOptions;
  iTask        : integer;
  numTasks     : integer;
  task         : IOmniTaskControl;
  begin
    …
    oplDataManager := CreateDataManager(oplSourceProvider,
      numTasks, dmOptions);
    …
    for iTask := 1 to numTasks do
    begin
      task := CreateTask(
        procedure (const task: IOmniTask)
        begin
          …
          taskDelegate(task);
          …
        end,
        …
      task.Schedule(GParallelPool);
    end;
    …
  end;
end;

Об’єкт “менеджер даних” це частина в TOmniParallelLoop . Він є глобальним для всіх делегатів. Це зроблено для того, щоб можна було його просто використовувати і викликати в делегатові завдання.Більш досконалий проект мав би послати цей об’єкт делегату завдання як вхідний параметр. Можливо, автор зробить це в майбутньому, однак у версії 2.0 менеджер даних один і він глобальний, прийміть це за факт.Найпростіший делегат завдання (нижче) тільки створює локальну чергу і передає значення перерахування один за одним. Такий підхід призводить до багатьох локальних черг. Отриманий результат передається в задачу пов’язану з менеджером даних.На випадок якщо Ви ставите питанням, що таке loopBody – то це анонімний метод, який Ви передали в методі Execute Parallel.ForEach.

procedure InternalExecuteTask(const task: IOmniTask)
var
  localQueue: TOmniLocalQueue;
  value     : TOmniValue;
begin
  localQueue := oplDataManager.CreateLocalQueue;
  try
    while (not Stopped) and localQueue.GetNext(value)
    do
      loopBody(task, value);
    finally
      FreeAndNil(localQueue);
    end;
end;

Давайте повторимо:

Як ви бачите, локальна чергу отримує дані в пакетах (data package) з менеджера даних і посилає ці дані в вихідний буфер який засвідчується, що надіслані дані отримані в правильному порядку.Якщо завдання вичерпує роботу, вона просить новий пакет даних від менеджера даних, який отримує ці дані від вихідного провайдера (більш докладно розглянемо це далі). Якщо вихідний провайдер вичерпає дані, то менеджер даних спробує вкрасти деякі дані від інших завдань. Схематично це виглядає так:


Все це було розроблено, щоб забезпечити швидкий доступ до даних (блокування обмежена вихідним провайдером всі інші взаємодії відбуваються без блокування), що забезпечує гарний розподіл робочого навантаження.
 Автор: Кузан Дмитро

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*