1

我有一个来自 TThread 的孩子。一切正常,但我怎样才能对我创建的线程进行大规模挂起或恢复?或者我怎样才能只挂起第二个线程(在 Button2Click 中创建)?这是我的代码的一部分:

TMyThread = class(TThread)
private
  source_file, destination_file: string;
  total_size, current_size, download_item_id: integer;
protected
  procedure ShowResult;
  procedure Execute; override;
public
end;

var
 MyThread: TMyThread;

begin

procedure TMyThread.Execute;
 begin
  //Some code for download file here, it doesn't matter
 end;


procedure TForm1.Button1Click(Sender: TObject);
begin
  MyThread := TMyThread.Create(True);
  MyThread.source_file :='http://example.com/download1.zip';
  MyThread.destination_file := 'c:\download1.zip';
  MyThread.download_item_id := 0;
  MyThread.Priority := tpNormal;
  MyThread.FreeOnTerminate := True;
  MyThread.Resume;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  MyThread := TMyThread.Create(True);
  MyThread.source_file :='http://example.com/download2.zip';
  MyThread.destination_file := 'c:\download2.zip';
  MyThread.download_item_id := 1;
  MyThread.Priority := tpNormal;
  MyThread.FreeOnTerminate := True;
  MyThread.Resume;
end;

end.

也就是说,如果我创建这样的线程 - 它对我有用:

var
 MyThread1, MyThread2: TMyThread;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
  MyThread1 := TMyThread.Create(True);
  MyThread1.source_file :='http://example.com/download1.zip';
  MyThread1.destination_file := 'c:\download1.zip';
  MyThread1.download_item_id := 0;
  MyThread1.Priority := tpNormal;
  MyThread1.FreeOnTerminate := True;
  MyThread1.Resume;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  MyThread2 := TMyThread.Create(True);
  MyThread2.source_file :='http://example.com/download2.zip';
  MyThread2.destination_file := 'c:\download2.zip';
  MyThread2.download_item_id := 1;
  MyThread2.Priority := tpNormal;
  MyThread2.FreeOnTerminate := True;
  MyThread2.Resume;
end;

//Terminate all of TMyThread
procedure TForm1.Button3Click(Sender: TObject);
begin
  MyThread1.Terminate;
  MyThread2.Terminate;
  ShowMessage('All downloads were terminated!');
end;

//Terminate ONLY the second of TMyThread
procedure TForm1.Button4Click(Sender: TObject);
begin
  MyThread2.Terminate;
  ShowMessage('The second download was terminated!');
end;

但是如何为一组动态创建的 TMyThread(如第一个代码示例)做到这一点?

4

2 回答 2

8

局限性

你真的,真的不应该保留对在终止时设置为空闲的线程的引用。它要求各种各样的问题。下面的代码确实使用了 FreeOnTerminate 设置为 True 的线程。这只是安全的,因为:1)在线程终止和释放之前,引用被删除;2) 在主线程的上下文中调用 OnTerminate 处理程序和/或使用线程安全的 TList 后代;并且 - 最重要的是 - 3) 引用仅用于从主线程的上下文中选择性地取消线程。

一旦您想将引用用于其他任何事情(如暂停和恢复)或可以从主线程以外的任何线程的上下文中访问它们,您确实应该更深入地研究线程程序执行的各种机制。

几个关键字:线程同步、事件信号、互斥体、信号量、临界区。

使用 Delphi 进行多线程编程的一个很好的参考是Multithreading - The Delphi Way

回答

如果您以后确实需要引用线程,则确实需要将引用存储在某处。但是您不需要为您创建的每个线程都有单独的变量。有很多选择。我想到了一个数组,但最简单的可能是一个对象列表(来自Contnrs单元):

TForm1 = class(TForm)
  private
    MyThreads: TObjectList;

constructor TForm1.Create(AOwner: TComponent);
begin
  inherited;
  MyThreads := TObjectList.Create;
end;

destructor TForm1.Destroy;
begin
  MyThreads.Free;
  inherited;
end;

TObjectList 有一个OwnsObjects属性,默认情况下是True. 这意味着释放对象列表也将释放它包含的实例。这意味着您需要确保列表仅包含有效的引用。通常这不是问题,但是对于线程,您需要特别小心,尤其是当您使用FreeOnTerminateset to时True

像在第一个示例中一样创建线程,并进行一些更改:

使用局部变量(您应该避免使用全局变量,即使它们尽可能只在单元内可见)。

将线程添加到您的对象列表中。

当您还想使用设置为 True 的“FreeOnTerminate”时,设置 OnTerminate 处理程序以从对象列表中删除实例。

procedure TForm1.Button1Click(Sender: TObject);
var
  Thread: TThread;                                       // Local var
begin
  Thread := TMyThread.Create(True);
  MyThreads.Add(Thread);                                 // Add to list
  Thread.source_file :='http://example.com/download1.zip';
  Thread.destination_file := 'c:\download1.zip';
  Thread.download_item_id := 0;
  Thread.Priority := tpNormal;
  Thread.OnTerminate := HandleThreadTerminate;           // Ensure you keep list valid
  Thread.FreeOnTerminate := True;                        // Advice against this!
  Thread.Start;
end;

procedure TForm1.HandleThreadTerminate(Sender: TObject);
var
  idx: Integer;
begin
  // Acquire Lock to protect list access from two threads
  idx := MyThreads.IndexOf(Sender);
  if idx > -1 then
    MyThreads.Delete(idx);
  // Release lock
end;

