Delphi多线程编程入门:工作线程与主线程的协作

一、什么是主线程?什么是工作线程?

在Delphi窗体应用中,线程主要分为两类,职责明确,新手记住"分工"就能分清:

  • 主线程(也叫界面线程) :每个窗体应用程序启动时创建的第一条线程,是程序的"门面担当"。负责做和界面相关的所有事,比如:
    • 创建窗体、显示按钮/输入框等控件
    • 响应你的鼠标点击、键盘输入等windows信息、刷新界面内容等。
    • 负责创建新线程
  • 工作线程(后台线程) :除主线程外,我们自己创建的所有线程都是工作线程 ,是程序的"幕后打工人"。负责处理耗时、繁琐的后台任务,比如接收邮件、下载文件、批量处理数据、连接数据库查询等。
    核心原则:工作线程不能直接操作界面控件(比如改按钮文字、更改进度条),必须通过特定方式通知主线程处理,否则会导致程序崩溃或界面错乱。

二、Delphi线程基础:必须继承TThread类

Delphi已经帮我们封装好了线程的核心逻辑,我们不需要从零写线程,只要做两件事:

  1. 从系统自带的 TThread 类继承一个自定义线程类;
  2. 重写(覆盖)TThread类的 Execute 方法------这个方法就是工作线程的"核心工作区",里面的代码都会在后台执行。

这里有个关键知识点:只有Execute方法在工作线程中执行,线程类的其他方法(比如构造函数、析构函数、Resume等)都在主线程中执行

2.1 最简单的Delphi线程类实例

下面是一个可直接参考的基础线程类代码:

bash 复制代码
unit MyThreadUnit; // 自定义线程单元

interface

uses
  Classes; // 必须引入Classes单元,TThread在这里定义

// 自定义线程类,继承自TThread
type
  TMyThread = class(TThread)
  protected
    // 重写Execute方法,工作线程的核心逻辑写在这里
    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;
begin
  // 这里写工作线程的具体任务,以下是示例
  Writeln('我是工作线程,正在后台干活!');
  // 实际开发中可替换为:接收邮件、处理数据、连接FTP等任务
end;

end.

2.2 Delphi7 专属:TThread 属性和方法(标注兼容性差异)

属性/方法 作用 说明(重点标注 Delphi7 特性)
Terminated 终止标志(Delphi7 无公开属性) 高版本为公开布尔属性,True 表示线程应尽快结束;Delphi7 仅为受保护字段FTerminated,外部无法直接访问,需自定义属性替代
Terminate 终止方法(Delphi7 为静态方法) 作用是设置内部FTerminated为 True;Delphi7 中无virtual关键字,无法被 override 重写,需自定义方法同步状态
Suspended 暂停状态(Delphi7 公开属性) True = 暂停(挂起),False = 运行;可直接读取,是 Delphi7 控制线程暂停的核心属性
Suspend 暂停方法(Delphi7 支持) 暂停线程执行,调用一次挂起计数 + 1;Delphi7 必备,新版本已废弃(替换为 Start)
Resume 继续方法(Delphi7 支持) 唤醒暂停的线程,调用一次挂起计数 - 1;仅当挂起计数 = 0 时线程恢复运行,Delphi7 启动挂起线程的唯一方法
FreeOnTerminate 自动释放(Delphi7 支持) True = 线程结束后自动释放内存;Delphi7 推荐开启,避免手动释放的繁琐和风险

三、线程的创建与运行:控制线程什么时候开始工作

创建自定义线程后,我们可以控制它"立即运行 "或"稍后运行 ",关键在于构造函数的CreateSuspended 参数(布尔值)。

3.1 方式1:创建后立即运行

参数传 False,线程创建完成后马上执行 Execute 方法中的逻辑:

bash 复制代码
var
  myThread: TMyThread;
begin
  // 创建线程,参数False=立即运行
  myThread := TMyThread.Create(False);
end;

3.2 方式2:创建后先暂停,稍后再运行

参数传 True,线程创建后处于"挂起状态",需要调用 Resume 方法才能启动:

bash 复制代码
var
  myThread: TMyThread;
begin
  // 创建线程,参数True=先挂起(不运行)
  myThread := TMyThread.Create(True);
  
  // 适当的时刻启动线程(比如点击"开始处理"按钮后)
  myThread.Resume;
