Рейтинг@Mail.ru

Параллельное программирование в Delphi XE7

Автор: Alex. Опубликовано в Программирование . просмотров: 20537

Рейтинг:  5 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда активна
 

В Delphi XE7 была включена библиотека для параллельного программирования. Чтобы её использовать, достаточно подключить юнит System.Threading. Давайте разберёмся, что в этой библиотеке есть и как её можно использовать.

Библиотека параллельного программирования Parallel Programming Library (PPL) работает на Windows, MacOSX, Android и iOS устройствах и позволяет вашим приложениям запускать задачи параллельно, используя тем самым преимущества многопроцессорных девайсов или компьютеров. Библиотека PPL позволит вам запускать задачи, объединять их, ожидать выполнение группы задач и многое другое. Для всего этого в библиотеке есть автоматически настраиваемый (в зависимости от загрузки процессора) пул потоков (см. класс TThreadPool), который позволит вам забыть про создание и управление потоками.

Используя библиотеку PPL вы сможете с лёгкостью:

- создавать циклы, используя конструкцию TParallel.For;
- запускать параллельные задачи, используя TTask и ITask;
- бросить выполняемый процесс, сфокусировавшись на другие задачи, а затем получить результат этого процесса в точке, в которой вы хотите.

Использование TTask

TTask – это класс, который позволяет запускать одну задачу или несколько задач параллельно. При этом вам не придётся создавать поток и управлять им. Класс TTask реализует интерфейс ITask. В интерфейсе ITask в вашем распоряжении есть функции для запуска (Start), ожидания (Wait) и отмены (Cancel) задачи и статус (Status), позволяющий узнать, что происходит с задачей. Вот возможные статусы задачи: Created (задача создана), WaitingToRun (задача ожидает окончания выполнения другого процесса), Running (задача выполняется), Completed (задача завершена), WaitingForChildren (задача ожидает окончания выполнения дочерней задачи), Canceled (задача была отменена), Exception (при выполнении задачи произошла ошибка).

Посмотрим теперь, как создать и запустить задачу. Для этого нужно создать класс TTask, передав в конструктор класса процедуру, которая будет выполняться в потоке, а затем вызвать функцию Start.

uses System.Threading;
 
 procedure TForm1.Button1Click(Sender: TObject);
var
   task: ITask;
begin
   //Создаём задачу.
   task := TTask.Create(procedure ()
      begin
         //Выполняем задачу 3 секунды.
         Sleep(3000);
         //Задача выполнена!
         ShowMessage('Задача выполнена!');
      end);
   //Запускаем задачу.
   task.Start;
end;

А теперь попробуем выполнить несколько задач параллельно. Для этого нужно создать массив задач, запустить задачи и ждать когда они выполнятся. Для ожидания есть две статические функции класса TTask: WaitForAll (ожидать выполнения всех задач) и WaitForAny (ожидать выполнения хотя бы одной из задач).

uses System.Threading, System.SyncObjs;
 
 procedure TForm1.Button2Click(Sender: TObject);
var
   tasks: array of ITask;
   task: ITask;
   value: integer;
 
   procedure CreateTasks;
   begin
      value := 0;
      tasks := [
         TTask.Create(procedure()
            begin
               //Выполняем задачу 5 секунд.
               Sleep(5000);
               //Добавляем к результату 5000.
               TInterlocked.Add(value, 5000);
            end
         ),
         TTask.Create(procedure()
            begin
               //Выполняем задачу 3 секунды.
               Sleep(3000);
               //Добавляем к результату 3000.
               TInterlocked.Add(value, 3000);
            end
         )
      ];
   end;
 
 begin
   //Создаём задачи и инициализируем переменную value.
   CreateTasks;
   //Запускаем все задачи в массиве.
   for task in tasks do
      task.Start;
   //Ждём выполнение всех задач.
   TTask.WaitForAll(tasks);
   //Результат будет 8000.
   ShowMessage('Все задания выполнены. Результат: ' + IntToStr(value));
   //Создаём задачи и инициализируем переменную value.
   CreateTasks;
   //Запускаем все задачи в массиве.
   for task in tasks do
      task.Start;
   //Ждём выполнение любой из задач.
   TTask.WaitForAny(tasks);
   //Результат будет 3000.
   ShowMessage('Все задания выполнены. Результат: ' + IntToStr(value));
end;

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

unit Unit1;
 
interface
 
uses
   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls, System.Threading;
 
