2. クリティカルセクションとロック
クリティカルセクションとは、同時実行されると危険な領域の事を指します。クリティカルセクションを一つのスレッドだけが実行できるようにするのが、いわゆる Single Threaded Execution です。この Single Threaded Execution の事を Critical Section と呼ぶ事もあります。混乱を避けるため、当記事では次のような意味で使っています。
単語 | 意味 |
---|---|
クリティカルセクション | 同時実行されると危険な領域 |
Critical Section | Single Threaded Execution な機構の事 |
TCriticalSection
を使うと、マルチスレッドアプリケーションにおいて、あるスレッドが、コードのブロックへ他のスレッドがアクセスするのを一時的に防ぐことができます。
TCriticalSection
は System.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 アプリケーションを新規作成し、フォームにボタンを一つ追加し、
次のようなコードを書きます。
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.
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
これをコンパイルして実行し、ボタンを押してみます。
表示される値はバラバラだと思いますが、すくなくとも 100 にはならないと思います。
・Critical Section 変数を使った場合
次に、Critical Section 変数を使うように書き換えてみます。コードが一部省略されている事に注意してください。
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.
そして実行してみます。
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:
- System.SyncObjs.TCriticalSectionHelper (DocWiki)
- Critical Section Objects (クリティカル セクション オブジェクト (docs.microsoft.com))
2.3. System.TMonitor
モニタとは
- データを格納する構造体
- 同期機構
- それらを操作するメソッド
を一つにまとめたもので、オブジェクト指向プログラミング (OOP) をサポートする言語だと、クラスにマルチスレッド (やマルチプロセス) の機能を持たせたものがそれに該当します。
TMonitor
レコード 3 は TCriticalSection
と同じような使い方ができますが、保護する対象がオブジェクト (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
が存在するからです。
この問題を解決するために 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:
- System.System.SyncObjs.TSpinLock (DocWiki)
- スピンロック (Wikipedia)
- SpinLock 構造体 (docs.microsoft.com)
- Guarded suspension (Wikipedia: en)
- 「Java言語で学ぶデザインパターン入門マルチスレッド編」を Delphi に移植してみた (ScriptBrowserK)
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:
- System.System.SyncObjs.TInterlocked (DocWiki)
- Interlocked クラス (docs.microsoft.com)
- インタロック変数アクセス (docs.microsoft.com)
- アトミック関数 (docs.microsoft.com)
2.6. TMultiReadExclusiveWriteSynchronizer
System.SysUtils
で定義されている TMultiReadExclusiveWriteSynchronizer
クラス 6 は 複数読み出し排他的書き込みシンクロナイザ (MREWS) とよばれます。名前が長いので TMREWSync
というエイリアスも用意されています 7。
いわゆる Readers–writer lock です。意訳すると "生徒-先生ロック" です。板書するのは先生だけで、生徒は黒板を見てるだけです。黒板を見るのは生徒が同時に行えますが (Multi-read)、板書は先生だけしか行えません。実際の所、先生は複数人居てもいいのですが、板書できる先生は一人だけです (Exclusive-write)。先生たちが同時に板書する事はできません。
理屈的に生徒 (Readers)
と先生 (Writer)
のスレッドは分離する必要があります。
古くからあるのにあまり使われていないのは、(Delphi の場合) 機構が複雑になる割に Critical Section よりもパフォーマンスが劣る事が多いからだと思います。
See also:
- System.SysUtils.TMultiReadExclusiveWriteSynchronizer (DocWiki)
- 複数読み出し排他的書き込みシンクロナイザを使用する
- Readers–writer lock (Wikipedia: en)
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
レコード 9 は TMultiReadExclusiveWriteSynchronizer
よりも高速で軽量な新しい Readers–writer lock の実装です。
TLightweightMREW
は、各プラットフォームにおける Readers–writer lock のネイティブ実装をラッピングしています。例えば Windows の場合には SRW ロック 10 のラッパーとなっています。
See also:
- System.SyncObjs.TLightweightMREW (DocWiki)
- 10.4.1 の新機能: 新しい TLightweightMREW レコード (blogs.embarcadero.com)
- Slim Reader/Writer (SRW) Locks (docs.microsoft.com)
参考
- Delphi で Singleton パターンを実装する (Monitor 版) (Owl's perspective)
- 詳説!DataSnap 2010 (第15回 エンバカデロ・デベロッパーキャンプ【A5】)
- Multithreading - The Delphi Way. (Martin Harvey)
- THttpClient の落とし穴2 (Qiita: @pik)
- [C++Builder] TMultiReadExclusiveWriteSynchronizerでRead-Write Lockパターン (山本隆の開発日誌)
- .NETマルチスレッド・プログラミング入門 (@IT)
- Readers-writer lock - Part 1: Why? (The Delphi Geek)
- Readers-writer lock - Part 2: Implementation (The Delphi Geek)
- Readers-writer lock - Part 3: Some numbers (The Delphi Geek)
- Readers-writer lock - Part 4: Improving TLightweightMREW (The Delphi Geek)
索引
[ ← 1. スレッドオブジェクト ] [ ↑ 目次へ ] [ → 3. スレッドローカル変数 (スレッドローカルストレージ) ]
-
TCriticalSection
は Delphi 3 以降で利用可能です。 ↩ -
TRTLCriticalSectionHelper
は Delphi 2009 以降で利用可能です。 ↩ -
TMonitor
は Delphi 2009 以降で利用可能です。 ↩ -
TSpinLock
は Delphi XE 以降で利用可能です。 ↩ -
TInterlocked
は Delphi XE 以降で利用可能です。 ↩ -
TMultiReadExclusiveWriteSynchronizer
は Delphi 4 以降で利用可能です。 ↩ -
TMREWSync
は Delphi 6 以降で利用可能です。 ↩ -
TSimpleRWSync
は Delphi 6 以降で利用可能です。 ↩ -
TLightweightMREW
は Delphi 10.4.1 Sydney 以降で利用可能です。 ↩ -
SRW ロック
は Windows Vista 以降で利用可能です。 ↩