2014-11-14 4 views
6

현재 두 개의 스레드를 번갈아 실행하고 서로 기다리게하는 최선의 방법을 찾고 있습니다.Delphi에서 두 스레드를 서로 동기화하는 가장 좋은 방법

(*) 낮은 CPU를 가진 것은

나는 내가 발견 한 문제를 보여주기 위해 몇 가지 데모 응용 프로그램에 함께 넣어 지금까지 세 가지 방법을 발견 비용 동안 빠른되는 최상의 조합.

일반적인 대기/펄스 패턴을 따르는 TMonitor를 사용하면 모든 잠금 때문에 성능이 좋지 않습니다 (SamplingProfiler는 이러한 기능에서 대부분의 시간을 소모합니다). 나는 Windows 이벤트 (SyncObjs.TEvent)를 사용하여 같은 것을 시도했지만 비슷한 (즉, 불량) 이벤트를 수행했다.

TThread.Yield를 호출하는 대기 루프를 사용하면 성능은 좋지만 분명히 CPU주기는 열중합니다. 전환 작업이 매우 빨라지더라도 문제가되지는 않지만 스레드가 실제로 기다리고있을 때 아프다 (데모에서 볼 수 있음).

TSpinWait을 사용하면 위의 세 가지 중에서 가장 좋지는 않지만 스위치가 매우 빠르게 작동하는 경우에만 수행됩니다. TSpinWait의 작동 방식 때문에 성능이 저하되는 데 오랜 시간이 걸립니다.

멀티 스레딩이 내 강점 중 하나가 아니기 때문에 두 가지 시나리오 (빠른 스위치와 느린 스위치) 모두에서 좋은 성능을 얻기 위해 이러한 방법 또는 일부 완전히 다른 접근 방법이 있는지 궁금합니다.

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. 

업데이트 : 이것은 섬유를 기반으로 구현했을 때보에만 약 5-6 배 느리게 수행

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; 

: 나는 이벤트의 조합과 spinwait 함께했다

Ping 호출간에 작업을 추가 할 때 빠른 전환 및 1 % 미만의 속도 저하가 발생합니다. 물론 광섬유를 사용할 때 단 하나가 아닌 2 개의 코어로 작동합니다.

+2

TMonitor로 할 수있는 몇 가지 조정 방법이 있습니다. [모니터 모니터링] (http://blogs.embarcadero.com/abauer/2013/08/23/38952) –

+0

섬유를 사용하는 방법을 살펴볼 수도 있습니다. –

+1

그것은 당신이 기대하는 것에 많이 의존합니다. 대부분 즉시 성공할 것인가 등등. 이상적으로는 몇 사이클 동안 회전하고 TMonitor 스타일 대기 상태로 전환하지 않는 것이 좋습니다. – Graymatter

답변

3

이런 상황에서 자신을 발견하면 Windows 이벤트를 사용하고 싶습니다. WaitForSingleObject 인 TEvent 클래스를 사용하여 Delphi에서 노출됩니다.

따라서 Thread1NotActive와 Thread2NotActive의 두 가지 이벤트를 사용할 수 있습니다. 일단 Thread1이 완료되면 Thread1에 의해 대기 된 Thread1NotActive 플래그를 설정합니다. 반대로 Thread2가 처리를 중지하면 Thread2가 모니터하는 Thread2NotActive를 설정합니다.

이것은 경쟁 조건 (1 대신 두 개의 이벤트 사용을 제안하는 이유)을 피할 수 있어야하며 과도한 양의 CPU 시간을 소비하지 않고 프로세스에서 정상적으로 유지해야합니다.

더 자세한 예제가 필요하면 내일 기다려야합니다.

+2

언급 했어야하지만 이미 시도해 봤어. 불행히도 성능은 TMonitor만큼 나쁩니다. –

관련 문제