【问题标题】:Gracefully terminating all the threads优雅地终止所有线程
【发布时间】:2017-11-22 07:56:44
【问题描述】:

我在我的一个解决方案中使用this

我的要求是在单击停止按钮时清除队列并优雅地终止所有线程。

为此我创建了一个ObjectList

var
  List: TObjectList<TMyConsumerItem>;
begin
  { Create a new List. }
  List := TObjectList<TMyConsumerItem>.Create();

后来我做了这个修改:

procedure TForm1.DoSomeJob(myListItems: TStringList);
...
for i := 1 to cThreadCount do
    List.Add(TMyConsumerItem.Create(aQueue, aCounter));

在停止按钮上单击我正在这样做

for i := 0 to List.Count - 1 do
  begin
    List.Item[i].Terminate;
  end;
  aCounter.Free;
  aQueue.Free;

在执行此操作时,我的应用程序被挂起。这是正确的方法还是我错过了什么?

我使用的是 10.2 Tokyo

编辑 1:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type

  TMyConsumerItem = class(TThread)
  private
    FQueue : TThreadedQueue<TProc>;
    FSignal : TCountDownEvent; 
  protected
    procedure Execute; override;
  public
    constructor Create( aQueue : TThreadedQueue<TProc>; aSignal : TCountdownEvent);
  end;


  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure StopClick(Sender: TObject);
  private
    { Private declarations }
    List: TObjectList<TMyConsumerItem>;
    aQueue: TThreadedQueue<TProc>;
    aCounter: TCountDownEvent;
    procedure DoSomeJob( myListItems : TStringList);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses
  SyncObjs, Generics.Collections;

{- Include TMyConsumerItem class here }

procedure TForm1.Button1Click(Sender: TObject);
var
  aList : TStringList;
  i : Integer;
begin
  aList := TStringList.Create;
  Screen.Cursor := crHourGlass;
  try
    for i := 1 to 20 do aList.Add(IntToStr(i));
    DoSomeJob(aList);
  finally
    aList.Free;
    Screen.Cursor := crDefault;
  end;
end;

procedure TForm1.StopClick(Sender: TObject);
begin
  for i := 0 to List.Count - 1 do
  begin
    List.Item[i].Terminate;
  end;
  List.Free;
  aCounter.WaitFor;
  aCounter.Free;
  aQueue.Free;
end;

procedure TForm1.DoSomeJob(myListItems: TStringList);
const
  cThreadCount = 10;
  cMyQueueDepth = 100;
var
  i: Integer;

  function CaptureJob(const aString: string): TProc;
  begin
    Result :=
      procedure
      var
        i,j : Integer;
      begin
        // Do some job with aString
        for i := 0 to 1000000 do
          j := i;
        // Report status to main thread
        TThread.Synchronize(nil,
          procedure
          begin
            Memo1.Lines.Add('Job with:'+aString+' done.');
          end
        );

      end;
  end;
var
  aThread : TThread;
begin
  List := TObjectList<TMyConsumerItem>.Create();
  List.OwnsObjects := False;
  aQueue := TThreadedQueue<TProc>.Create(cMyQueueDepth);
  aCounter := TCountDownEvent.Create(cThreadCount);
  try
    for i := 1 to cThreadCount do
       List.Add(TMyConsumerItem.Create(aQueue, aCounter));
    for i := 0 to myListItems.Count - 1 do
    begin
      aQueue.PushItem(CaptureJob(myListItems[i]));
    end;
  finally

  end;
end;


constructor TMyConsumerItem.Create(aQueue: TThreadedQueue<TProc>; aSignal : TCountDownEvent);
begin
 Inherited Create(false);
 Self.FreeOnTerminate := true;
 FQueue := aQueue;
 FSignal := aSignal;
end;

procedure TMyConsumerItem.Execute;
var
aProc : TProc;
begin
 try
 repeat
  FQueue.PopItem(aProc);
  aProc();
 until Terminated;
 finally
  FSignal.Signal;
 end;
end;
end.

【问题讨论】:

  • 你为什么不用TTask?

标签: multithreading delphi delphi-10.2-tokyo


【解决方案1】:

您遗漏了一些关于作业队列如何工作以及如何与线程池交互的重要内容。

  1. 引用自终止线程是错误的。删除List,因为它没用。
  2. 为了稍后完成队列,请将 aQueue 设为全局。
  3. 要完成线程池,请将与线程数一样多的空任务添加到队列中。
  4. 请参阅下面的示例如何实现停止方法。请注意,aCounteraQueue 在范围内都必须是全局的。免责声明未经测试,目前不在编译器面前。
  5. 如果您需要中止作业任务中正在进行的工作,则必须为每个作业任务提供对全局(范围内)标志的引用,并发出结束任务的信号。
  6. 还有其他库可以执行类似的工作,请参阅Delphi PPL 或经过充分验证的OTL library

procedure TForm1.StopClick(Sender: TObject);
var
  i : Integer;
  aThread : TThread;
begin
  // Kill the worker threads by pushing nil
  for i := 1 to cThreadCount do
    aQueue.PushItem(nil);

  // Since the worker threads synchronizes with the main thread,
  // we must wait for them in another thread.
  aThread := TThread.CreateAnonymousThread(
    procedure
    begin
      aCounter.WaitFor; // Wait for threads to finish
      aCounter.Free;
      aQueue.Free;
    end
  );
  aThread.FreeOnTerminate := false;
  aThread.Start;
  aThread.WaitFor;  // Safe to wait for the anonymous thread
  aThread.Free;
end;

【讨论】:

  • 谢谢。我更新了使 aQueue 成为全局的问题。因此,要中止正在进行的工作,我可以放置一个全局布尔变量,将其设置为停止按钮单击并在该布尔值设置为 true 时从 Thread.Exceute 中断?
  • 一个全局布尔值设置为 true 并在作业方法中定期检查以中止作业本身。您不需要中止线程,因为将空白作业送入队列会解决这个问题。
【解决方案2】:

Terminate 仅将 Terminated 属性设置为 true。重要的是线程的内部循环定期检查Terminated 属性,并在设置为true 时从Execute 方法返回。之后,在主线程中使用WaitFor,在释放队列或线程池对象之前检查线程是否已全部结束。

【讨论】:

  • 或者定义一个OnTerminate事件方法,在最后一个线程执行时释放对象。这将使应用程序不必通过调用WaitFor 来主动等待线程完成。
  • @Stijn Sanders - 是的,我正在检查 Thread.Excecute 中的 Terminated 属性。并且执行 WaitFor 仍然会挂起应用程序。
  • @LURD 是的,我会尝试的。所以要找到最后一个线程,我应该使用 aCounter.CurrentCount = 1。抱歉,语法可能是错误的,因为我现在无法访问我的 delphi 系统
  • @Bharat,是的,每次创建新线程时递增一个计数器,并在 OnTerminate 事件中递减计数器。当计数器为零时,释放对象,也许让用户知道它以某种方式完成。
  • @LURD 谢谢我明白了,它有帮助。我会实施的。您可以发表您的评论作为答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-02-14
  • 2015-06-07
  • 2012-03-16
  • 1970-01-01
  • 2019-11-10
  • 2011-01-04
  • 1970-01-01
相关资源
最近更新 更多