type
   TForm1 = class(TForm)
      btnStart: TButton;
      btnCancel: TButton;
      Label1: TLabel;
      ProgressBar1: TProgressBar;
      procedure btnStartClick(Sender: TObject);
      procedure btnCancelClick(Sender: TObject);
      procedure FormClose(Sender: TObject; var Action: TCloseAction);
   private
      { Private declarations }
      task: ITask;
   public
      { Public declarations }
   end;
 
var
   Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
procedure TForm1.btnStartClick(Sender: TObject);
begin
   //Если задание уже есть и оно выполняется, останавливаем его.
   if Assigned(task) and (task.Status = TTaskStatus.Running) then
      task.Cancel;
   //Создаём новое задание.
   task := TTask.Create(procedure()
      var
         task: ITask;
         stopwatch, curPos, maxPos: integer;
      begin
         //Сохраняем ссылку на интерфейс ITask в локальной переменной.
         task := self.task;
         //Обнуляем секундомер.
         stopwatch := 0;
         //Работать с контролами формы нужно из главного потока,
         //поэтому используем метод TThread.Synchronize для синхронного взаимодействия
         //или TThread.Queue - для асинхронного.
         TThread.Synchronize(nil,
            procedure()
            begin
               //Сохраняем максимальное значение прогресс бара в локальной переменной.
               maxPos := Progressbar1.Max;
               //Сбрасываем текущее положение прогресс бара в Progressbar1.Min.
               curPos := Progressbar1.Min;
               Progressbar1.Position := Progressbar1.Min;
               //Сбрасываем показания секундомера.
               Label1.Caption := '0';
            end
         );
         //Выполняем цикл, пока не дойдём до Progressbar1.Max.
         while curPos < maxPos do
         begin
            //Увеличиваем количество секунд в секундомере.
            //Переменная stopwatch локальная, поэтому TInterlocked.Add не используем.
            Inc(stopwatch, 2);
            //Увеличиваем прогресс.
            //Переменная curPos локальная, поэтому TInterlocked.Add не используем.
            Inc(curPos);
            TThread.Synchronize(nil,
               procedure()
               begin
                  //Показываем количество пройденных секунд.
                  Label1.Caption := stopwatch.ToString;
                  //Показываем прогресс.
                  Progressbar1.Position := curPos;
               end
            );
            //Спим 2 секунды.
            Sleep(2000);
            //Проверяем, не отменили ли задание.
            if task.Status = TTaskStatus.Canceled then
               //Если отменили, выходим из цикла.
               break;
         end;
      end
   );
   //Стартуем созданное задание.
   task.Start;
end;
 
procedure TForm1.btnCancelClick(Sender: TObject);
begin
   //Отменяем выполнение задания.
   if Assigned(task) then
      task.Cancel;
end;
 
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   //Если форма закрылась, то отменяем выполнение задания, если задание есть и оно выполняется.
   if Assigned(task) and (task.Status = TTaskStatus.Running) then
      task.Cancel;
end;
 
end.

Использование TParallel.For

Статическая функция For класса TParallel позволяет с лёгкостью выполнять вашу функцию в параллельном цикле. В качестве примера рассмотрим алгоритм поиска простых чисел. Функция, определяющая является ли число простым, будет следующей:

function IsPrime (n: Integer): boolean;
var
   test: integer;
begin
   //Считаем, что число по умолчанию простое.
   IsPrime := true;
   //Пробуем делить число на другие числа.
   for test := 2 to n - 1 do
      if (n mod test) = 0 then
      begin
         //Если число делится на другое число (кроме 1 или n) без остатка,
         //то число не является простым.
         IsPrime := false;
         //Выходим из цикла.
         break;
      end;
end;

Классический массив перебора чисел и подсчёта количества найденных простых чисел будет выглядеть так:

procedure TForm1.Button3Click(Sender: TObject);
const
   max = 50000;
var
   i, total: integer;
begin
   total := 0;
   for i := 1 to max do
      if IsPrime(i) then
         Inc(total);
   ShowMessage('Количество найденных простых чисел: ' + IntToStr(total));
end;

Сделать параллельный цикл в этом случае можно заменив оператор for на функцию класса TParallel.For и подставив код цикла в анонимную функцию.

procedure TForm1.Button4Click(Sender: TObject);
const
   max = 50000;
var
   i, total: integer;
begin
   total := 0;
   TParallel.For(1, max, procedure(i: integer)
      begin
         if IsPrime(i) then
            TInterlocked.Increment(total);
      end
   );
   ShowMessage('Количество найденных простых чисел: ' + IntToStr(total));
end;

Теперь давайте посмотрим, как прервать цикл TParallel.For. Допустим нам нужно найти не меньше 1000 простых чисел, а затем прервать цикл. Вот каким станет наш код.

