Delphi Thread(3)

本文详细介绍了如何在Delphi中使用TThread进行多线程编程,强调了Execute方法中的线程执行逻辑以及Terminated属性的重要性。通过示例解释了如何避免长时间阻塞导致的程序关闭延迟,以及如何利用OnTerminate事件进行线程间通信。同时,文章提醒开发者注意线程释放的时机和同步问题,以确保程序的稳定运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

TThread 详解
        我们常有工作线程和主线程之分,工作线程负责作一些后台操作,比如接收邮件

                                                              主线程负责界面上的一些显示

工作线程的好处在某些时候是不言而喻的,你的主界面可以响应任何操作,而背后的线程却在默默地工作。

         VCL中,工作线程执行在Execute方法中,你必须从TThread继承一个类并覆盖Execute方法,在这个方法中,所有代码都是在另一个 线程中执行的,除此之外,你的线程类的其他方法都在主线程执行,包括构造方法,析构方法,Resume等,很多人常常忽略了这一点。

最简单的一个线程类如下

TMyThread = class(TThread)
protected
procedure Execute; override;
end;

在Execute中的代码,有一个技术要点,如果你的代码执行时间很短,像这样,Sleep(1000),那没有关系;如果是这样Sleep (10000),10秒,那么你就不能直接这样写了,须把这10秒拆分成10个1秒,然后判断Terminated属性,像下面这样:

procedure TMyThread.Execute;
var
   i: Integer;
begin
   for i := 0 to 9 do
      if not Terminated then
        Sleep(1000)
     else
        Break;
end;

这样写有什么好处呢

      想想你要关闭程序,在关闭的时候调用MyThread.Free,这个时候线程并没有马上结束,它调用WaitFor,等待 Execute执行完后才能释放。

       你的程序就必须等10秒以后才能关闭,受得了吗。如果像上面那样写,在程序关闭时,调用Free之后,它顶多再等一秒就 会关闭。

        为什么?答案得去线程类的Destroy中找,它会先调用Terminate方法,在这个方法里面它把Terminated设为True(仅此而 已,很多人以为是结束线程,其实不是)。

      请记住这一切是在主线程中操作的,所以和Execute是并行执行的。既然Terminated属性已为 Ture,那么在Execute中判断之后,当然就Break了,Execute执行完毕,线程类也正常释放。

或者有人说,TThread可以设FreeOnTerminate属性为True,线程类就能自动释放。除非你的线程执行的任务很简单,不然,还是不要去理会这个属性,一切由你来操作,才能使线程更灵活强大。

          接下来的问题是如何使工作线程和主线程很好的通信,很多时候主线程必须得到工作线程的通知,才能做出响应。
比如接收邮件,工作线程向服务器收取邮件,收取完毕之后,它得通知主线程收到多少封邮件,主线程才能弹出一个窗口通知用户

在VCL中,我们可以用两种方法,一种是向主线程中的窗体发送消息,另一种是使用异步事件

第一种方法其实没有第二种来得方便。想想线程类中的OnTerminate事件,这个事件由线程函数的堆栈引起,却在主线程执行。

事实上,真正的线程函数是这个:
function ThreadProc(Thread: TThread): Integer;

函数里面有Thread.Execute,这就是为什么Execute是在其他线程中执行,该方法执行之后,有如下句:
Thread.DoTerminate;

而线程类的DoTerminate方法里面是
if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);

显然Synchronize方法使得CallOnTerminate在主线程中执行,而CallOnTerminate里面的代码其实就是:
if Assigned(FOnTerminate) then FOnTerminate(Self);

只要Execute方法一执行完就发生OnTerminate事件。不过有一点是必须注意,OnTerminate事件发生后,线程类不一定会释 放,只有在FreeOnTerminate为True之后,才会Thread.Free。看一下ThreadProc函数就知道。

依照Onterminate事件,我们可以设计自己的异步事件。

Synchronize方法只能传进一个无参数的方法类型,但我们的事件经常是要带一些参数的,这个稍加思考就可以得到解决,即在线程类中保存参数,触发事件前先设置参数,再调用异步事件,参数复杂的可以用记录或者类来实现。

