一个简单的程序,里面只有一个线程。对于 Delphi 的程序来说,也就是只有一个主线程。代码从头到尾一行一行地执行。
一个复杂的程序,可能需要很多不同的线程。
多个线程,可能需要协调它们的执行步骤(同步)。
例子
比如,A 出差后要报销住宿和打车的发票,他的执行流程是:
填写发票;
提交发票给经理;
等待经理签字;
拿到发票的经理签字后;提交发票给财务;
财务付钱。
从财务拿到钱以后,数钱,核实数字;
钱的金额没问题,报销流程结束;
金额有问题,重新向财务发起报销核实流程。
如果超过时间经理没有签字,则:
向经理发出一个请求发票签字的提醒。
上述流程,在程序中由一个线程去顺序执行。假设我们把这个线程叫做主线程。
然后,经理签字,由另外一个线程去执行;
然后,财务付钱,由另外一个线程去执行。
因此,主线程在提交发票给经理签字以后,就需要阻塞,不要继续执行下面的流程。一直阻塞到经理的签字结束,才继续往下走流程。
同样,继续往下的流程走到提交发票给财务,主线程再次阻塞,等到财务处理完,从财务手上拿到钱,主线程的流程才继续往下走。
上面的例子,描述了两个线程之间的【同步】。
解决方案
当一个线程(线程A),执行到某个位置,需要停下来等待另外一个线程(线程B)完成某些动作的时候,在 Delphi 程序里面,是线程A调用 TEvent 来进行阻塞。当线程B的动作完成后,由线程B调用同一个 TEvent 的实例的 SetEvent 方法,来解除线程A的阻塞。
例子代码
unit Unit3;
{-------------------------------------------------------------------------------
多线程里面的阻塞:
1. 使用 TEvent 对线程进行阻塞。
2. 如果 TEvent 超时,则阻塞解除,继续往下执行。此时如果再次阻塞,没有问题。
3. 如果在另外一个线程里面,调用 FEventA.SetEvent,解除阻塞。然后必须执行 ResetEvent;
3.1. 如果不执行 ResetEvent 则循环回来再次阻塞时,无法阻塞住(SetEvent 的信号一直在);
3.2. ResetEvent 由 SetEvent 的线程执行,也可以由被阻塞的线程在解除阻塞后执行,都可以。
4. 多个不同的 TEvent 实例,独立工作,互相不会被影响。也就是程序里面允许多个不同的 TEvent 实例对多个不同的线程进行阻塞。
-------------------------------------------------------------------------------}
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
System.Threading, System.SyncObjs,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;
type
TForm3 = class(TForm)
Memo1: TMemo;
Panel1: TPanel;
Button1: TButton;
Button2: TButton;
Button3: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
private
{ Private declarations }
FEventA: TEvent;
FEventB: TEvent;
public
{ Public declarations }
procedure ShowLog(const S: string);
end;
TMyThreadA = class(TThread)
private
public
procedure Execute; override;
end;
TMyThreadB = class(TThread)
private
public
procedure Execute; override;
end;
var
Form3: TForm3;
implementation
{$R *.dfm}
{ TForm3 }
procedure TForm3.Button1Click(Sender: TObject);
var
T1, T2: TThread;
begin
FEventA := TEvent.Create;
FEventB := TEvent.Create;
T1 := TMyThreadA.Create(True);
T2 := TMyThreadB.Create(True);
T1.FreeOnTerminate := True;
T2.FreeOnTerminate := True;
T1.Resume;
T2.Resume;
end;
procedure TForm3.Button2Click(Sender: TObject);
begin
FEventA.SetEvent;
FEventA.ResetEvent; //获得信号(SetEvent)以后,必须 ResetEvent
end;
procedure TForm3.Button3Click(Sender: TObject);
begin
FEventB.SetEvent;
end;
procedure TForm3.ShowLog(const S: string);
begin
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add(S);
end
);
end;
{ TMyThreadA }
procedure TMyThreadA.Execute;
begin
inherited;
Form3.ShowLog('ThreadA 开始');
while not Terminated do
begin
if Form3.FEventA.WaitFor(2000) = TWaitResult.wrSignaled then
begin
Form3.ShowLog('ThreadA SetEvent');
end
else
begin
Form3.ShowLog('ThreadA 超时');
end;
//Form3.FEventA.ResetEvent; //获得信号(SetEvent)以后,必须 ResetEvent
end;
Form3.ShowLog('ThreadA 线程退出');
end;
{ TMyThreadB }
procedure TMyThreadB.Execute;
begin
inherited;
Form3.ShowLog('ThreadB 开始');
while not Terminated do
begin
if Form3.FEventB.WaitFor(5000) = TWaitResult.wrSignaled then
begin
Form3.ShowLog('ThreadB SetEvent');
end
else
begin
Form3.ShowLog('ThreadB 超时');
end;
Form3.FEventB.ResetEvent; //获得信号(SetEvent)以后,必须 ResetEvent
//Sleep(300);
end;
Form3.ShowLog('ThreadB 线程退出');
end;
end.
总结一下:
1. TEvent 声明在:System.SyncObjs;
2. 多个不同的线程可以有多个不同的 TEvent 的实例来进行各自的阻塞;
3. TEvent.SetEvent 解除阻塞后,必须执行一次 TEvent.ResetEvent;否则就再也阻塞不住了;
4. ResetEvent 由任何一个线程来执行都可以;
5. 如果 TEvent 是超时导致的阻塞解除,不需要做 ResetEvent;