【问题标题】:Execute thread on some object property change在某些对象属性更改上执行线程
【发布时间】:2025-12-11 16:55:02
【问题描述】:

我创建了一个日志应用程序,并且我有一个带有一些字符串属性的 LogEvent 对象。我想让这个日志记录异步并在另一个线程中不阻塞应用程序 GUI 线程。

想法是,当我启动应用程序时,一些 LogEventThread 一直在后台运行。如果 LogEvent 属性发生变化,则执行线程,执行后线程暂停并等待另一个 LogEvent 对象属性更改,如果捕获到新的属性更改,则再次运行。

设计这个的最佳实践是什么?

编辑:

我创建了一个示例。请告诉我我是否走在正确的道路上。

我有一个 Form1:

unit MainWindow;

interface

uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, TrackEventSenderThread, Generics.Collections, TrackEvent;

type
  TForm1 = class(TForm)
    btnTest: TButton;
    procedure FormCreate(Sender: TObject);
    procedure btnTestClick(Sender: TObject);

  private
    teqTrackEventSenderThread: TTrackEventSenderThread;
    trackEventQueue: TThreadedQueue<TTrackEvent>;

  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.btnTestClick(Sender: TObject);
var
  trackEvent: TTrackEvent;

begin
  trackEvent := TTrackEvent.Create;
  trackEvent.Category := 'test';
  trackEvent.Action := 'test';

  trackEventQueue.PushItem(trackEvent);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  trackEventQueue := TThreadedQueue<TTrackEvent>.Create;

  teqTrackEventSenderThread := TTrackEventSenderThread.Create(True);
  teqTrackEventSenderThread.TrackEventQueue := trackEventQueue;

  teqTrackEventSenderThread.Start;
end;

end.

TrackEvent 类:

unit TrackEvent;

interface

type
  TTrackEvent = class(TObject)

  private
    sCategory: string;
    sAction: string;

  public
    property Category: string read sCategory write sCategory;
    property Action: string read sAction write sAction;

  end;

implementation

end.

和线程类:

unit TrackEventSenderThread;

interface

uses Classes, Generics.Collections, TrackEvent;

type
  TTrackEventSenderThread = class(TThread)

  private
    trackEvent: TTrackEvent;
    teqTrackEventQueue: TThreadedQueue<TTrackEvent>;

  public
    constructor Create(CreateSuspended: Boolean);
    property TrackEventQueue: TThreadedQueue<TTrackEvent> read teqTrackEventQueue write teqTrackEventQueue;


  protected
    procedure Execute; override;

  end;

implementation

constructor TTrackEventSenderThread.Create(CreateSuspended: Boolean);
begin
  inherited;
end;

procedure TTrackEventSenderThread.Execute;
begin
  while not Terminated do
  begin
    if teqTrackEventQueue.QueueSize > 0 then
    begin
      trackEvent := teqTrackEventQueue.PopItem;

      //send data to server
    end;
  end;
end;

end.

