一、为什么需要多线程编程

想象一下你正在经营一家快餐店。如果只有一个服务员,顾客点餐、制作、结账全由他一个人完成,效率肯定低得让人抓狂。多线程就像雇佣多个服务员——有人专门接单,有人负责制作,还有人处理付款,整个系统运转速度立刻提升。

在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.

关键点说明:

  1. TThread是所有线程的基类,必须重写Execute方法
  2. Create(False)参数决定是否立即启动线程
  3. FreeOnTerminate能避免内存泄漏
  4. 线程间输出可能交错——这正是并发执行的证据

三、共享资源的安全访问

当多个线程同时修改同一个变量时,就会像两个厨师争抢同一个锅铲。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.QueueTThread.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.

六、性能优化与陷阱规避

  1. 线程池模式:频繁创建/销毁线程代价高昂,建议复用线程
  2. 虚假共享:看似不相关的变量可能因CPU缓存行导致性能下降
  3. 测量为王:实际测试比理论推测更重要,使用GetTickCount64计时
var
  StartTime: QWord;
begin
  StartTime := GetTickCount64;
  // 执行多线程操作
  WriteLn('耗时: ', GetTickCount64 - StartTime, 'ms');
end;

七、总结与选择建议

Pascal的多线程虽然需要手动管理更多细节,但这种控制力在某些场景下反而是优势。对于计算密集型任务,合理使用线程能获得接近C的性能;而对于IO密集型操作,建议结合异步模式。记住:

  • 同步原语是你的朋友,但滥用会导致性能瓶颈
  • 永远假设共享数据是不安全的
  • 复杂的多线程调试可以借助WriteLn日志(是的,有时候简单最有效)