end;

tips

  • Delphi 7中使用Resume启动线程,新版本Delphi使用Start。
  • Resume方法在主线程执行,只能唤醒"挂起状态"的线程,不能重复调用(除非线程被多次挂起)。

四、线程的退出:什么时候线程会停止工作?

线程的"生命周期"很简单:当 Execute 方法中的代码执行完毕,工作线程就会正常退出。

4.1 正常退出示例

比如下面的线程,执行完"打印一句话"的逻辑后,线程就结束了:

bash 复制代码
procedure TMyThread.Execute;
begin
  // 执行完这行代码,线程就退出了
  Writeln('I am a new thread');
end;

4.2 主动控制退出:用Terminated属性和Terminate方法

如果线程执行的是 "循环任务"(比如持续监听数据、批量处理数据),不能等代码自然执行完,这时候需要主动通知线程退出。由于 Delphi7 无公开的Terminated属性,我们需要通过 "自定义终止标记" 实现,核心逻辑不变(发通知 + 定期检查)。

核心思路

在线程类中定义私有布尔字段FIsTerminated,作为自定义终止标记(替代原生Terminated)。

定义公共方法MarkAsTerminated,用于设置FIsTerminated=True(替代原生Terminate方法),同时调用原生Terminate更新内部FTerminated。

在Execute方法的循环中,定期检查FIsTerminated,收到 "退出通知" 后主动退出循环。

4.2.1 主动退出示例(循环任务场景)

bash 复制代码
unit MyThreadUnit;

interface

uses
  Classes;

type
  TMyThread = class(TThread)
  private
    FIsTerminated: Boolean; // 自定义终止标记(替代Delphi7缺失的Terminated属性)
  protected
    procedure Execute; override;
  public
    constructor Create(CreateSuspended: Boolean);
    procedure MarkAsTerminated; // 自定义终止方法(替代原生Terminate,Delphi7必备)
    property IsTerminated: Boolean read FIsTerminated; // 公开自定义属性,供外部读取
  end;

implementation

{ TMyThread }

constructor TMyThread.Create(CreateSuspended: Boolean);
begin
  inherited Create(CreateSuspended);
  FIsTerminated := False; // 初始化终止标记为"未终止"
  FreeOnTerminate := True; // 开启自动释放
end;

// 自定义终止方法(Delphi7专属,替代原生Terminate,无法重写只能自定义)
procedure TMyThread.MarkAsTerminated;
begin
  FIsTerminated := True; // 设置自定义终止标记
  inherited Terminate; // 调用原生静态方法,更新内部FTerminated(保证线程原生逻辑)
end;

procedure TMyThread.Execute;
begin
  // 循环执行后台任务(比如持续接收数据、批量处理数据)
  while not FIsTerminated do // 定期检查自定义终止标记(替代原生Terminated)
  begin
    // 核心任务:接收邮件、处理数据、更新进度等
    OutputDebugString('正在后台处理任务...');
    Sleep(1000); // 暂停1秒,模拟耗时操作,同时让出CPU(Delphi7避免CPU占用过高)
  end;
  
  // 当FIsTerminated为True时,退出循环,Execute执行完毕,线程退出
  OutputDebugString('线程收到退出通知,已停止工作');
end;

end.

4.2.2 主线程中通知线程退出(Delphi7 适配,按钮点击事件示例)

bash 复制代码
// 主窗体中声明线程对象(全局或窗体私有变量)
var
  myThread: TMyThread;

// 点击"停止处理"按钮,通知线程退出(Delphi7适配)
procedure TForm1.btnStopClick(Sender: TObject);
begin
  if Assigned(myThread) then // 安全判断,避免空对象调用报错(Delphi7必备)
  begin
    myThread.MarkAsTerminated; // 调用自定义终止方法,发"停工通知"
  end;
end;

通俗理解MarkAsTerminated方法就像你喊 "打工人下班了",Execute方法里的循环就像 "打工人定期看时间",看到 "下班通知"(FIsTerminated=True)就停止干活,否则继续工作。Delphi7 中只是多了一步 "自定义通知牌",核心逻辑和高版本一致。

五、线程的暂停与唤醒:SuspendResume方法

