6

私は現在、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 コアで動作します。

4

1 に答える 1

3

このような状況に陥ったとき、私は Windows イベントを使用するのが好きです。Delphi では、WaitForSingleObject を使用する TEvent クラスを使用して公開されます。

したがって、Thread1NotActive と Thread2NotActive の 2 つのイベントを使用できます。Thread1 が完了すると、Thread2 によって待機される Thread1NotActive フラグが設定されます。逆に、Thread2 が処理を停止すると、Thread1 によって監視される Thread2NotActive が設定されます。

これにより、競合状態を回避することができ (1 つではなく 2 つのイベントを使用することをお勧めしているのはそのためです)、CPU 時間を過度に消費することなく、プロセスで正気を保つことができます。

より完全な例が必要な場合は、明日待つ必要があります:)

于 2014-11-14T18:36:51.283 に答える