TThreadedQueue は複数のコンシューマーに対応していませんか? 質問する

TThreadedQueue は複数のコンシューマーに対応していませんか? 質問する

TThreadedQueue (Generics.Collections) を単一プロデューサー複数コンシューマー スキームで使用しようとしています。(Delphi-XE) オブジェクトをキューにプッシュし、複数のワーカー スレッドでキューを空にするというのがその考え方です。

ただし、期待どおりには動作しません。2 つ以上のワーカー スレッドが PopItem を呼び出すと、TThreadedQueue からアクセス違反がスローされます。

PopItem への呼び出しがクリティカル セクションでシリアル化されている場合は、すべて正常です。

確かに TThreadedQueue は複数のコンシューマーを処理できるはずですが、何か見落としているのでしょうか、それともこれは TThreadedQueue の純粋なバグなのでしょうか?

以下はエラーを生成する簡単な例です。

program TestThreadedQueue;

{$APPTYPE CONSOLE}

uses
//  FastMM4 in '..\..\..\FastMM4\FastMM4.pas',
  Windows,
  Messages,
  Classes,
  SysUtils,
  SyncObjs,
  Generics.Collections;

type TThreadTaskMsg =
       class(TObject)
         private
           threadID  : integer;
           threadMsg : string;
         public
           Constructor Create( ID : integer; const msg : string);
       end;

type TThreadReader =
       class(TThread)
         private
           fPopQueue   : TThreadedQueue<TObject>;
           fSync       : TCriticalSection;
           fMsg        : TThreadTaskMsg;
           fException  : Exception;
           procedure DoSync;
           procedure DoHandleException;
         public
           Constructor Create( popQueue : TThreadedQueue<TObject>;
                               sync     : TCriticalSection);
           procedure Execute; override;
       end;

Constructor TThreadReader.Create( popQueue : TThreadedQueue<TObject>;
                                  sync     : TCriticalSection);
begin
  fPopQueue:=            popQueue;
  fMsg:=                 nil;
  fSync:=                sync;
  Self.FreeOnTerminate:= FALSE;
  fException:=           nil;

  Inherited Create( FALSE);
end;

procedure TThreadReader.DoSync ;
begin
  WriteLn(fMsg.threadMsg + ' ' + IntToStr(fMsg.threadId));
end;

procedure TThreadReader.DoHandleException;
begin
  WriteLn('Exception ->' + fException.Message);
end;

procedure TThreadReader.Execute;
var signal : TWaitResult;
begin
  NameThreadForDebugging('QueuePop worker');
  while not Terminated do
  begin
    try
      {- Calling PopItem can return empty without waittime !? Let other threads in by sleeping. }
      Sleep(20);
      {- Serializing calls to PopItem works }
      if Assigned(fSync) then fSync.Enter;
      try
        signal:= fPopQueue.PopItem( TObject(fMsg));
      finally
        if Assigned(fSync) then fSync.Release;
      end;
      if (signal = wrSignaled) then
      begin
        try
          if Assigned(fMsg) then
          begin
            fMsg.threadMsg:= '<Thread id :' +IntToStr( Self.threadId) + '>';
            fMsg.Free; // We are just dumping the message in this test
            //Synchronize( Self.DoSync);
            //PostMessage( fParentForm.Handle,WM_TestQueue_Message,Cardinal(fMsg),0);
          end;
        except
          on E:Exception do begin
          end;
        end;
      end;
      except
       FException:= Exception(ExceptObject);
      try
        if not (FException is EAbort) then
        begin
          {Synchronize(} DoHandleException; //);
        end;
      finally
        FException:= nil;
      end;
   end;
  end;
end;

Constructor TThreadTaskMsg.Create( ID : Integer; Const msg : string);
begin
  Inherited Create;

  threadID:= ID;
  threadMsg:= msg;
end;

var
    fSync : TCriticalSection;
    fThreadQueue : TThreadedQueue<TObject>;
    fReaderArr : array[1..4] of TThreadReader;
    i : integer;

begin
  try
    IsMultiThread:= TRUE;

    fSync:=        TCriticalSection.Create;
    fThreadQueue:= TThreadedQueue<TObject>.Create(1024,1,100);
    try
      {- Calling without fSync throws exceptions when two or more threads calls PopItem
         at the same time }
      WriteLn('Creating worker threads ...');
      for i:= 1 to 4 do fReaderArr[i]:= TThreadReader.Create( fThreadQueue,Nil);
      {- Calling with fSync works ! }
      //for i:= 1 to 4 do fReaderArr[i]:= TThreadReader.Create( fThreadQueue,fSync);
       WriteLn('Init done. Pushing items ...');

      for i:= 1 to 100 do fThreadQueue.PushItem( TThreadTaskMsg.Create( i,''));

      ReadLn;

    finally
      for i:= 1 to 4 do fReaderArr[i].Free;
      fThreadQueue.Free;
      fSync.Free;
    end;

  except
    on E: Exception do
      begin
        Writeln(E.ClassName, ': ', E.Message);
        ReadLn;
      end;
  end;
end.

アップデート: TThreadedQueue のクラッシュの原因となっていた TMonitor のエラーは、Delphi XE2 で修正されました。

アップデート2: 上記のテストでは、キューを空の状態にしてストレスをかけました。Darian Miller は、キューを満杯の状態にしてストレスをかけると、XE2 でエラーが再現されることを発見しました。このエラーも TMonitor にあります。詳細については、以下の回答を参照してください。また、QC101114 へのリンクもあります。

アップデート3TMonitor: Delphi-XE2 アップデート 4 では、の問題を修正するの修正が発表されましたTThreadedQueue。これまでのテストでは、 のエラーを再現できていませんTThreadedQueue。キューが空およびいっぱいのときに、単一のプロデューサー/複数のコンシューマー スレッドをテストしました。また、複数のプロデューサー/複数のコンシューマーもテストしました。リーダー スレッドとライター スレッドを 1 から 100 まで変化させましたが、問題はありませんでした。しかし、歴史を知っているので、 を破るのは他の人に任せたいと思いますTMonitor

ベストアンサー1

まあ、多くのテストを行わないと確信するのは難しいですが、これは TThreadedQueue または TMonitor のどちらかのバグであるように思われます。いずれにせよ、これは RTL にあり、コードではありません。これを QC レポートとして提出し、上記の例を「再現方法」コードとして使用する必要があります。

おすすめ記事