【问题讨论】:

    标签: multithreading delphi delphi-xe


    【解决方案1】:

    您可以构建一个用于Producer-Consumer 模型的线程安全队列类。您的 TThread 后代类应该拥有这个 Queue 类的一个实例。

    当您启动应用程序时,您的队列为空,并且您的日志记录线程被阻塞等待队列。当您从主线程将新字符串推送到队列中时,您的队列会向日志线程发出脉冲,您的日志线程会唤醒并从队列中弹出项目,直到队列再次为空。

    要在Delphi 2010中实现队列,可以使用TQueue泛型类作为基类型,使用System.TMonitor进行同步。在 Delphi XE 中,已经有一个类可以为您实现此功能,名为 TThreadedQueue。因此,如果您使用的是 Delphi XE,请创建一个 TThreadedQueue 实例,并在您的日志记录线程中尝试调用其 PopItem() 方法。

    编辑:

    这是一个接收字符串日志的示例日志线程:

    unit uLoggingThread;
    
    interface
    
    uses
      SysUtils, Classes, Generics.Collections, SyncObjs {$IFDEF MSWINDOWS} , Windows {$ENDIF};
    
    type
      TLoggingThread = class(TThread)
      private
        FFileName : string;
        FLogQueue : TThreadedQueue<string>;
      protected
        procedure Execute; override;
      public
        constructor Create(const FileName: string);
        destructor Destroy; override;
        property LogQueue: TThreadedQueue<string> read FLogQueue;
      end;
    
    implementation
    
    { TLoggingThread }
    
    constructor TLoggingThread.Create(const FileName: string);
    begin
      inherited Create(False);
      FFileName := FileName;
      FLogQueue := TThreadedQueue<string>.Create;
    end;
    
    destructor TLoggingThread.Destroy;
    begin
      FLogQueue.Free;
      inherited;
    end;
    
    procedure TLoggingThread.Execute;
    var
      LogFile : TFileStream;
      FileMode : Word;
      ALog : string;
    begin
      NameThreadForDebugging('Logging Thread');
    //  FreeOnTerminate := True;
    
      if FileExists(FFileName) then
        FileMode := fmOpenWrite or fmShareDenyWrite
      else
        FileMode := fmCreate or fmShareDenyWrite;
      LogFile := TFileStream.Create(FFileName,FileMode);
      try
        while not Terminated do
        begin
          ALog := FLogQueue.PopItem;
          if (ALog <> '')  then
            LogFile.Write(ALog[1],Length(ALog)*SizeOf(Char));
        end;
      finally
        LogFile.Free;
      end;
    end;
    
    end.
    

    此 TThread 后代使用 TThreadedQueue 对象作为缓冲区。调用 FLogQueue.PopItem 时,如果队列为空,则线程进入睡眠状态,并等待直到有东西被推入队列。当队列中有可用的项目时,线程将其弹出,并将其写入文件。这是一个非常简单的代码,只是让您了解应该做什么的基础知识。

    这是一个在主线程上下文中运行的表单的示例代码,并且正在记录一个示例消息:

    unit fMain;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, StdCtrls, uLogginThread;
    
    type
      TfrmMain = class(TForm)
        btnAddLog: TButton;
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
        procedure btnAddLogClick(Sender: TObject);
      private
        FLoggingThread : TLoggingThread;
      public
        { Public declarations }
      end;
    
    var
      frmMain: TfrmMain;
    
    implementation
    
    {$R *.dfm}
    
    procedure TfrmMain.FormCreate(Sender: TObject);
    begin
      FLoggingThread := TLoggingThread.Create(ExtractFilePath(Application.ExeName) + 'Logs.txt');
    end;
    
    procedure TfrmMain.FormDestroy(Sender: TObject);
    begin
      FLoggingThread.Terminate;
      FLoggingThread.LogQueue.DoShutDown;
      FLoggingThread.WaitFor;
      FreeAndNil(FLoggingThread);
    end;
    
    procedure TfrmMain.btnAddLogClick(Sender: TObject);
    begin
      FLoggingThread.LogQueue.PushItem('This is a test log. ');
    end;
    
    end.
    

    这里在表单初始化时创建了一个 TLoggingThread 的实例。当您按下 btnAddLog 时,一条示例消息将通过其 LogQueue 属性发送到记录器线程。 注意线程是如何在 FormDestroy 方法中终止的。首先线程被通知终止,然后我们告诉 LogQueue 释放任何锁,所以如果 logger 线程正在等待队列,它会在调用 DoShutDown 后自动唤醒。然后我们调用WaitFor方法等待线程结束,最终销毁线程实例。

    祝你好运

    【讨论】:

    • 如果我错了,请让我理解并纠正我。我在 Form1 中创建了一个 TThreadedQueue 对象,并在那里创建了 TThread 对象。我将 TThreadedQueue 对象作为 TThread 对象的属性。然后调用 TThread.Start() 方法。在 Form1 on button 单击我将 LogEvent 对象添加到队列中。在 Execute 方法中,我调用 PopItem() 方法直到队列为空?
    • 在我的问题中添加了示例,请查看是否可以。
    • @evilone:在我的答案中添加了示例,以帮助您更好地理解它。我希望它现在有用。
    • 谢谢,非常有帮助!!!现在我明白了……如果将项目推入队列,线程会自动启动,我们不需要启动调用 TThread.Start() 的线程。
    【解决方案2】:

    在多线程应用程序中,使用 TEvent 允许一个线程发出信号 到事件具有的其他线程 发生了。

    http://docwiki.embarcadero.com/VCL/en/SyncObjs.TEvent

    【讨论】:

    • 我只有一个线程执行发送日志事件到服务器的动作
    • 据我了解您的问题,您有两个线程:主线程 (GUI) 和日志线程(将事件发送到服务器)。
    【解决方案3】:

    我会在push()pop() 中使用带有临界区的字符串队列。在线程内部,我会弹出字符串并记录它们。在 GUI 线程中,我会将字符串推送到队列中。我之前做过类似的事情,实现起来很简单。


    编辑

    界面:

    TThreadSafeQueue = class(TQueue)
    protected
      procedure PushItem(AItem: Pointer); override;
      function PopItem: Pointer; override;
      function PeekItem: Pointer; override;
    end;
    
    var
      CRITICAL_SECTION: TCriticalSection;
    

    实施:

    function TThreadSafeQueue.PeekItem: Pointer;
    begin
      CRITICAL_SECTION.Enter;
      Result := inherited PeekItem;
      CRITICAL_SECTION.Leave;
    end;
    
    function TThreadSafeQueue.PopItem: Pointer;
    begin
      CRITICAL_SECTION.Enter;
      Result := inherited PopItem;
      CRITICAL_SECTION.Leave;
    end;
    
    procedure TThreadSafeQueue.PushItem(AItem: Pointer);
    begin
      CRITICAL_SECTION.Enter;
      inherited PushItem(AItem);
      CRITICAL_SECTION.Leave;
    end;
    

    初始化

    CRITICAL_SECTION := TCriticalSection.Create;
    

    完成

    FreeAndNil(CRITICAL_SECTION);
    

    此代码使用指向对象的指针,但您可以使用字符串列表或数组或任何最适合您的目的的对象内部的字符串创建存储,并更改 pop 和 push 方法以在您自己的存储上操作。


    编辑

    类似这样的:

    procedure TMyThread.Execute;
    var
      Msg: string;
    begin
      while not Terminated do
      begin
        if FQueue.Count > 0 then
        begin
          Msg := FQueue.pop();
          PerformLog(Msg); {Whatever your logging method is}
        end;
        Sleep(0); 
      end;
    end;      
    

    【讨论】:

    • 是的,我认为队列也可以使用,因为它记录到服务器并且它可能需要在一段时间内保存多个对象。你能给我一些简单的例子,然后这个答案将被接受:)
    • 上面添加的代码只是一个粗略的建议。如果您希望我为您写一个完整的工作示例,我可以提交报价吗?
    • 谢谢,我想我可以用 TThreadedQueue 做到这一点,就像@vcldeveloper 建议的那样。只是我不明白,我怎么把这个TThreadedQueue发送给TThread类,就像一个属性一样?
    最近更新 更多