私は現在、2 つのスレッドを交互に実行し、それらを互いに待機させるための最良の (*) 方法を見つけようとしています。
(*) 低 CPU コストで高速であることの最適な組み合わせ
これまでに見つけた 3 つの方法をデモ アプリケーションにまとめて、見つけた問題を示しました。
古典的な待機/パルス パターンに従って TMonitor を使用すると、すべてのロックが原因でパフォーマンスが低下します (SamplingProfiler によると、ほとんどの場合、これらの関数で焼き付きます)。Windows イベント (SyncObjs.TEvent) を使用して同じことを試みましたが、同じように実行されました (つまり、悪い結果でした)。
TThread.Yield を呼び出す待機ループを使用すると、最高のパフォーマンスが得られますが、明らかに CPU サイクルが狂ったように消費されます。切り替えが非常に迅速に行われるかどうかは問題ではありませんが、スレッドが実際に待機している場合は問題になります (デモで確認できます)。
TSpinWait を使用すると (これら 3 つの中で最高ではないにしても) 優れたパフォーマンスが得られますが、切り替えが非常に迅速に行われる場合に限られます。切り替えに時間がかかるほど、TSpinWait の動作が原因でパフォーマンスが低下します。
マルチスレッドは私の強みの 1 つではないので、両方のシナリオ (高速スイッチと低速スイッチ) で優れたパフォーマンスを達成するために、これらの方法の組み合わせまたはまったく異なるアプローチがあるかどうか疑問に思っていました。
program PingPongThreads;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Classes,
Diagnostics,
SyncObjs,
SysUtils;
type
TPingPongThread = class(TThread)
private
fCount: Integer;
protected
procedure Execute; override;
procedure Pong; virtual;
public
procedure Ping; virtual;
property Count: Integer read fCount;
end;
TPingPongThreadClass = class of TPingPongThread;
TMonitorThread = class(TPingPongThread)
protected
procedure Pong; override;
procedure TerminatedSet; override;
public
procedure Ping; override;
end;
TYieldThread = class(TPingPongThread)
private
fState: Integer;
protected
procedure Pong; override;
public
procedure Ping; override;
end;
TSpinWaitThread = class(TPingPongThread)
private
fState: Integer;
protected
procedure Pong; override;
public
procedure Ping; override;
end;
{ TPingPongThread }
procedure TPingPongThread.Execute;
begin
while not Terminated do
Pong;
end;
procedure TPingPongThread.Ping;
begin
TInterlocked.Increment(fCount);
end;
procedure TPingPongThread.Pong;
begin
TInterlocked.Increment(fCount);
end;
{ TMonitorThread }
procedure TMonitorThread.Ping;
begin
inherited;
TMonitor.Enter(Self);
try
if Suspended then
Start
else
TMonitor.Pulse(Self);
TMonitor.Wait(Self, INFINITE);
finally
TMonitor.Exit(Self);
end;
end;
procedure TMonitorThread.Pong;
begin
inherited;
TMonitor.Enter(Self);
try
TMonitor.Pulse(Self);
if not Terminated then
TMonitor.Wait(Self, INFINITE);
finally
TMonitor.Exit(Self);
end;
end;
procedure TMonitorThread.TerminatedSet;
begin
TMonitor.Enter(Self);
try
TMonitor.Pulse(Self);
finally
TMonitor.Exit(Self);
end;
end;
{ TYieldThread }
procedure TYieldThread.Ping;
begin
inherited;
if Suspended then
Start
else
fState := 3;
while TInterlocked.CompareExchange(fState, 2, 1) <> 1 do
TThread.Yield;
end;
procedure TYieldThread.Pong;
begin
inherited;
fState := 1;
while TInterlocked.CompareExchange(fState, 0, 3) <> 3 do
if Terminated then
Abort
else
TThread.Yield;
end;
{ TSpinWaitThread }
procedure TSpinWaitThread.Ping;
var
w: TSpinWait;
begin
inherited;
if Suspended then
Start
else
fState := 3;
w.Reset;
while TInterlocked.CompareExchange(fState, 2, 1) <> 1 do
w.SpinCycle;
end;
procedure TSpinWaitThread.Pong;
var
w: TSpinWait;
begin
inherited;
fState := 1;
w.Reset;
while TInterlocked.CompareExchange(fState, 0, 3) <> 3 do
if Terminated then
Abort
else
w.SpinCycle;
end;
procedure TestPingPongThread(threadClass: TPingPongThreadClass; quickSwitch: Boolean);
const
MAXCOUNT = 10000;
var
t: TPingPongThread;
i: Integer;
sw: TStopwatch;
w: TSpinWait;
begin
t := threadClass.Create(True);
try
for i := 1 to MAXCOUNT do
begin
t.Ping;
if not quickSwitch then
begin
// simulate some work
w.Reset;
while w.Count < 20 do
w.SpinCycle;
end;
if i = 1 then
begin
if not quickSwitch then
begin
Writeln('Check CPU usage. Press <Enter> to continue');
Readln;
end;
sw := TStopwatch.StartNew;
end;
end;
Writeln(threadClass.ClassName, ' quick switches: ', quickSwitch);
Writeln('Duration: ', sw.ElapsedMilliseconds, ' ms');
Writeln('Call count: ', t.Count);
Writeln;
finally
t.Free;
end;
end;
procedure Main;
begin
TestPingPongThread(TMonitorThread, False);
TestPingPongThread(TYieldThread, False);
TestPingPongThread(TSpinWaitThread, False);
TestPingPongThread(TMonitorThread, True);
TestPingPongThread(TYieldThread, True);
TestPingPongThread(TSpinWaitThread, True);
end;
begin
try
Main;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Writeln('Press <Enter> to exit');
Readln;
end.
アップデート:
イベントとスピンウェイトの組み合わせを思いついた:
constructor TSpinEvent.Create;
begin
inherited Create(nil, False, False, '');
end;
procedure TSpinEvent.SetEvent;
begin
fState := 1;
inherited;
end;
procedure TSpinEvent.WaitFor;
var
startCount: Cardinal;
begin
startCount := TThread.GetTickCount;
while TInterlocked.CompareExchange(fState, 0, 1) <> 1 do
begin
if (TThread.GetTickCount - startCount) >= YieldTimeout then // YieldTimeout = 10
inherited WaitFor(INFINITE)
else
TThread.Yield;
end;
end;
これは、迅速な切り替えを行う場合、ファイバーベースの実装よりも約 5 ~ 6 倍遅く、Ping 呼び出しの間に何らかの作業を追加する場合は 1% 未満しか遅くなりません。もちろん、ファイバーを使用する場合は、1 コアだけではなく 2 コアで動作します。