procedure TForm1.Button4Click(Sender: TObject);
const
   max = 50000;
var
   i, total: integer;
begin
   total := 0;
   TParallel.For(1, max, procedure(i: integer; loopState: TParallel.TLoopState)
      begin
         if IsPrime(i) then
         begin
            System.TMonitor.Enter(self);
            try
               Inc(total);
               if total > 1000 then
                  loopState.Break;
            finally
               System.TMonitor.Exit(self);
            end;
         end;
      end
   );
   ShowMessage('Количество найденных простых чисел: ' + IntToStr(total));
end;

Использование TTask.IFuture

Используя интерфейс IFuture вы можете посчитать что-нибудь или выполнить какой-либо запрос, параллельно выполняя другие задачи, и получить результат в то время, когда вам это будет нужно. Если в этой точке значение ещё не будет готово, запрашивающий поток будет заблокирован, пока значение не будет получено. Это позволяет выставлять приоритеты блокам вашего кода, выполняя их в определённом порядке, и при этом быть уверенными, что вы получите значение в определённой точке.

Сделаем простой пример использования интерфейса IFuture. Для начала запустим задачу и попробуем сразу получить значение. В следующем примере выполнение программы приостановится, пока не отработает задача.

procedure TForm1.Button5Click(Sender: TObject);
var
   future: IFuture<integer>;
begin
   //Создаём задачу.
   future := TTask.Future<integer>(function: integer
      begin
         Sleep(3000);
         Result := 10;
      end
   );
   //Запускаем задачу.
   future.Start;
   //Узнать результат получится только после завершения задачи: через 3 секунды.
   ShowMessage('Результат: ' + IntToStr(future.Value));
end;

А вот пример, когда программа запрашивает результат после того как задача уже отработала. В этом случае результат будет получен сразу.

procedure TForm1.Button6Click(Sender: TObject);
var
   future: IFuture<integer>;
begin
   //Создаём задачу.
   future := TTask.Future<integer>(function: integer
      begin
         Sleep(3000);
         Result := 20;
      end
   );
   //Запускаем задачу.
   future.Start;
   //Выполняем какие-то другие задачи.
   Sleep(5000);
   //Узнать результат здесь мы можем сразу,
   //т.к. задача в этой точке отработала 2 секунды назад.
   ShowMessage('Результат: ' + IntToStr(future.Value));
end;

Управление пулом потоков TThreadPool

Получить текущий экземпляр класса TThreadPool вы можете из свойства класса TThreadPool.Default, у которого вы сможете считать или у которого вы сможете изменить максимальное и минимальное количество потоков. Как это сделать, показано в примере.


procedure TForm1.Button7Click(Sender: TObject);
begin
   //Считываем размеры пула.
   ShowMessage('Максимальный размер пула: '
      + IntToStr(TThreadPool.Default.MaxWorkerThreads));
   ShowMessage('Минимальный размер пула: '
      + IntToStr(TThreadPool.Default.MinWorkerThreads));
   //Устанавливаем размеры пула.
   TThreadPool.Default.SetMaxWorkerThreads(10);
   TThreadPool.Default.SetMinWorkerThreads(2);
end;

Стоит ли использовать библиотеку параллельного программирования Delphi?

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

Tags: Учебники по программированию Параллельное программирование Delphi

Комментарии   

Евгений
0 #21 Евгений 02.04.2016 03:36
Здравствуйте.
Подскажите пожалуйста, как с помощью этого можно реализовать следующее:
Есть 4 сайта, с которых с определенной периодичностью нужно получать информацию через их API.
Раз, например, в секунду нужно вызывать idhttp.get, получить из него результат и как-то обработать. Я так понимаю, что это будет вызов по таймеру, но нужно, чтобы этот запрос не "вешал" программу, т.к. параллельно должны быть запущены еще 3 таких запроса. При всем при этом пользователь должен продолжать работать с программой.
Буду очень признателен за помощь.
Цитировать
Alex
+1 #22 Alex 02.04.2016 14:18
Цитирую Евгений:
Здравствуйте.
Подскажите пожалуйста, как с помощью этого можно реализовать следующее:
Есть 4 сайта, с которых с определенной периодичностью нужно получать информацию через их API.
Раз, например, в секунду нужно вызывать idhttp.get, получить из него результат и как-то обработать. Я так понимаю, что это будет вызов по таймеру, но нужно, чтобы этот запрос не "вешал" программу, т.к. параллельно должны быть запущены еще 3 таких запроса. При всем при этом пользователь должен продолжать работать с программой.
Буду очень признателен за помощь.

