TThreadedQueue不具备多个消费者的能力?

LU *_* RD 43 delphi queue delphi-xe delphi-xe2

尝试在单个生产者多个消费者方案中使用TThreadedQueue(Generics.Collections).(DELPHI-XE).我们的想法是将对象推入队列,让几个工作线程排空队列.

但它没有按预期工作.当两个或多个工作线程调用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.
Run Code Online (Sandbox Code Playgroud)

更新:导致TThreadedQueue崩溃的TMonitor中的错误在Delphi XE2中得到修复.

更新2:上面的测试强调队列处于空状态.Darian Miller发现强调队列处于满状态,仍然可以重现XE2中的错误.错误再一次出现在TMonitor中.有关详细信息,请参阅下面的答案.还有QC101114的链接.

更新3:随着Delphi-XE2更新4,有一个宣布的解决方案TMonitor将解决问题TThreadedQueue.到目前为止,我的测试无法重现任何错误TThreadedQueue.当队列为空且已满时,测试单个生产者/多个使用者线程.还测试了多个生产者/多个消费者.我将读取器线程和写入器线程从1改为100而没有任何故障.但是知道历史,我敢于让别人打破TMonitor.

Mas*_*ler 19

好吧,没有大量的测试很难确定,但看起来这确实是一个错误,无论是在TThreadedQueue还是在TMonitor中.无论哪种方式,它都在RTL而不是您的代码中.您应该将此文件作为QC报告提交,并使用上面的示例作为"如何重现"代码.

  • QC#91246 TThreadedQueue因多个消费者而失败.如果你愿意,可以投票给它. (7认同)
  • 与QCReport相关:[http://qc.embarcadero.com/wc/qcmain.aspx?d=91246 ](http://qc.embarcadero.com/wc/qcmain.aspx?d = 91246) (5认同)
  • 似乎没有修复 - 添加社区维基答案,通过调整示例代码在XE2中生成AV (3认同)

RBA*_*RBA 10

我建议你在使用线程,并行性等时使用OmniThreadLibrary http://www.thedelphigeek.com/search/label/OmniThreadLibrary.Primoz做得非常好,在网站上你会发现很多有用的文档.