LoginSignup
8
2

More than 1 year has passed since last update.

<2> クリティカルセクションとロック (Delphi コンカレントプログラミング)

Last updated at Posted at 2021-12-07

2. クリティカルセクションとロック

クリティカルセクションとは、同時実行されると危険な領域の事を指します。クリティカルセクションを一つのスレッドだけが実行できるようにするのが、いわゆる Single Threaded Execution です。この Single Threaded Execution の事を Critical Section と呼ぶ事もあります。混乱を避けるため、当記事では次のような意味で使っています。

単語 意味
クリティカルセクション 同時実行されると危険な領域
Critical Section Single Threaded Execution な機構の事

TCriticalSection を使うと、マルチスレッドアプリケーションにおいて、あるスレッドが、コードのブロックへ他のスレッドがアクセスするのを一時的に防ぐことができます。

TCriticalSectionSystem.SyncObjs で定義されています 1

最初期のこのクラスは Windows API をラップしたものでした。

Delphi Windows API
TCriticalSection.Create() コンストラクタ InitializeCriticalSection()
TCriticalSection.Destroy デストラクタ DeleteCriticalSection()
TCriticalSection.Acquire() メソッド /
TCriticalSection.Enter() メソッド
EnterCriticalSection()
TCriticalSection.Release() メソッド /
TCriticalSection.Leave() メソッド
LeaveCriticalSection()

Windows API の Critical Section は次のような使い方になります。

...

uses
  ..., WinApi.Windows;

...

var
  CriticalSection: TRTLCriticalSection;
...


initialization
  InitializeCriticalSection(CriticalSection);
finalization
  DeleteCriticalSection(CriticalSection);
end.

最近の Delphi の TCriticalSection クラスはマルチプラットフォーム対応なので、Windows API を直接扱う事はあまりないかと思います。

2.1. TCriticalSection の使い方

TCriticalSection はインスタンス化して使いますが、各スレッドクラスの中でインスタンス化するコードを記述してはいけません。

...

uses
  ..., System.SyncObjs;

...

var
  CriticalSection: TCriticalSection;
...


initialization
  CriticalSection := TCriticalSection.Create;
finalization
  CriticalSection.Free;
end.

必ずグローバルな Critical Section 変数を定義する必要があります。

See also:

2.1.1. Critical Section 変数の使い方

Critical Section 変数を使ってみる前に、まずは「使わなかったらどうなるのか?」をテストしてみましょう。

グローバル変数 Counter の値を読み取って +1 し、Counter に書き戻すという処理をスレッド内で行います。そして、このスレッドを 100 回実行してみます。

・Critical Section 変数を使わない場合

VCL アプリケーションを新規作成し、フォームにボタンを一つ追加し、
image.png
次のようなコードを書きます。

unit1.pas
unit Unit1;

interface

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

type
  TMyThread = class(TThread)
  private
    { Private 宣言 }
  protected
    procedure Execute; override;
  end;

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

const
  NUM_OF_THREAD = 100;

var
  Form1: TForm1;
  Counter: Integer;
  TermCount: Integer;

implementation

{$R *.dfm}

procedure TForm1.MyThreadTerminate(Sender: TObject);
begin
  Inc(TermCount);                   // 終了したスレッドをカウント
  if TermCount = NUM_OF_THREAD then // すべてのスレッドが終了したら
    begin
      ShowMessage(Counter.ToString);  // Counter の値をダイアログで表示
      Button1.Enabled := True;
    end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Enabled := False;
  Counter := 0;
  TermCount := 0;
  for var i := 1 to NUM_OF_THREAD do
    with TMyThread.Create(True) do
      begin
        FreeOnTerminate := True;
        OnTerminate := MyThreadTerminate;
        Start;
      end;
end;

{ TMyThread }

procedure TMyThread.Execute;
begin
  inherited;
  var v := Counter;
  Inc(v);
  TThread.Sleep(20);
  Counter := v;
end;

end.
Unit1.dfm
object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 441
  ClientWidth = 624
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -12
  Font.Name = 'Segoe UI'
  Font.Style = []
  PixelsPerInch = 96
  TextHeight = 15
  object Button1: TButton
    Left = 24
    Top = 24
    Width = 80
    Height = 25
    Caption = 'Button1'
    TabOrder = 0
    OnClick = Button1Click
  end
end

これをコンパイルして実行し、ボタンを押してみます。
image.png
表示される値はバラバラだと思いますが、すくなくとも 100 にはならないと思います。

