多线程应用实时保存TXT日志且避免应用冻结的最优方法
多线程Delphi日志无冻结实现方案
你的代码出现UI冻结的核心原因是:用Synchronize把文件IO操作强行放到主线程执行,而文件读写是阻塞性操作,主线程被卡住后无法响应用户操作,导致冻结。
下面是解决思路和具体实现方案:
一、核心优化方向
- 永远不要在主线程做文件IO,把日志写入逻辑放到独立的工作线程
- 用线程安全的队列传递日志消息,避免多线程直接竞争文件资源
- 减少文件打开/关闭的频次,降低IO开销
二、具体实现代码(独立日志线程+队列)
这个方案用专门的日志线程处理所有写入操作,其他线程只需要把日志消息扔进队列,完全不阻塞主线程:
unit LogUnit; interface uses System.Classes, System.SysUtils, SyncObjs; type TLogMsg = record Msg: string; ShowUI: Boolean; end; TLogThread = class(TThread) private FQueue: array of TLogMsg; FCriticalSection: TCriticalSection; FLogFile: TextFile; FCurrentDate: TDate; FEvent: TEvent; procedure OpenLogFile(const Date: TDate); procedure CloseLogFile; procedure UpdateMemo(const Msg: string); protected procedure Execute; override; public constructor Create; destructor Destroy; override; procedure AddMsg(const AMsg: string; AShowUI: Boolean); end; var LogThread: TLogThread; implementation uses MainForm; // 替换成你的主窗体单元名称 { TLogThread } constructor TLogThread.Create; begin inherited Create(True); FCriticalSection := TCriticalSection.Create; FEvent := TEvent.Create(nil, False, False, ''); FCurrentDate := 0; FreeOnTerminate := False; Resume; end; destructor TLogThread.Destroy; begin CloseLogFile; FEvent.SetEvent; // 唤醒线程让它退出 WaitFor; FEvent.Free; FCriticalSection.Free; inherited; end; procedure TLogThread.OpenLogFile(const Date: TDate); var FileName: string; begin CloseLogFile; FileName := ExtractFilePath(ParamStr(0)) + 'Logs\FILE_' + FormatDateTime('dd_mm_yyyy', Date) + '.txt'; ForceDirectories(ExtractFilePath(FileName)); // 确保日志目录存在,避免创建文件失败 AssignFile(FLogFile, FileName); if FileExists(FileName) then Append(FLogFile) else Rewrite(FLogFile); FCurrentDate := Date; end; procedure TLogThread.CloseLogFile; begin if FCurrentDate <> 0 then begin CloseFile(FLogFile); FCurrentDate := 0; end; end; procedure TLogThread.UpdateMemo(const Msg: string); begin MainForm.Form1.Memo2.Lines.Add(Msg); end; procedure TLogThread.AddMsg(const AMsg: string; AShowUI: Boolean); var LogMsg: TLogMsg; begin FCriticalSection.Enter; try LogMsg.Msg := AMsg; LogMsg.ShowUI := AShowUI; SetLength(FQueue, Length(FQueue)+1); FQueue[High(FQueue)] := LogMsg; finally FCriticalSection.Leave; end; FEvent.SetEvent; // 唤醒日志线程处理新消息 end; procedure TLogThread.Execute; var I: Integer; CurrentMsg: TLogMsg; Today: TDate; begin while not Terminated do begin // 等待新消息,没有消息时线程休眠,不占用CPU if FEvent.WaitFor(1000) = wrTimeout then Continue; FCriticalSection.Enter; try if Length(FQueue) = 0 then begin FEvent.ResetEvent; FCriticalSection.Leave; Continue; end; // 取出队列第一个消息 CurrentMsg := FQueue[0]; // 移除第一个消息 for I := 0 to High(FQueue)-1 do FQueue[I] := FQueue[I+1]; SetLength(FQueue, Length(FQueue)-1); if Length(FQueue) = 0 then FEvent.ResetEvent; finally FCriticalSection.Leave; end; // 处理日志写入 Today := Date; if Today <> FCurrentDate then OpenLogFile(Today); try Writeln(FLogFile, CurrentMsg.Msg); // 加换行让日志更易读 Flush(FLogFile); // 强制写入磁盘,避免缓存丢失 except // 这里可以加异常日志,比如写入失败时记录到临时文件 end; // 处理UI更新(只在需要时用Synchronize,且只做UI操作) if CurrentMsg.ShowUI and not MainForm.Form1.CheckBox1.Checked then begin Synchronize(procedure begin UpdateMemo(CurrentMsg.Msg); end); end; end; CloseLogFile; end; initialization LogThread := TLogThread.Create; finalization LogThread.Terminate; LogThread.Free; end.
三、使用方法
在任何线程(包括主线程)里直接调用:
LogThread.AddMsg('用户登录成功', True);
第一个参数是日志内容,第二个参数控制是否显示到Memo里。
四、其他优化建议
- 可以给日志自动加上时间戳,比如在
AddMsg里拼接FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ': ' + AMsg - 如果日志量极大,可以考虑批量写入队列,减少线程唤醒次数
- 异常处理部分可以补充逻辑,比如文件被占用时尝试重试,或者切换到临时日志文件
内容的提问来源于stack exchange,提问作者Luiz Felipe
相关产品推荐
相关产品推荐

