5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DelphiAdvent Calendar 2021

Day 9

<3> スレッドローカル変数 (Delphi コンカレントプログラミング)

Last updated at Posted at 2021-12-08

3. スレッドローカル変数 (スレッドローカルストレージ)

各スレッドがスレッド固有の情報を保持できる記憶領域の事をスレッドローカルストレージ (TLS)といい、Delphi ではスレッドローカル変数として実装されています。いわゆる Thread-Specific Storage です。

See also:

3.1. threadvar

変数を宣言する時に var ではなく threadvar で宣言すると、スレッドローカル変数を宣言できます。

スレッドローカル変数には次のような制限があります。

  • 手続きまたは関数の内部では宣言できない (ユニットレベルでしか使えない)
  • 初期化を含めることができない
  • absolute 指令を指定できない
  • 長い文字列、ワイド文字列、動的配列、バリアント、インターフェイスなどの構造化型はスレッドが終了する前に使用したヒープ領域を解放する必要がある

スレッドローカル変数に構造化型を指定する事は推奨されていません。

3.1.1. threadvar の使い方

threadvar はスレッドでローカルとなるため、クリティカルセクションを設けて保護する必要はありません。もちろん、スレッドローカル変数の値を別のスレッドで参照する事もできません。

次のコードはフォームにメモとボタンが一つずつある想定です。実行してみるとスレッドローカル変数がどういう動きをするのか理解できると思います。

...

threadvar
  tvValue: Integer;

...

procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Clear;
  tvValue := 100; // メインスレッドの tvValue
  Memo1.Lines.Add('#1: ' +  tvValue.ToString); // メインスレッドの tvValue なので 100
  Inc(tvValue);   // メインスレッドの tvValue + 1 (101)

  // Thread A
  TThread.CreateAnonymousThread(
    procedure
    begin
      Inc(tvValue); // Thread A の tvValue + 1 (1)
      var dmyValue := tvValue;
      TThread.Synchronize(TThread.CurrentThread,
        procedure
        begin
          Memo1.Lines.Add('#2: ' + tvValue.ToString);  // メインスレッドの tvValue なので 101
          Memo1.Lines.Add('#3: ' + dmyValue.ToString); // Thread A の dmyValue の値なので 1
        end);
    end
  ).Start;

  // Thread B
  TThread.CreateAnonymousThread(
    procedure
    begin
      Inc(tvValue, 2); // Thread B の tvValue + 2 (2)
      var dmyValue := tvValue;
      TThread.Synchronize(TThread.CurrentThread,
        procedure
        begin
          Memo1.Lines.Add('#4: ' + tvValue.ToString);  // メインスレッドの tvValue なので 101
          Memo1.Lines.Add('#5: ' + dmyValue.ToString); // Thread B の dmyValue の値なので 2
        end);
    end
  ).Start;

  Memo1.Lines.Add('#6: ' +  tvValue.ToString); // メインスレッドの tvValue なので 101
end;

通常は threadvar でスレッドローカル変数を宣言せずに、スレッドクラスのフィールド (変数) を使うべきかと思います。

  • BeginThread() ルーチンを使った時
  • スレッドクラスにフィールドを追加したくない時 (あるの?)
  • コードブロックがどのスレッドで実行されているか判らない時のためのデバッグ用変数として

あえて threadvar を使わなくてはならないケースがあるとしたらこれくらいでしょうか?

DLL 内で threadvar を使うと問題が発生する事があります。
心当たりがある方は、後述するスレッドローカルストレージ API の利用を検討してください。

3.1.2. スレッドローカル変数が利用したヒープ領域の解放

先述の通り、スレッドローカル変数に構造化型を使った場合には自前で領域を解放する必要があります。次のようなコードはメモリリークを起こします。

threadvar
  sName : string;
  vData: variant;
  dArr: array of Integer;

...

  sName := 'John Doe';
  vData := 'Jane Doe';
  dArr := [100, 200];

Finalize() 手続きを使うとスレッドローカル変数が利用したヒープ領域を解放できます。

  Finalize(sName);
  Finalize(vData);
  Finalize(dArr);

See also:

3.1.3. class threadvar

ドキュメントには記載がありませんが、クラスフィールドとしての threadvar も利用可能です 1

  TMyThread = class(TThread)
  private
    { Private 宣言 }
  protected
    procedure Execute; override;
  public
    class threadvar
      RecordCount: Integer;
  end;

このように定義されたスレッドローカル変数 RecordCountpublic なので、インスタンス化していなくても TMyThread.RecordCount としてアクセスできます。

See also:

3.2. スレッドローカルストレージ API

Windows には (動的) スレッドローカルストレージ (Local Thread Storage) を扱うための API があります。これらの API の定義は Winapi.Windows にあります。

Windows API 説明
TlsAlloc() スレッドローカルストレージインデックスを割り当てる
TlsFree() スレッドローカルストレージインデックスを解放する
TlsGetValue() 指定のスレッドローカルストレージインデックスに設定されているメモリオブジェクトを返す
TlsSetValue() 指定のスレッドローカルストレージインデックスにメモリオブジェクトを設定する

各プロセスで利用可能な TLS スロットの最小数TLS_MINIMUM_AVAILABLE により定義されており、現在の Windows では 64 個となっています (最大で 1,088 個)。Windows 2000 よりも前の Windows の場合、TLS スロットは最大で 64 個でした。

Delphi では threadvar でスレッドローカル変数をいくつ宣言しても、たった一つの TLS スロットしか消費しない構造になっています。Delphi では独自で管理されるスレッドローカル変数用メモリブロックへのポインタとしてのみ TLS スロットを利用しています。

See also:

3.3. TLS 用グローバル変数

SysInit 内には TLS 用のグローバル変数が 2 つあります。詳細なドキュメントがないのですが、恐らく次のような意味合いだと思われます。

変数 意味
TlsIndex Delphi が使うスレッドローカルストレージ (TLS) のインデックス。
TlsLast この変数のアドレスは Delphi が使う TLS 用メモリブロックの最後を指します。TLS が未使用の場合には nil となります。

検証用のコードを次に示します。

program Project1;
{$APPTYPE CONSOLE}
uses
  System.SysUtils, System.Classes, WinAPI.Windows;

threadvar
  a: Integer;
  b: Integer;
  c: Integer;

begin
  var Idx := TlsAlloc;
  Writeln('AllocIndex:', Idx);
  TlsSetValue(Idx, Pointer(100));
  Writeln('AllocValue:', NativeInt(TlsGetValue(Idx)));
  TlsFree(Idx);

  TThread.CreateAnonymousThread(procedure
    begin
      a := 100;
      b := 200;
      // 変数を増やしたり減らしたりしてみる
    end);

  Writeln('TlsIndex:', SysInit.TlsIndex);
  Writeln('TlsLast:', NativeInt(Addr(SysInit.TlsLast)).ToHexString);
  Readln;
end.

上記コードでは TLS をストレージとして利用していますが、本来は Delphi がやっているように独自に管理されたメモリブロックへのポインタとして使うべきです。

See also:

参考

索引

[ ← 2. クリティカルセクションとロック ] [ ↑ 目次へ ] [ → 4. 並列プログラミングライブラリ (PPL) ]

  1. クラスフィールド class threadvar は Delphi XE3 以降で利用可能です。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?