HandleThreadTerminate 过程应该使用某种锁,因此两个线程不能同时尝试从列表中删除实例,因为在 IndexOf 和 Delete 之间切换可能意味着删除错误的实例。处理程序中的代码可以写成MyThreads.Remove(Sender). 尽管这是一条语句,但它不是线程安全的,因为它与我在幕后展示的代码相同。

编辑:实际上,正如@LURD 提到的那样,它恰好是线程安全的,因为在主线程的上下文中调用了 OnTerminate。我将离开 TThreadList 示例,因为如果您需要/碰巧从多个线程访问列表,这是一种很好的方法。

要自动处理锁定内容,您可以使用TThreadList(来自classes单元)。所需的代码更改:

当然你需要实例化一个 TThreadList 而不是 TObjectList。

constructor TForm1.Create(AOwner: TComponent);
begin
  inherited;
  MyThreads := TThreadList.Create;
end;

因为 TThreadList 没有 的概念OwnsObjects,所以您必须自己释放任何剩余的线程:

destructor TForm1.Destroy;
var
  LockedList: TList;
  idx: integer;
begin
  LockedList := MyThreads.LockList;
  try
    for idx := LockedList.Count - 1 downto 0 do
      TObject(MyThreads.Items(idx)).Free;
  finally
    MyThreads.UnlockList;
  end;

  MyThreads.Free;
  inherited;
end;

现在可以安全地简化 OnTerminate 处理程序,因为 TThreadList 将负责所需的锁定。

procedure TForm1.HandleThreadTerminate(Sender: TObject);
begin
  MyThreads.Remove(idx);
end;

编辑:正如@mghie 所提到的,当 FreeOnTerminate 为真时释放线程与终止时释放的整个想法相冲突。您可以在释放线程(这将终止它)之前将 FreeOnTerminate 设置为 false,或者如果您想保持 FreeOnTerminate 处理,您应该断开 OnTerminate 处理程序,然后使用 Terminate 来告诉线程终止。

destructor TForm1.Destroy;
var
  LockedList: TList;
  idx: integer;
begin
  LockedList := MyThreads.LockList;
  try
    for idx := LockedList.Count - 1 downto 0 do
    begin
      TThread(MyThreads.Items(idx)).OnTerminate := nil;
      TThread(MyThreads.Items(idx)).Terminate;
    end;
  finally
    MyThreads.UnlockList;
  end;

  MyThreads.Free;
  inherited;
end;
于 2012-12-16T11:31:56.670 回答
1

答案很简单——如果你创建了一个 FreeOnTerminate 类型的 TThread,那么你根本就不会使用任何外部引用到那个 TThread 实例。这样做就像玩俄罗斯轮盘赌......它迟早会在你的脸上炸开。是的,有一些方法可以做到这一点,但你不应该这样做。

你可以做的是在父母和孩子之间引入一个共享的沟通媒介。有许多不同的方式可以从子线程传达状态更新,这些方式不涉及对 TThread 实例的任何直接引用。(查看自定义窗口消息,可能是最常用的方法。)

一个非常明显(我认为)、简单化的例子。我在此消息框中对此进行了编码,因此不要期望它可以编译...这只是为了帮助证明需要配置一个单独的机制,以便您可以在父子之间安全地进行通信,而无需依赖引用一些 TThread 实例。(这假设有一个子线程......是的,还有很多其他方法可以做到这一点。)

unit MyPubComm.pas;

initialization
uses ...;

type
  TThreadComm = class
  private
    fThreadIsBusy:Boolean;  //more likely a State of some sort
    fThreadMessage:String;
  public
   property ThreadIsBusy:Boolean read fThreadIsBusy write fThreadIsBusy;
   property ThreadMessage:String read fThreadMessage write fThreadMessage;
  end;

  function IsChildBusy:Boolean;
  function ChildMessage:String;
  procedure SetThreadMessage(const MessageText:String);

var
  pubThreadStatus:TThreadComm;


implementation

function IsChildBusy:Boolean;
begin
  pubThreadStatus.Monitor.Enter;
  try
    Result := pubThreadStatus.ThreadIsBusy;
  finally
    pubThreadStatus.Monitor.Exit;
  end;
end;

function ChildMessage:String;
begin
  pubThreadStatus.Monitor.Enter;
  try
    Result := pubThreadStatus.ThreadMessage;
  finally
    pubThreadStatus.Monitor.Exit;
  end;
end;

procedure SetThreadMessage(const MessageText:String);
begin
  pubThreadStatus.Monitor.Enter;
  try
    pubThreadStatus.ThreadMessage := MessageText;
  finally
    pubThreadStatus.Monitor.Exit;
  end;
end;


initialization
  pubThreadStatus := TThreadCom.Create;
finalization
  pubThreadStatus.Free();

--------
unit MainAppCode.pas
...
implementation
Uses MyPubComm;

procedure TMyForm1.WorkingOnSomething();
begin
   if IsChildBusy() then  //safely check child state
   begin 
      label1.caption := 'Child thread busy...';
   end
   else
   begin
      label1.caption := ChildMessage();  //safely retrieve shared data
   end;
end;
------
MyThread.pas
...
implementation
Uses MyPubComm;

function TMyThread.WorkerBee();
begin
   If TimeToCommunicateThreadMessage then
   begin 
     SetThreadMessage(MyMessage);  //safely update shared data
   end;
end;

这也应该在 WorkingOnSomething() 中显示一个明显的问题 在检查 IsChildBusy() 和 ChildMessage() 调用之间,实际的忙碌状态和消息文本可能会发生变化,这就是需要更强大的通信方法的原因。

于 2012-12-18T06:15:38.107 回答