Можете написать функцию, которая будет принимать на вход, например, адрес сайта и циклично делать то, что вы описали. Затем создать 4 задания, в конструкторе каждому из них передать адрес сайта и адрес этой функции. Затем запустить все эти 4 задания. Только, чтобы сделать паузы, нужно использовать не таймер, а Sleep или TEvent.WaitFor.
Цитировать
Евгений
0 #23 Евгений 04.04.2016 11:43
Цитата:
Можете написать функцию, которая будет принимать на вход, например, адрес сайта и циклично делать то, что вы описали. Затем создать 4 задания, в конструкторе каждому из них передать адрес сайта и адрес этой функции. Затем запустить все эти 4 задания. Только, чтобы сделать паузы, нужно использовать не таймер, а Sleep или TEvent.WaitFor.
А не могли бы сделать небольшой пример? Просто не могу понять как передать адрес функции. Все, что видел в интернете сводится к тому, что тело функции описывается при создании задания.
Заранее спасибо за ответ.
Цитировать
Alex
0 #24 Alex 05.04.2016 08:45
Цитирую Евгений:
А не могли бы сделать небольшой пример? Просто не могу понять как передать адрес функции. Все, что видел в интернете сводится к тому, что тело функции описывается при создании задания.
Заранее спасибо за ответ.

Посмотрите комментарий #11 к этой статье, я там как раз привёл такой пример.
Цитировать
Евгений
0 #25 Евгений 05.04.2016 10:38
Спасибо, видимо пропустил:)
Цитировать
Максим
0 #26 Максим 17.03.2017 13:05
Здравствуйте.
Помогите пожалуйста с такой проблемой.
Допустим есть объект в котором запускается Task, этот таск после своей работы должен вернуть данные в основной поток, я их возвращаю через TThread.Synchronize. Однако объект может быть разрушен до отработки потока, разрушать объект в котором ещё работает таск не хорошо, нужно как-то заблокировать разрушение объекта, пока не отработает таск. Но сделать это через Wait невозможно, иначе я получу Deadlock, главный поток будет ждать окончания работы таска, а таск будет ждать освобождения главного потока, чтобы выполнить Synchronize. Классно было бы если бы можно вызвать Cancel для таска и внутри не вызывать Synchronize, если было вызвано прерывание. Но если вызвать Cancel, то уже невозможно использовать Wait. Какой-то замкнутый круг, непонятно как решить эту проблему.
Цитировать
Alex
0 #27 Alex 19.03.2017 07:18
Цитирую Максим:
Классно было бы если бы можно вызвать Cancel для таска и внутри не вызывать Synchronize, если было вызвано прерывание. Но если вызвать Cancel, то уже невозможно использовать Wait. Какой-то замкнутый круг, непонятно как решить эту проблему.

Здравствуйте. Вместо вызова Cancel, можете просто завести boolean-переменную, выствлять её в true, если нужно завершить таск, и ждать вызовом Wait. А в таске всё время проверять значение этой переменной, и, если оно true, то завершать таск.
Цитировать
Максим
0 #28 Максим 19.03.2017 12:19
Цитирую Alex:
Здравствуйте. Вместо вызова Cancel, можете просто завести boolean-переменную, выствлять её в true, если нужно завершить таск, и ждать вызовом Wait. А в таске всё время проверять значение этой переменной, и, если оно true, то завершать таск.

В таком случае получается не синхронизированный доступ к переменной, что может привести к плохим последствиям. А если из таска получать эту переменную через Synchronize, то от проблемы мы никуда не уходим.
Пока единственное решение, которое мне приходит в голову это вот такой костыль:
Цитата:
while Assigned(Task) and (Task.Status = TTaskStatus.Running) do
begin
Application.HandleMessage;
end;
Вызвать его и ждать пока не отработает таск. Ещё можно HandleMessage заменить на ProcessMessages и Sleep, но смысл тот же.
Цитировать
Alex
+2 #29 Alex 19.03.2017 19:25
Цитирую Максим:
В таком случае получается не синхронизированный доступ к переменной, что может привести к плохим последствиям.

Если у вас Delphi Berlin, то используйте атрибут volatile, иначе используйте System.TMonitor.Enter(...) - System.TMonitor.Exit(...).
Цитировать
Максим
0 #30 Максим 19.03.2017 20:56
Цитирую Alex:
Если у вас Delphi Berlin, то используйте атрибут volatile, иначе используйте System.TMonitor.Enter(...) - System.TMonitor.Exit(...).

Не знал про volatile. Спасибо за помощь, проблема решена.
Цитировать

Добавить комментарий


Защитный код
Обновить