如果需要临时让线程"停工",之后再"复工",就用 Suspend(暂停)和 Resume(唤醒)方法,核心靠"挂起计数"控制:

  • 调用 Suspend 一次,挂起计数+1,线程进入"挂起状态"(暂停工作);
  • 调用 Resume 一次,挂起计数-1;
  • 只有当挂起计数=0时,线程才会恢复运行;计数>0时,线程保持挂起。

5.1 暂停与唤醒示例

bash 复制代码
var
  myThread: TMyThread;
begin
  // 创建并启动线程
  myThread := TMyThread.Create(False);
  
  // 临时暂停线程(挂起计数=1)
  myThread.Suspend;
  
  // 再次暂停(挂起计数=2)
  myThread.Suspend;
  
  // 第一次唤醒(挂起计数=1,仍暂停)
  myThread.Resume;
  
  // 第二次唤醒(挂起计数=0,恢复运行)
  myThread.Resume;
end;

实用场景 :比如线程正在批量处理数据,点击"暂停 "按钮调用 Suspend,点击"继续 "按钮调用 Resume,适合需要手动控制"启停"的场景。

六、线程的销毁:如何释放线程资源?

线程退出后,需要释放它占用的内存资源,否则会导致内存泄漏。有两种销毁方式,根据场景选择:

6.1 自动销毁:设置FreeOnTerminate为True

FreeOnTerminate 是 TThread 的布尔属性,设为 True 后,线程退出(Execute执行完毕)时,系统会自动释放线程对象,不需要我们手动处理。

bash 复制代码
constructor TMyThread.Create(CreateSuspended: Boolean);
begin
  inherited Create(CreateSuspended);
  // 设为True,线程退出后自动释放
  FreeOnTerminate := True;
end;

适用场景

  • 线程任务简单,不需要在退出后做额外处理(比如不需要获取线程的执行结果、不需要更新最终界面状态);
  • 批量创建多个线程,无法逐个手动管理的场景(比如多线程下载文件)。

6.2 手动销毁:用FreeAndNil

如果没有设置 FreeOnTerminate 为 True,就需要我们在主线程中"手动销毁"线程对象。注意必须等线程完全退出后再销毁!

bash 复制代码
var
  myThread: TMyThread;
begin
  // 创建并启动线程
  myThread := TMyThread.Create(False);
  
  // 等待线程退出(比如通过Terminated控制退出后)
  // (实际开发中可通过循环判断线程状态,或用OnTerminate事件,后面会讲)
  
  // 手动释放线程资源
  FreeAndNil(myThread);
end;

推荐原则

  • 如果线程任务复杂,或需要在退出后获取结果、更新界面,建议手动销毁;
  • 简单任务可使用自动销毁,更省心。

七、核心难点:工作线程与主线程通信

前面提到"工作线程不能直接操作界面 ",Delphi7 中该规则更为严格,直接操作界面控件(比如改进度条、改标签文字)会立即导致程序崩溃。此时需要通过 Synchronize 方法实现线程间通信,这是 Delphi7 中最基础、最安全的通信方式。

7.1 先搞懂通信原理

Delphi的线程通信核心逻辑可以简单理解为:

  1. 工作线程的核心逻辑在 Execute 方法中执行,无法直接访问界面控件;
  2. 在线程类中定义 "成员变量",用于存储需要传递给主线程的数据(比如处理进度、执行结果);
  3. 在线程类中定义 "无参数同步方法",用于读取成员变量并操作界面控件;
  4. 在 Execute 方法中,调用 Synchronize 方法,将同步方法 "切换到主线程执行";
  5. 线程退出后,可绑定 OnTerminate 事件,在主线程中做收尾工作(比如显示 "处理完成")。

7.2 Synchronize方法:让工作线程代码在主线程执行

Synchronize 方法的作用是"同步"------把工作线程中的某个方法,放到主线程中执行。它有个限制:只能传递"无参数的方法 "。
解决参数问题的思路:在线程类中定义"成员变量"存储需要传递的数据,调用 Synchronize 前给变量赋值,在同步方法中读取变量并操作界面。

7.2.1 通信示例:工作线程更新界面进度

bash 复制代码
unit MyThreadUnit;

interface

uses
  Classes, Forms; 