・Critical Section 変数を使った場合

次に、Critical Section 変数を使うように書き換えてみます。コードが一部省略されている事に注意してください。

unit1.pas
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.SyncObjs {追加};

type
  TMyThread = class(TThread)
  private
    { Private 宣言 }
  protected
    procedure Execute; override;
  end;

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

const
  NUM_OF_THREAD = 100;

var
  Form1: TForm1;
  Counter: Integer;
  TermCount: Integer;
  CriticalSection: TCriticalSection; // 追加

implementation

{$R *.dfm}

procedure TForm1.MyThreadTerminate(Sender: TObject); [...]

procedure TForm1.Button1Click(Sender: TObject); [...]

{ TMyThread }

procedure TMyThread.Execute;
begin
  inherited;
  CriticalSection.Acquire;   // または CriticalSection.Enter
  try
    var v := Counter;
    Inc(v);
    TThread.Sleep(20);
    Counter := v;
  finally
    CriticalSection.Release; // または CriticalSection.Leave
  end;
end;

initialization
  CriticalSection := TCriticalSection.Create;
finalization
  CriticalSection.Free;
end.

そして実行してみます。
image.png
Sleep() のパラメータを大きくしても小さくしても常に 100 が返ってきます。

2.2. TRTLCriticalSectionHelper

プラットフォーム依存ですが、System.SyncObjs には TRTLCriticalSectionHelper というレコードヘルパーが存在し 2 TRTLCriticalSection レコードを TCriticalSection クラスのように使う事ができます。

...

uses
  ..., System.SyncObjs;

...

var
  CriticalSection: TRTLCriticalSection;
...


initialization
  CriticalSection.Initialize;
finalization
  CriticalSection.Destroy; // または CriticalSection.Free
end.

Enter() メソッドや Leave() メソッドも用意されています。

procedure TMyThread.Execute;
begin
  inherited;
  CriticalSection.Enter;
  try
    var v := Counter;
    Inc(v);
    TThread.Sleep(20);
    Counter := v;
  finally
    CriticalSection.Free;
  end;
end;

See also:

2.3. System.TMonitor

モニタとは

  • データを格納する構造体
  • 同期機構
  • それらを操作するメソッド

を一つにまとめたもので、オブジェクト指向プログラミング (OOP) をサポートする言語だと、クラスにマルチスレッド (やマルチプロセス) の機能を持たせたものがそれに該当します。

TMonitor レコード 3TCriticalSection と同じような使い方ができますが、保護する対象がオブジェクト (TObject の派生インスタンス) となります。保護する対象のオブジェクトは、単純な (レコードのような) クラスで構いません。

type
  TCounter = class
    FCounter: Integer;
  end;
...

var
  Counter: TCounter;
...

initialization
  Counter := TCounter.Create;
finalization
  Counter.Free;
end.

ロック / 解除も単に Enter() / Exit() を呼び出すだけで行えます。

procedure TMyThread.Execute;
begin
  inherited;
  System.TMonitor.Enter(Counter);
  try
    var v := Counter.FCounter;
    Inc(v);
    TThread.Sleep(20);
    Counter.FCounter := v;
  finally
    System.TMonitor.Exit(Counter)
  end;
end;

モニタの概念からすると、レコードではなく抽象クラスで実装され、サブクラス化して使うようになっていれば名前と機能が一致していたと思うのですが...。

VCL フォームアプリケーションで TMonitor を使う際には System.TMonitor のように修飾しないとエラーになります。VCL にも Vcl.Forms.TMonitor が存在するからです。
image.png
この問題を解決するために System.MonitorEnter() および System.MonitorExit() ルーチンが用意されています。

procedure TMyThread.Execute;
begin
  inherited;
  MonitorEnter(Counter);
  try
    var v := Counter.FCounter;
    Inc(v);
    TThread.Sleep(20);
    Counter.FCounter := v;
  finally
    MonitorExit(Counter)
  end;
end;

TMonitor は XE4 以前でパフォーマンスの問題を抱えています。

See also:

2.4. TSpinLock

System.SyncObjs で定義されている TSpinLock レコード 4 は .NET の SpinLock 構造体と互換性があります。

いわゆる Guarded Suspension です。意訳すると"駅前ロータリーロック"です。電車が到着して、もうすぐ彼女 (or 彼氏) が駅から出てくるはずなのでロータリーを車でグルグル回って待つという手法です。一本後の電車に乗ったとかだとこの待ち合わせは失敗します。ロータリーを回っていられるのはごく短い時間だけです。

