Рейтинг@Mail.ru

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

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

Рейтинг:  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

Комментарии   

ruslan123456
0 #31 ruslan123456 19.11.2017 14:16
Как остановить task, если он в данный момент выполняет длительную операцию?
Цитировать
Alex
0 #32 Alex 20.11.2017 22:45
Цитирую ruslan123456:
Как остановить task, если он в данный момент выполняет длительную операцию?

Правильный способ - это подать сигнал внутреннему потоку, что ему пора завершаться. После этого можно подождать завершения задания. Сигнал можно подать через переменную (например, установив её в true) или событие (TEvent).

Плохой способ - это убивать поток задания функцией TerminateThread.
Цитировать

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