type
  TMyThread = class(TThread)
  private
    FProgress: Integer; // 存储进度值(传递给主线程的数据,Delphi7必备成员变量)
    FLogMsg: string;    // 存储日志信息(传递给主线程的额外数据)
    procedure SyncUpdateProgress; // 同步方法(无参数,Delphi7要求)
    procedure SyncUpdateLog;      // 同步方法(无参数,更新日志界面)
  protected
    procedure Execute; override;
  public
    constructor Create(CreateSuspended: Boolean);
  end;

implementation

uses
  MainForm; // 引入主窗体单元(包含要操作的进度条、富文本框,Delphi7需确保窗体类名一致)

{ TMyThread }

constructor TMyThread.Create(CreateSuspended: Boolean);
begin
  inherited Create(CreateSuspended);
  FProgress := 0; // 初始化进度值
  FLogMsg := '';  // 初始化日志信息
  FreeOnTerminate := True; // 开启自动释放
end;

// 同步方法:更新进度条(在主线程执行,可安全操作界面控件,Delphi7适配)
procedure TMyThread.SyncUpdateProgress;
var
  MainFormRef: TForm1; // 明确窗体引用变量,解决Variable required报错(Delphi7必备)
begin
  // 安全判断:窗体存在且类型正确
  if Assigned(Application.MainForm) and (Application.MainForm is TForm1) then
  begin
    MainFormRef := TForm1(Application.MainForm);
    // 操作主窗体进度条(主线程执行,安全无报错)
    MainFormRef.ProgressBar1.Position := FProgress;
  end;
end;

// 同步方法:更新日志框(在主线程执行,解决EM_SCROLLCARET未定义报错,Delphi7适配)
procedure TMyThread.SyncUpdateLog;
var
  MainFormRef: TForm1;
  LogEntry: string;
begin
  if Assigned(Application.MainForm) and (Application.MainForm is TForm1) then
  begin
    MainFormRef := TForm1(Application.MainForm);
    // 构造日志条目,包含时间戳
    LogEntry := FormatDateTime('yyyy-mm-dd hh:nn:ss', Now()) + ' >>> ' + FLogMsg;
    // 追加日志到富文本框
    MainFormRef.RichEdit1.Lines.Add(LogEntry);
    // 滚动到最新日志
    MainFormRef.RichEdit1.SelStart := Length(MainFormRef.RichEdit1.Text);
  end;
end;

procedure TMyThread.Execute;
begin
  // 初始化日志,同步到主线程界面
  FLogMsg := '工作线程启动,开始处理任务...';
  Synchronize(SyncUpdateLog); // 传递方法

  // 模拟循环处理任务,更新进度
  while (FProgress < 100) and not Terminated do // 此处Terminated为内部字段,循环内安全判断
  begin
    Inc(FProgress); // 更新进度值(每次+1)
    FLogMsg := Format('正在处理,当前进度 %d%%', [FProgress]); // 更新日志信息

    // 同步到主线程,更新界面(Delphi7中Synchronize会阻塞工作线程,直到主线程处理完毕)
    Synchronize(SyncUpdateProgress);
    Synchronize(SyncUpdateLog);

    Sleep(50); // 暂停50毫秒,模拟耗时操作,避免CPU占用过高(Delphi7必备)
  end;

  // 任务完成,更新最终日志
  FLogMsg := '任务处理完成,最终进度 100%';
  Synchronize(SyncUpdateLog);
end;

end.

7.2.2 主窗体代码(MainForm.pas,Delphi7 适配)

bash 复制代码
delphi
unit MainForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    btnStart: TButton;
    ProgressBar1: TProgressBar;
    RichEdit1: TRichEdit;
    procedure btnStartClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm} // 关联窗体布局文件,Delphi7必备

uses
  MyThreadUnit; // 引入自定义线程单元

// 点击"开始处理"按钮,创建并启动线程(Delphi7适配)
procedure TForm1.btnStartClick(Sender: TObject);
var
  myThread: TMyThread;
begin
  // 初始化界面
  ProgressBar1.Position := 0;
  RichEdit1.Clear;
  RichEdit1.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now()) + ' >>> 准备启动工作线程...');

  // 创建并启动线程(立即运行)
  myThread := TMyThread.Create(False);
end;

end.

7.3 OnTerminate事件:线程退出后的通知

OnTerminate 事件在"线程退出后"触发,且自动在主线程执行,适合用来做"收尾工作"(比如显示处理结果、释放资源)。

