一、为什么需要多线程编程
想象一下你正在经营一家快餐店。如果只有一个服务员,顾客点餐、制作、结账全由他一个人完成,效率肯定低得让人抓狂。多线程就像雇佣多个服务员——有人专门接单,有人负责制作,还有人处理付款,整个系统运转速度立刻提升。
在Pascal中,虽然它不像Java或C#那样天生为多线程设计,但通过正确使用TThread类和同步机制,我们完全可以实现高效的并发编程。特别是在处理文件IO、网络请求或复杂计算时,多线程能显著改善程序响应速度。
二、Pascal多线程基础
让我们从一个简单的示例开始(技术栈:Free Pascal/Lazarus):
program SimpleThreadDemo;
uses
Classes, SysUtils;
type
// 自定义线程类
TMyWorker = class(TThread)
private
FId: Integer;
protected
procedure Execute; override; // 线程入口点
public
constructor Create(AId: Integer);
end;
constructor TMyWorker.Create(AId: Integer);
begin
inherited Create(False); // False表示立即启动线程
FId := AId;
FreeOnTerminate := True; // 线程结束后自动释放
end;
procedure TMyWorker.Execute;
var
I: Integer;
begin
for I := 1 to 5 do
begin
WriteLn('线程 ', FId, ' 正在工作: ', I);
Sleep(500); // 模拟耗时操作
end;
end;
var
i: Integer;
begin
for i := 1 to 3 do
TMyWorker.Create(i); // 创建3个线程
ReadLn; // 防止主线程退出
end.
关键点说明:
TThread是所有线程的基类,必须重写Execute方法Create(False)参数决定是否立即启动线程FreeOnTerminate能避免内存泄漏- 线程间输出可能交错——这正是并发执行的证据
三、共享资源的安全访问
当多个线程同时修改同一个变量时,就会像两个厨师争抢同一个锅铲。Pascal提供了几种同步工具:
1. 临界区(Critical Section)
program CriticalSectionDemo;
uses
Classes, SysUtils, SyncObjs;
var
Counter: Integer = 0;
Lock: TCriticalSection; // 同步锁
type
TCounterThread = class(TThread)
protected
procedure Execute; override;
end;
procedure TCounterThread.Execute;
var
i: Integer;
begin
for i := 1 to 1000 do
begin
Lock.Acquire; // 获取锁
try
Inc(Counter); // 安全修改共享变量
finally
Lock.Release; // 释放锁
end;
end;
end;
begin
Lock := TCriticalSection.Create;
try
with TCounterThread.Create(False) do
WaitFor; // 等待线程结束
WriteLn('最终计数: ', Counter); // 正确输出1000
finally
Lock.Free;
end;
ReadLn;
end.
2. 信号量(Semaphore)
适合控制有限资源的访问,比如数据库连接池:
program SemaphoreDemo;
uses
Classes, SysUtils, SyncObjs;
var
Sem: TSemaphore;
ActiveThreads: Integer = 0;
type
TResourceUser = class(TThread)
protected
procedure Execute; override;
end;
procedure TResourceUser.Execute;
begin
Sem.WaitFor; // 等待信号量
try
InterlockedIncrement(ActiveThreads); // 原子操作
WriteLn(ThreadID, ' 获取资源,当前活跃: ', ActiveThreads);
Sleep(1000); // 模拟资源使用
finally
InterlockedDecrement(ActiveThreads);
Sem.Release; // 释放信号量
end;
end;
begin
Sem := TSemaphore.Create(nil, 3, 3, ''); // 允许3个并发
try
// 创建5个线程,但只有3个能同时运行
with TResourceUser.Create(False) do
with TResourceUser.Create(False) do
with TResourceUser.Create(False) do
with TResourceUser.Create(False) do
with TResourceUser.Create(False) do
Sleep(5000); // 给足时间观察
finally
Sem.Free;
end;
end.
四、高级模式与最佳实践
1. 线程间通信
使用TThread.Queue或TThread.Synchronize安全更新UI:
procedure TMyThread.UpdateGUI;
begin
Form1.Memo1.Lines.Add('来自线程的消息');
end;
procedure TMyThread.Execute;
begin
// ... 某些操作后
Synchronize(@UpdateGUI); // 安全调用主线程方法
end;
2. 避免死锁的黄金法则
- 总是以相同的顺序获取多个锁
- 设置锁超时时间
- 使用
TryEnterCriticalSection而非阻塞调用
if Lock.TryEnter then
try
// 临界区代码
finally
Lock.Leave;
end
else
WriteLn('获取锁失败,避免死锁');
五、实战:多线程文件处理器
下面是一个完整的文件哈希计算器(技术栈:Free Pascal):
program MultiThreadFileHasher;
uses
Classes, SysUtils, MD5, SyncObjs;
type
THashResult = record
FileName: string;
Hash: string;
end;
var
Results: array of THashResult;
Lock: TCriticalSection;
FileQueue: TStringList;
type
THashWorker = class(TThread)
protected
procedure Execute; override;
function ComputeHash(const FileName: string): string;
end;
function THashWorker.ComputeHash(const FileName: string): string;
var
FS: TFileStream;
Hash: TMD5;
begin
FS := TFileStream.Create(FileName, fmOpenRead);
try
Hash := TMD5.Create;
try
Result := Hash.HashStream(FS);
finally
Hash.Free;
end;
finally
FS.Free;
end;
end;
procedure THashWorker.Execute;
var
FileName: string;
Idx: Integer;
begin
while True do
begin
Lock.Acquire;
try
if FileQueue.Count = 0 then Break;
FileName := FileQueue[0];
FileQueue.Delete(0);
Idx := Length(Results);
SetLength(Results, Idx + 1);
finally
Lock.Release;
end;
Results[Idx].FileName := FileName;
Results[Idx].Hash := ComputeHash(FileName);
end;
end;
procedure ProcessFiles(const Path: string);
var
i: Integer;
Workers: array[0..3] of THashWorker;
begin
FileQueue := TStringList.Create;
Lock := TCriticalSection.Create;
try
// 查找所有文件
FileQueue.AddStrings(FindAllFiles(Path));
// 启动4个工作线程
for i := 0 to High(Workers) do
Workers[i] := THashWorker.Create(False);
// 等待所有线程结束
for i := 0 to High(Workers) do
Workers[i].WaitFor;
// 输出结果
for i := 0 to High(Results) do
WriteLn(Results[i].FileName, ' -> ', Results[i].Hash);
finally
Lock.Free;
FileQueue.Free;
end;
end;
begin
if ParamCount > 0 then
ProcessFiles(ParamStr(1))
else
WriteLn('请指定目录路径');
end.
六、性能优化与陷阱规避
- 线程池模式:频繁创建/销毁线程代价高昂,建议复用线程
- 虚假共享:看似不相关的变量可能因CPU缓存行导致性能下降
- 测量为王:实际测试比理论推测更重要,使用
GetTickCount64计时
var
StartTime: QWord;
begin
StartTime := GetTickCount64;
// 执行多线程操作
WriteLn('耗时: ', GetTickCount64 - StartTime, 'ms');
end;
七、总结与选择建议
Pascal的多线程虽然需要手动管理更多细节,但这种控制力在某些场景下反而是优势。对于计算密集型任务,合理使用线程能获得接近C的性能;而对于IO密集型操作,建议结合异步模式。记住:
- 同步原语是你的朋友,但滥用会导致性能瓶颈
- 永远假设共享数据是不安全的
- 复杂的多线程调试可以借助
WriteLn日志(是的,有时候简单最有效)
评论