理屈的にスピンロックをロックの最初の候補として利用すべきではなく、他の方法で正しく動作している場合のパフォーマンスチューニング手段だと考えた方がいいように思います。

See also:

2.5. TInterlocked

System.SyncObjs で定義されている TInterlocked クラス 5 では、値のインクリメントやデクリメント、値の交換などを Critical Section を使うことなく簡単に行えます。.NET の Interlocked クラスと互換性があります。

Delphi の TInterlocked クラスには次のようなクラスメソッドがあります。

メソッド 説明
Add() パラメータに指定された整数型の値を加算し、その結果を返します。
Increment() パラメータに指定された整数型の値をインクリメントし、その結果を返します。
Decrement() パラメータに指定された整数型の値をデクリメントし、その結果を返します。
Exchange() 指定されたパラメータの値を交換し、元の値を返します。
Read() パラメータに指定された Int64 型変数の値を返します。

カウンタとして使うのがとても簡単です。

...

uses
  ..., System.SyncObjs;

...

var
  Counter: Int64;
...

procedure TMyThread.Execute;
begin
  inherited;
  var v1 := TInterlocked.Read(Counter);
  var v2 := v1;
  Inc(v1);
  TThread.Sleep(20);
  TInterlocked.Add(Counter, v1 - v2); // 差分を加算
end;

See also:

2.6. TMultiReadExclusiveWriteSynchronizer

System.SysUtils で定義されている TMultiReadExclusiveWriteSynchronizer クラス 6複数読み出し排他的書き込みシンクロナイザ (MREWS) とよばれます。名前が長いので TMREWSync というエイリアスも用意されています 7

いわゆる Readers–writer lock です。意訳すると "生徒-先生ロック" です。板書するのは先生だけで、生徒は黒板を見てるだけです。黒板を見るのは生徒が同時に行えますが (Multi-read)、板書は先生だけしか行えません。実際の所、先生は複数人居てもいいのですが、板書できる先生は一人だけです (Exclusive-write)。先生たちが同時に板書する事はできません。

理屈的に生徒 (Readers)先生 (Writer) のスレッドは分離する必要があります。

古くからあるのにあまり使われていないのは、(Delphi の場合) 機構が複雑になる割に Critical Section よりもパフォーマンスが劣る事が多いからだと思います。

See also:

2.7. TSimpleRWSync

System.SysUtils で定義されている TSimpleRWSync クラス 8 は、古い Delphi だと CriticalSection API のラッパーで、現在の Delphi だと TMonitor のラッパーです。

BeginRead()BeginWrite()EndRead()EndWrite() はそれぞれ同じ動作になります。つまりは Critical Section であって、Readers–writer lock ではありません。

TMultiReadExclusiveWriteSynchronizer は Windows 以外のプラットフォームだと TSimpleRWSync のエイリアスになっています。

See also:

2.8. TLightweightMREW

System.SyncObjs で定義されている TLightweightMREW レコード 9TMultiReadExclusiveWriteSynchronizer よりも高速で軽量な新しい Readers–writer lock の実装です。

TLightweightMREW は、各プラットフォームにおける Readers–writer lock のネイティブ実装をラッピングしています。例えば Windows の場合には SRW ロック 10 のラッパーとなっています。

See also:

参考

索引

[ ← 1. スレッドオブジェクト ] [ ↑ 目次へ ] [ → 3. スレッドローカル変数 (スレッドローカルストレージ) ]


  1. TCriticalSection は Delphi 3 以降で利用可能です。 

  2. TRTLCriticalSectionHelper は Delphi 2009 以降で利用可能です。 

  3. TMonitor は Delphi 2009 以降で利用可能です。 

  4. TSpinLock は Delphi XE 以降で利用可能です。 

  5. TInterlocked は Delphi XE 以降で利用可能です。 

  6. TMultiReadExclusiveWriteSynchronizer は Delphi 4 以降で利用可能です。 

  7. TMREWSync は Delphi 6 以降で利用可能です。 

  8. TSimpleRWSync は Delphi 6 以降で利用可能です。 

  9. TLightweightMREW は Delphi 10.4.1 Sydney 以降で利用可能です。 

  10. SRW ロック は Windows Vista 以降で利用可能です。 

8
2
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
8
2