bash 复制代码
// 主窗体中使用线程,绑定OnTerminate事件
procedure TMainForm.btnStartClick(Sender: TObject);
var
  myThread: TMyThread;
begin
  myThread := TMyThread.Create(True);
  
  // 绑定线程退出事件(在主线程执行)
  myThread.OnTerminate := ThreadTerminateHandler;
  
  myThread.Resume;
end;

// OnTerminate事件处理函数(主线程执行)
procedure TMainForm.ThreadTerminateHandler(Sender: TObject);
begin
  // 显示处理完成
  ShowMessage('线程处理完成!');
  
  // 手动释放线程资源(如果没设置FreeOnTerminate)
  FreeAndNil(TMyThread(Sender));
end;

八、总结

Delphi多线程核心要点

  1. 主线程管界面,工作线程管后台任务,Delphi7 中工作线程绝对不能直接操作 VCL 控件,否则立即崩溃;
  2. 自定义线程必须继承TThread,重写Execute方法(核心工作区),其他方法均在主线程执行;
  3. 线程创建参数False=立即运行,True=先挂起,Delphi7 中需用Resume唤醒挂起线程;
  4. 线程退出:Execute执行完自然退出,循环任务用自定义IsTerminated属性控制(弥补 Delphi7 缺失的原生Terminated属性);
  5. 线程销毁:优先开启FreeOnTerminate=True实现自动销毁,手动销毁需等线程完全退出后用FreeAndNil;
  6. 线程通信:用Synchronize方法同步无参数过程,通过成员变量传递数据;

最佳实践建议

  • 及时检查自定义终止标记:在循环任务中,每次迭代都要检查FIsTerminated,确保线程能快速响应退出通知,避免线程 "卡死";
  • 避免长时间占用CPU :在循环中添加Sleep(哪怕 10 毫秒),让出 CPU 时间片,避免窗体卡死,同时减少系统资源占用;
  • 资源清理:确保线程结束时释放所有资源
  • 异常处理:在线程中捕获异常,避免程序崩溃
  • 不要过度创建线程:线程创建和销毁有开销,合理使用线程池

常见问题解答

Q1: 为什么报 "Undeclared identifier: Terminated" 错误?
A1 : Delphi7 中TThread无公开的Terminated属性,只有受保护的FTerminated字段,外部无法直接访问。
解决方案:自定义FIsTerminated字段和IsTerminated属性,替代原生Terminated。

Q2: 为什么线程退出后,窗体还会卡死?

A2: 可能是没有调用Application.ProcessMessages处理界面消息,或Synchronize方法调用过于频繁。
解决方案:在等待线程退出的循环中添加Application.ProcessMessages,减少Synchronize方法的调用频率(比如每更新 10% 进度同步一次界面)。

Q3: 如何在线程间传递数据?

A3: 使用线程类的属性或全局变量,但要注意线程安全。

Q:4 什么时候使用多线程?

A4: 当需要执行耗时操作(如下载文件、处理大量数据)而不希望阻塞界面时。

相关推荐
Da Da 泓14 小时前
多线程(八)【定时器】
java·学习·多线程·定时器
七夜zippoe15 小时前
Python多线程性能优化实战:突破GIL限制的高性能并发编程指南
python·macos·多线程·读写锁·gil·rcu
努力的小帅2 天前
Linux_多线程(Linux入门到精通)
linux·多线程·多进程·线程同步·线程互斥·生产消费者模型
C雨后彩虹3 天前
volatile 实战应用篇 —— 典型场景
java·多线程·并发·volatile
阿_旭4 天前
【实战干货】YOLO26 + 多线程实现多视频流实时对象跟踪【附源码+详解】
多线程·多视频流
放逐者-保持本心,方可放逐6 天前
Node.js 多线程与高并发+实例+思考(简要版)
node.js·编辑器·vim·高并发·多线程·场景应用实例
小灰灰搞电子6 天前
C++ 多线程详解
c++·多线程
袁慎建@ThoughtWorks6 天前
ThreadLocal那些事儿
java·jdk·多线程·threadlocal
小毅&Nora7 天前
【Java线程安全实战】⑬ volatile的奥秘:从“共享冰箱“到内存可见性的终极解析
java·多线程·volatile
这周也會开心7 天前
多线程与并发-知识总结1
java·多线程·并发