假设这样,上面的代码每睡一秒,线程即向外面引发一次事件,我们的类可以这样设计

TSecondEvent = procedure (Second: Integer) of object;
TMyThread = class(TThread)
private
FSecond: Integer;
FSecondEvent: TSecondEvent;
procedure CallSecondEvent;
protected
procedure Execute; override;
public
property SencondEvent: TSecondEvent read FSecondEvent
write FSecondEvent;
end;

{ TMyThread }

procedure TMyThread.CallSecondEvent;
begin
if Assigned(FSecondEvent) then
FSecondEvent(FSecond);
end;

procedure TMyThread.Execute;
var
i: Integer;
begin
for i := 0 to 9 do
if not Terminated then
begin
Sleep(1000);
FSecond := i;
Synchronize(CallSecondEvent);
end
else
Break;
end; 
在主窗体中假设我们这样操作线程:

procedure TForm1.Button1Click(Sender: TObject);
begin
MyThread := TMyThread.Create(true);
MyThread.OnTerminate := ThreadTerminate;
MyThread.SencondEvent := SecondEvent;
MyThread.Resume;
end;

procedure TForm1.ThreadTerminate(Sender: TObject);
begin
ShowMessage('ok');
end;

procedure TForm1.SecondEvent(Second: Integer);
begin
Edit1.Text := IntToStr(Second);
end;

我们将每隔一秒就得到一次通知并在Edit中显示出来。

现在我们已经知道如何正确使用Execute方法,以及如何在主线程与工作线程之间通信了。但问题还没有结束,有一种情况出乎我的意料之外,即如果 线程中有一些资源,Execute正在使用这些资源,而主线程要释放这个线程,这个线程在释放的过程中会释放掉资源。想想会不会有问题呢,两个线程,一个 在使用资源,一个在释放资源,会出现什么情况呢, 

用下面代码来说明:

type
TMyClass = class
private
FSecond: Integer;
public
procedure SleepOneSecond;
end;

TMyThread = class(TThread)
private
FMyClass: TMyClass;
protected
procedure Execute; override;
public
constructor MyCreate(CreateSuspended: Boolean);
destructor Destroy; override;
end;

implementation

{ TMyThread }

constructor TMyThread.MyCreate(CreateSuspended: Boolean);
begin
inherited Create(CreateSuspended);
FMyClass := TMyClass.Create;
end;

destructor TMyThread.Destroy;
begin
FMyClass.Free;
FMyClass := nil;
inherited;
end;

procedure TMyThread.Execute;
var
i: Integer;
begin
for i := 0 to 9 do
FMyClass.SleepOneSecond;
end;

{ TMyClass }

procedure TMyClass.SleepOneSecond;
begin
FSecond := 0;
Sleep(1000);
end;

end. 

用下面的代码来调用上面的类:

procedure TForm1.Button1Click(Sender: TObject);
begin
MyThread := TMyThread.MyCreate(true);
MyThread.OnTerminate := ThreadTerminate;
MyThread.Resume;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
MyThread.Free;
end;

 

先点击Button1创建一个线程,再点击Button2释放该类,出现什么情况呢,违法访问,是的,MyThread.Free时,MyClass被释放掉了

FMyClass.Free;

FMyClass := nil;

而此时Execute却还在执行,并且调用MyClass的方法,当然就出现违法访问。对于这种情况,有什么办法来防止呢,我想到一种方法,即在线程类中使用一个成员,假设为FFinished,在Execute方法中有如下的形式:

FFinished := False;
try
//... ...
finally
FFinished := True;
End;

接着在线程类的Destroy中有如下形式:

While not FFinished do
Sleep(100);
MyClass.Free;

这样便能保证MyClass能被正确释放。

             线程是一种很有用的技术。但使用不当,常使人头痛。在优快云论坛上看到一些人问,我的窗口在线程中调用为什么出错,主线程怎么向其他线程发送消息等等,其实,我们在抱怨线程难用时,也要想想我们使用的方法对不对,

