<4> 並列プログラミングライブラリ (PPL) (Delphi コンカレントプログラミング)

# 4. 並列プログラミングライブラリ (PPL)
クロスプラットフォームで使える**並列プログラミングライブラリ** (**P**arallel **P**rogramming **L**ibrary / **PPL**) は `System.Threading` を **uses** に加えることで利用可能になります [^1]。

この並列プログラミングライブラリはマルチスレッドを抽象化しており、`TThread` を使う事なくマルチスレッドを利用可能です。

## 4.1. スレッドプール
例えば 1 万ファイルをスレッドで処理するのに、1 万スレッド生成するのは効率の面で問題があります。生成されるスレッドの上限を決めておき、同時実行されるスレッドの管理を行う機構がスレッドプールです。いわゆる **Worker Thread** です。

Delphi の PPL では、CPU の負荷に応じて自動的にワーカースレッドが作成されます。 ワーカースレッドは `System.Threading.TThreadPool` クラスが管理しています。

`TThreadPool` クラスをそのまま使う事はまずないかと思います。

**See also:**

 - [System.Threading.TThreadPool (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TThreadPool)
 - [並列プログラミングでのスレッドプーリングの概要 (Support Wiki)](https://docwiki.embarcadero.com/Support/ja/%E4%B8%A6%E5%88%97%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%A7%E3%81%AE%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%83%97%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E6%A6%82%E8%A6%81)
 - [スレッドプール (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/windows/win32/procthread/thread-pools)
 - [マネージドスレッドプール (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/dotnet/standard/threading/the-managed-thread-pool)
 - [Thread pool (Wikipedia: en)](https://en.wikipedia.org/wiki/Thread_pool)

## 4.2. TTask
コードブロックを簡単にスレッドで実行することができます。

```pascal
uses
  ..., System.Threading;

...

  TTask.Run(procedure
    begin
      // 何かの処理
      TThread.Synchronize(nil,
        procedure
        begin
          // 何かの処理
        end);
    end);
```

コンストラクタと `Start()` メソッドを使って `TThread.CreateAnonymousThread` のように使う事もできます。

```pascal
  TTask.Create(procedure
    begin
      // 何かの処理
      TThread.Synchronize(nil,
        procedure
        begin
          // 何かの処理
        end);
    end).Start;
```

インターフェイス型変数に取って処理する事もできます。次のコードの `Task` 変数はインターフェイス型変数なので、明示的な破棄は不要です。

```pascal
var
  Task: ITask; 

...

  Task := TTask.Create(procedure
    begin
      // 何かの処理
      TThread.Synchronize(nil,
        procedure
        begin
          // 何かの処理
        end);
    end);
  Task.Start;
```

`WaitForAll()` を使うと、ITask 配列で処理されるスレッドの終了待ちを行う事ができます。

```pascal
var
  TaskArr: array of ITask;
begin
  Setlength (TaskArr ,3);
  TaskArr[0] := TTask.Run(procedure begin {何かの処理} end);
  TaskArr[1] := TTask.Run(procedure begin {何かの処理} end);
  TaskArr[2] := TTask.Run(procedure begin {何かの処理} end);
  TTask.WaitForAll(TaskArr);
end;
```

もっとシンプルに書くこともできます。

```pascal
  TTask.WaitForAll([
    TTask.Run(procedure begin {何かの処理} end),
    TTask.Run(procedure begin {何かの処理} end),
    TTask.Run(procedure begin {何かの処理} end)
  ]);
```

タスクを停止させるには、`Cancel()` メソッドを使います。

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  var Task := TTask.Run(
    procedure
    begin
      TThread.Sleep(2000);
      TThread.Synchronize(nil,
        procedure
        begin
          Button1.Caption := 'Done.'; // ボタンを押して 2 秒後にボタンのキャプションが...
        end);
    end);
  Task.Cancel; // 書き変わらない (w
end;
```

**See also:**

 - [System.Threading.TTask (DocWiki)](https://docwiki.embarcadero.com/Libraries/Sydney/ja/System.Threading.TTask)
 - [System.Threading.TTask.Run (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TTask.Run)
 - [System.Threading.TTask.Create (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TTask.Create)
 - [System.Threading.ITask.Start (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.ITask.Start)
 - [System.Threading.ITask.Cancel (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.ITask.Cancel)
 - [System.Threading.TTask.WaitForAll (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TTask.WaitForAll)
 - [チュートリアル：並列プログラミング ライブラリのタスクを使用する (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%EF%BC%9A%E4%B8%A6%E5%88%97%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0_%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%BF%E3%82%B9%E3%82%AF%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)
 - [Delphi でコントロール配列 \[小ネタ\] (Qiita)](https://qiita.com/ht_deko/items/9bf3b2e761cc4f169c75)

## 4.3. TTask.Future
いわゆる **Future** です。意訳すると **"引換券"** です。焼き芋屋さんに「焼いといて！」とお願いして、引換券をもらった感じです。すぐに引き換えに行っても「まだ焼けてないよ！」と待たされることになりますが、ちょっと経ってから行くとすぐに焼き芋を貰えます。

VCL フォームアプリケーションを新規作成し、フォームにボタンを置いて、次のようなコードを書きます。

```pascal
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Threading;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;
  Future: IFuture<Integer>;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage(Future.Value.ToString)
end;

initialization
  Future := TTask.Future<Integer>(
    function: Integer
    begin
      TThread.Sleep(5000);
      Result := 100;
  end);
end.
```

アプリケーションを起動してすぐにボタンを押すと結果が表示されるまでちょっと待たされますが、アプリケーションを起動して 5 秒以上待ってからボタンを押すと結果がすぐに表示されます。

コード中の `Future` はインターフェイス型変数なので、明示的な破棄は不要です。

:::note info
『Delphi クイックリファレンス』には TThread を使った TFuture の実装が載っています。
Ray Lischner (著), 光田 秀, 竹田 知生 (訳) (2001).「Delphi クイックリファレンス」オライリージャパン. pp.174-184.
:::

**See also:**

 - [System.Threading.TTask.Future (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TTask.Future)
 - [System.Threading.IFuture (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.IFuture)
 - [チュートリアル：並列プログラミング ライブラリのフューチャを使用する (DocWiki)](https://docwiki.embarcadero.com/RADStudio/Sydney/ja/%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%EF%BC%9A%E4%B8%A6%E5%88%97%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0_%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%83%95%E3%83%A5%E3%83%BC%E3%83%81%E3%83%A3%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)
 - [Future / Promise / Delay パターン (Wikipedia)](https://ja.wikipedia.org/wiki/Future_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)

## 4.4. TParallel.For
ものすごくざっくり言えば **for to do** 文の並列実行版です。一つのコードブロックを繰り返し並列実行します。

オーバーロードされたメソッドが多いので混乱しますが、最もシンプルなものは次のようなパラメータを持ちます。

```pascal
TParallel.For(開始値, 終了値, 実行する手続きへの参照型);
```

「実行する手続き」のうち最もシンプルなのは `TProc<Integer>` です。`i: Integer` のような Integer 型のパラメータを一つ持つ手続きです。パラメータ名は `i` でなくとも任意の識別子で構いません。

```pascal
uses
  ..., System.Threading;

  ...

  // シーケンシャル実行
  for var i:=1 to 10 do
  begin
    // ここのコードが繰り返し (直列) 実行される。
  end;

  // パラレル実行
  TTask.Run(procedure
    begin
      TParallel.For(1, 10, 
        procedure (i: Integer)
        begin
          // ここのコードがスレッドで並列実行される。
        end);
    end);
```

`TParallel.For()` を `TTask` 内で実行している理由の一つは `TParallel.For()` はメインスレッドで実行されると `TThread.Synchronize()` をブロックするからです。

```pascal
// 本当に固まるので実行注意！
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Clear;
  Memo1.Update;
  TParallel.For(1, 10,
    procedure (i: Integer)
    begin
      TThread.Sleep(5000);
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Lines.Add(i.ToString);
        end);
    end);
end;
```

`TThread.Queue()` だとブロックされないのですが、すべてのスレッドを実行し終わるまで (終了ではありません) はフォームを移動できません。

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Clear;
  Memo1.Update;
  TParallel.For(1, 10,
    procedure (i: Integer)
    begin
      TThread.Sleep(5000);
      TThread.Queue(nil,
        procedure
        begin
          Memo1.Lines.Add(i.ToString);
        end);
    end);
end;
```

...なので、`TParallel.For()` は基本的に `TTask` または、 

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Clear;
  Memo1.Update;
  TTask.Run(procedure
    begin
      TParallel.For(1, 10,
        procedure (i: Integer)
        begin
          TThread.Sleep(5000);
          TThread.Synchronize(nil,
            procedure
            begin
              Memo1.Lines.Add(i.ToString);
            end);
        end)
    end);
end;
```

`TThread` と併用する必要があります。

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Clear;
  Memo1.Update;
  TThread.CreateAnonymousThread(procedure
    begin
      TParallel.For(1, 10,
        procedure (i: Integer)
        begin
          TThread.Sleep(5000);
          TThread.Synchronize(nil,
            procedure
            begin
              Memo1.Lines.Add(i.ToString);
            end);
        end)
    end).Start;
end;
```

:::note warn
10.1 Berlin と 10.2 Tokyo には TParallel.For() が最初の 2 ループを同じスレッドで実行してしまうというバグがあります。
:::

**See also:**

 - [System.Threading.TParallel.For (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TParallel.For)
 - [並列プログラミング ライブラリの TParallel.For の使用 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E4%B8%A6%E5%88%97%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0_%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE_TParallel.For_%E3%81%AE%E4%BD%BF%E7%94%A8)
 - [チュートリアル：並列プログラミング ライブラリの for ループを使用する (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%EF%BC%9A%E4%B8%A6%E5%88%97%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0_%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE_for_%E3%83%AB%E3%83%BC%E3%83%97%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)
 - [RSP-20695: Parallel For or Join with n iterations runs in n-1 threads (Quality Portal)](https://quality.embarcadero.com/browse/RSP-20695)

## 4.5. TParallel.Join
`TParallel.Join` は、複数のコードブロックを並列実行します。

最もシンプルなものは次のようなパラメータを持ちます。

```pascal
TParallel.Join(実行する手続きへの参照型の配列);
```

`TTask.WaitForAll()` もそうでしたが、`TParallel.Join()` を無名メソッドで書く事はあまりないかもしれません。

```pascal
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Clear;
  Memo1.Update;
  TParallel.Join([
    procedure
    begin
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Lines.Add('A');
        end);
    end,
    procedure
    begin
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Lines.Add('B');
        end);
    end,
    procedure
    begin
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Lines.Add('C');
        end);
    end
  ]);
end;
```

好みの問題でしょうが、個人的には手続きは独立させておいた方が視認性が良いと感じます。

```pascal
procedure TForm1.A;
begin
  TThread.Synchronize(nil,
    procedure
    begin
      Memo1.Lines.Add('A');
    end);
end;

procedure TForm1.B;
begin
  TThread.Synchronize(nil,
    procedure
    begin
      Memo1.Lines.Add('B');
    end);
end;

procedure TForm1.C;
begin
  TThread.Synchronize(nil,
    procedure
    begin
      Memo1.Lines.Add('C');
    end);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Clear;
  Memo1.Update;
  TParallel.Join([A, B, C]);
end;
```

:::note warn
10.1 Berlin と 10.2 Tokyo には TParallel.Join() が最初の 2 つのタスクを同じスレッドで実行してしまうというバグがあります。
:::

**See also:**

 - [System.Threading.TParallel.Join (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TParallel.Join)
 - [RSP-19557: TParallel.Join does not create enough threads (Quality Portal)](https://quality.embarcadero.com/browse/RSP-19557)

## 4.6. TParallelArray.Sort
`TParallelArray.Sort()` はスレッドを使ってパラメータに指定された配列を高速にソートするパラメータ化メソッドです [^2]。

```pascal:ParallelArray1.dpr
program ParallelArray1;
{$APPTYPE CONSOLE}
uses
  System.SysUtils, System.Generics.Collections, System.Threading, System.Diagnostics;
  
var
  IntArr1, IntArr2: array of Integer;
  T: TStopWatch;
begin
  SetLength(IntArr1, 10000000);
  SetLength(IntArr2, 10000000);
  for var i:=Low(IntArr1) to High(IntArr1) do
  begin
    IntArr1[i] := Random(MAXINT);
    IntArr2[i] := IntArr1[i];
  end;  

  // TArray
  T := TStopWatch.StartNew;
  TArray.Sort<Integer>(IntArr1);
  Writeln(T.Elapsed.ToString);

  // TParallelArray
  T := TStopWatch.StartNew;
  TParallelArray.Sort<Integer>(IntArr2);
  Writeln(T.Elapsed.ToString);
end.
```

コンペアラ (比較者) をパラメータに指定可能なオーバーロードされたメソッドもあります。

```pascal:ParallelArray2.dpr
program ParallelArray2;
{$APPTYPE CONSOLE}
uses
  System.SysUtils, System.Generics.Collections, System.Generics.Defaults, System.Threading;
  
var
  IntArr: array of Integer;
  Comparer: IComparer<Integer>;
begin
  SetLength(IntArr, 10);
  for var i:=Low(IntArr) to High(IntArr) do
    IntArr[i] := Random(100);

  // Sort (昇順)
  TParallelArray.Sort<Integer>(IntArr);
  for var i:=Low(IntArr) to High(IntArr) do
    Writeln('#1: ', IntArr[i]);
  
  // Sort with Comparer (降順)
  Comparer := TDelegatedComparer<Integer>.Create(
    function(const Left, Right: Integer): Integer
    begin
      result := Right - Left; 
    end);
  TParallelArray.Sort<Integer>(IntArr, Comparer);
  for var i:=Low(IntArr) to High(IntArr) do
    Writeln('#2: ', IntArr[i]);  
end.
```

**See also:**

 - [System.Threading.TParallelArray.Sort (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TParallelArray.Sort)
 - [System.Threading.TParallelArray.SortThreshold (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TParallelArray.SortThreshold)


## 4.7. TParallelArray.For
`TParallelArray.For()` は配列データをスライスしてスレッド実行するパラメータ化メソッドです [^2]。

最もシンプルなものは次のようなパラメータを持ちます。実行する手続きは `TParallelArrayForProc` 型です。

```pascal
TParallelArray.For<T>(T 型の配列, 実行する手続きへの参照型);
```

例えば整数型の配列を伴って処理を行うには次のようになります。

```pascal
uses
  ..., System.SyncObjs, System.Threading;

  ...

procedure TForm1.Button1Click(Sender: TObject);
var
  IntArr: array of Integer;
begin
  SetLength(IntArr, 1000000);
  for var i:=Low(IntArr) to High(IntArr) do
    IntArr[i] := Random(100);

  // パラレル実行
  var Count := 0;
  var Value := 0;
  TParallelArray.For<Integer>(IntArr,
    procedure (const AValues: array of Integer; AFrom, ATo: NativeInt)
    begin
      TInterlocked.Add(Count, 1);  // この手続きが呼ばれた回数
      var v := 0;
      for var i := AFrom to ATo do // AFrom / ATo は毎回異なる
        Inc(v, AValues[i]);
      TInterlocked.Add(Value, v);
    end);
  Memo1.Lines.Add('Value: ' + Value.ToString);
  Memo1.Lines.Add('Count: ' + Count.ToString);
end;
```

`TParallelArray.For()` もメインスレッドで実行されると `TThread.Synchronize()` をブロックするので、処理によっては `TTask` を併用する必要があります。

```pascal
uses
  ..., System.SyncObjs, System.Threading;

  ...

procedure TForm1.Button1Click(Sender: TObject);
var
  IntArr: array of Integer;
begin
  SetLength(IntArr, 1000000);
  for var i:=Low(IntArr) to High(IntArr) do
    IntArr[i] := Random(100);

  // パラレル実行
  var Count := 0;
  var Value := 0;
  TTask.Run(procedure
    begin
      TParallelArray.For<Integer>(IntArr,
        procedure (const AValues: array of Integer; AFrom, ATo: NativeInt)
        begin
          TInterlocked.Add(Count, 1);  // この手続きが呼ばれた回数
          TThread.Synchronize(nil,
            procedure
            begin
              Memo1.Lines.Add(AFrom.ToString + ':' + ATo.ToString);
            end);
          var v := 0;
          for var i := AFrom to ATo do // AFrom / ATo は毎回異なる
            Inc(v, AValues[i]);
          TInterlocked.Add(Value, v);
        end);
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Lines.Add('Value: ' + Value.ToString);
          Memo1.Lines.Add('Count: ' + Count.ToString);
        end);
    end);
end;
```

**See also:**

 - [System.Threading.TParallelArray.For (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TParallelArray.For)
 - [System.Threading.TParallelArrayForProc (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TParallelArrayForProc)
 - [System.Threading.TParallelArray.ForThreshold (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Threading.TParallelArray.ForThreshold)

# 参考

 - [並列プログラミング ライブラリの使用 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E4%B8%A6%E5%88%97%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0_%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E4%BD%BF%E7%94%A8)
 - [RAD Studio XE7 - the new Parallel Programming Library (Youtube)](https://www.youtube.com/watch?v=x8S5b0peu7U)
 - [Parallel Programming Library (Delphi) (Youtube)](https://www.youtube.com/watch?v=Ni3JDxNFiiw)
 - [RAD Studio XE7の新機能 - パラレルライブラリ・動的配列 (Youtube)](https://www.youtube.com/watch?v=o5I4X5GU4Dk)
 - [コードサンプル: ライフゲーム (VCL) (docwiki)](https://docwiki.embarcadero.com/CodeExamples/en/RTL.ConwaysLifeVCL_Sample)
 - [コードサンプル: ライフゲーム (FireMonkey) (docwiki)](http://docwiki.embarcadero.com/codeExamples/en/RTL.ConwaysLifeFMX_Sample)
 - [タスク並列ライブラリ (TPL) (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/dotnet/standard/parallel-programming/task-parallel-library-tpl)
 - [CodeRage 9: Parallel Programming Library: Create Responsive Object Pascal Apps (Youtube)](https://www.youtube.com/watch?v=rZfux4by0po)
 - [Delphi - Multi threading (Chau Chee Yang Technical Blog)](http://chee-yang.blogspot.com/2015/12/delphi-multi-threading_4.html)
 - [Delphi の TTask でも ContinueWith がしたい！ (Qiita: @sagiri )](https://qiita.com/sagiri/items/e37533449f2b9be4664f)

# 索引
[ [← 3. スレッドローカル変数 (スレッドローカルストレージ)](https://qiita.com/ht_deko/items/09f04b5768135401eebb) ] [ [↑ 目次へ](https://qiita.com/items/e8c1ff3a4c74e4c2a4f3) ] [ [→ 5. サードパーティ製ライブラリ](https://qiita.com/ht_deko/items/e7f655bb73af05fbac35) ]

[^1]: `System.Threading` (PPL) は Delphi XE7 以降で利用可能です。
[^2]: `TParallelArray` は Delphi 12.2 Athens 以降で利用可能です。