只要遵循一些正确的使用规则,线程其实很简单

后记

上面有一处代码有些奇怪:FMyClass.Free; FMyClass := nil;如果你只写FMyClass.Free,线程类还不会出现异常,即调用FMyClass.SleepOneSecond不会出错。我在主线程中试了下面的代码

MyClass := TMyClass.Create;
MyClass.SleepOneSecond;
MyClass.Free;
MyClass.SleepOneSecond;

同样也不会出错,但关闭程序时就出错了,如果是这样:

MyClass := TMyClass.Create;
MyClass.SleepOneSecond;
MyClass.Free;
MyThread := TMyThread.MyCreate(true);
MyThread.OnTerminate := ThreadTerminate;
MyThread.Resume;
MyClass.SleepOneSecond;

马上就出错。所以这个和线程类无线,应该是Delphi对于堆栈空间的释放规则,

我想MyClass.Free之后,该对象在堆栈上空间还是保留 着,只是允许其他资源使用这个空间,

所以接着调用下面这一句MyClass.SleepOneSecond就不会出错,当程序退出时可能对堆栈作一些清理 导致出错。而如果MyClass.Free之后即创建MyThread,大概MyClass的空间已经被MyThread使用,所以再调用 MyClass.SleepOneSecond就出错了。

### Delphi 中线程使用的示例代码及教程 在 Delphi 中,线程的使用是通过 `TThread` 类实现的。以下是一个简单的线程示例,展示了如何创建和使用线程来执行后台任务。此外,还提供了关于线程与主消息循环交互的说明[^1]。 #### 简单的线程示例 以下代码展示了如何在 Delphi 中创建一个基本线程,并在线程中执行一些计算任务: ```delphi unit Unit1; interface uses Classes, SysUtils; type TMyThread = class(TThread) protected procedure Execute; override; public constructor Create(CreateSuspended: Boolean); end; implementation { TMyThread } constructor TMyThread.Create(CreateSuspended: Boolean); begin inherited Create(CreateSuspended); FreeOnTerminate := True; // 自动释放线程对象 end; procedure TMyThread.Execute; var I: Integer; begin for I := 1 to 10 do begin Sleep(1000); // 模拟耗时操作 Synchronize( procedure begin Writeln('Thread is running, iteration: ' + IntToStr(I)); end ); end; end; end. ``` #### 如何运行线程 可以通过以下方式启动上述线程: ```delphi var MyThread: TMyThread; begin MyThread := TMyThread.Create(False); // 启动线程 end; ``` #### 关于线程与主消息循环的关系 Delphi 应用程序默认是单线程的,主线程负责处理系统消息和界面更新等任务[^1]。如果需要创建额外的线程来执行后台任务,则需要注意以下几点: - 使用 `Synchronize` 方法确保线程安全地访问主线程中的资源。 - 如果线程需要处理窗口消息(例如通过 `AllocateHWND` 创建的隐藏窗口),则需要确保正确设置消息队列[^2]。 #### CEF 中多线程消息循环的参数说明 在某些情况下,应用程序可能需要与多线程消息循环集成。CEF(Chromium Embedded Framework)提供了一个名为 `multi_threaded_message_loop` 的参数,用于控制是否将主线程与浏览器进程的 UI 线程分离[^4]。如果设置为 `false`,则 CEF 的主线程将与应用程序的主线程共享消息循环。 ### 高级主题:调试线程 在调试多线程应用程序时,Delphi 的集成调试器可能会频繁中断程序运行。为了避免这种情况,可以暂时关闭调试器的自动中断功能[^3]。具体方法如下: - 在项目选项中启用“Run Parameters”下的“Disable runtime themes”。 - 或者在代码中添加适当的异常处理逻辑以避免调试器捕获运行时错误。 ### 安全性注意事项 对于涉及外部通信的应用程序(如通过 `SendMessage` 和 `WM_COPYDATA` 实现的接口),必须注意网络安全问题[^5]。建议实现双因素认证(2FA)或其他高级安全措施以保护数据传输的安全性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值