2013-02-22 56 views
1

我在写一个写事件日志的线程。当应用程序关闭时(优雅地),我需要确保该线程在释放日志之前完成其保存日志的工作。如果我直接调用Free到线程,它不应该立即被销毁,它应该等到线程完成并且没有更多的工作要做。如何让线程在free'd之前完成其工作?

这里是我有我的线程的执行奠定了:

procedure TEventLogger.Execute; 
var 
    L: TList; 
    E: PEventLog; //Custom record pointer 
begin 
    while not Terminated do begin //Repeat continuously until terminated 
    try 
     E:= nil; 
     L:= LockList; //Acquire locked queue of logs to be written 
     try 
     if L.Count > 0 then begin //Check if any logs exist in queue 
      E:= PEventLog(L[0]); //Get next log from queue 
      L.Delete(0); //Remove log from queue 
     end; 
     finally 
     UnlockList; 
     end; 
     if E <> nil then begin 
     WriteEventLog(E); //Actual call to save log 
     end; 
    except 
     //Handle exception... 
    end; 
    Sleep(1); 
    end; 
end; 

而这里的析构函数...

destructor TEventLogger.Destroy; 
begin 
    ClearQueue; //I'm sure this should be removed 
    FQueue.Free; 
    DeleteCriticalSection(FListLock); 
    inherited; 
end; 

现在我已经知道,在当时当Free被调用时,我应该提高一个标志,使得不可能将更多的日志添加到队列中 - 它只需要完成已经存在的内容。我的问题是,我知道当线程被释放时,上面的代码将被强制切断。

Free被调用时,我应该如何让这个线程完成它的工作?或者如果这是不可能的,一般来说应该如何构造这个线程才能实现这一点?

回答

11

如果我直接调用Free给线程,它不应该立即被销毁,它应该等到线程完成并且没有更多的工作要做。

我认为你对销毁线程时会发生什么有轻微的误解。当您在TThread调用Free,下面在析构函数发生了:

  1. Terminate被调用。
  2. WaitFor被调用。
  3. 然后运行线程的析构函数的其余部分。

换句话说,调用Free已经做了你所要求的,即通知线程方法,它需要终止,然后等待它。

由于您正在控制线程的Execute方法,因此一旦检测到Terminated标志已设置,您就可以在那里完成多少工作。正如雷米所说,你可以重写DoTerminate并在那里做最后的工作。


对于什么是值得的,这是一个糟糕的方式来实现一个队列。对Sleep(1)的呼叫立刻跳到我身上。你需要的是一个阻塞队列。您清空队列然后等待事件。当生产者添加到队列中时,事件会发出信号,以便您的线程可以唤醒。

+0

权,但你可以在上面我的代码看,我在一个循环',而不是终止...' – 2013-02-22 15:44:24

+0

所以,Terminated将被设置为True,然后Execute将返回。 – 2013-02-22 15:45:18

+0

但是仍然有剩余的项目需要保存,循环需要继续,直到队列为空。 – 2013-02-22 15:46:01

3

修改你的代码,我建议检查的,而最后一个队列数为好,注意变量LastCount我介绍这里:

procedure TEventLogger.Execute; 
var 
    L: TList; 
    E: PEventLog; //Custom record pointer 
    LastCount: integer; 
begin 
    LastCount:=0;//counter warning 
    while not (Terminated and (LastCount=0)) do begin //Repeat continuously until terminated 
    try 
     E:= nil; 
     L:= LockList; //Acquire locked queue of logs to be written 
     try 
     LastCount:=L.Count; 
     if LastCount > 0 then begin //Check if any logs exist in queue 
      E:= PEventLog(L[0]); //Get next log from queue 
      L.Delete(0); //Remove log from queue 
     end; 
     finally 
     UnlockList; 
     end; 
     if E <> nil then begin 
     WriteEventLog(E); //Actual call to save log 
     end; 
    except 
     //Handle exception... 
    end; 
    Sleep(1); 
    end; 
end; 
+0

您在分配之前正在阅读'I'。当然,这个变量可能会有更好的名字。 – 2013-02-22 16:36:41

+0

@大卫它并不一定很重要,但因为无论如何这个代码第一次运行时它不会被“终止”。 – 2013-02-22 16:45:48

+2

@杰里嗯,我喜欢这样的事情! – 2013-02-22 16:48:43

2

这里是一个“懒”事件记录线程,这将保存所有事件队列。

unit EventLogger; 

interface 

uses 
    Classes, SyncObjs, Contnrs; 

type 
    TEventItem = class 
    TimeStamp : TDateTime; 
    Info : string; 
    end; 

    TEventLogger = class(TThread) 
    private 
    FStream : TStream; 
    FEvent : TEvent; 
    FQueue : TThreadList; 
    protected 
    procedure TerminatedSet; override; 
    procedure Execute; override; 
    procedure WriteEvents; 
    function GetFirstItem(out AItem : TEventItem) : Boolean; 
    public 
    constructor Create; overload; 
    constructor Create(CreateSuspended : Boolean); overload; 
    destructor Destroy; override; 

    procedure LogEvent(const AInfo : string); 
    end; 

implementation 

uses 
    Windows, SysUtils; 

{ TEventLogger } 

constructor TEventLogger.Create(CreateSuspended : Boolean); 
begin 
    FEvent := TEvent.Create; 
    FQueue := TThreadList.Create; 

    inherited; 
end; 

constructor TEventLogger.Create; 
begin 
    Create(False); 
end; 

destructor TEventLogger.Destroy; 
begin 
    // first the inherited part 
    inherited; 
    // now freeing the internal instances 
    FStream.Free; 
    FQueue.Free; 
    FEvent.Free; 
end; 

procedure TEventLogger.Execute; 
var 
    LFinished : Boolean; 
begin 
    inherited; 
    LFinished := False; 
    while not LFinished do 
    begin 

     // waiting for event with 20 seconds timeout 
     // maybe terminated or full queue 
     WaitForSingleObject(FEvent.Handle, 20000); 

     // thread will finished if terminated 
     LFinished := Terminated; 

     // write all events from queue 
     WriteEvents; 

     // if the thread gets terminated while writing 
     // it will be still not finished ... and therefor one more loop 

    end; 
end; 

function TEventLogger.GetFirstItem(out AItem : TEventItem) : Boolean; 
var 
    LList : TList; 
begin 
    LList := FQueue.LockList; 
    try 
    if LList.Count > 0 
    then 
     begin 
     AItem := TEventItem(LList[0]); 
     LList.Delete(0); 
     Result := True; 
     end 
    else 
     Result := False; 
    finally 
    FQueue.UnlockList; 
    end; 
end; 

procedure TEventLogger.LogEvent(const AInfo : string); 
var 
    LList : TList; 
    LItem : TEventItem; 
begin 
    if Terminated 
    then 
    Exit; 

    LItem   := TEventItem.Create; 
    LItem.TimeStamp := now; 
    LItem.Info  := AInfo; 

    LList := FQueue.LockList; 
    try 

    LList.Add(LItem); 

    // if the queue is "full" we will set the event 

    if LList.Count > 50 
    then 
     FEvent.SetEvent; 

    finally 
    FQueue.UnlockList; 
    end; 

end; 

procedure TEventLogger.TerminatedSet; 
begin 
    // this is called if the thread is terminated 
    inherited; 
    FEvent.SetEvent; 
end; 

procedure TEventLogger.WriteEvents; 
var 
    LItem : TEventItem; 
    LStream : TStream; 
begin 
    // retrieve the first event in list 
    while GetFirstItem(LItem) do 
    try 

     // writing the event to a file 

     if not Assigned(FStream) 
     then 
     FStream := TFileStream.Create(ChangeFileExt(ParamStr(0), '.log'), fmCreate or fmShareDenyWrite); 

     // just a simple log row 
     LStream := 
     TStringStream.Create( 
      Format( 
      '[%s] %s : %s', 
      // when it is written to file 
      [FormatDateTime('dd.mm.yyyy hh:nn:ss.zzz', now), 
      // when did it happend 
      FormatDateTime('dd.mm.yyyy hh:nn:ss.zzz', LItem.TimeStamp), 
      // whats about 
      LItem.Info]) + sLineBreak, 
      TEncoding.UTF8); 
     try 
     LStream.Seek(0, soFromBeginning); 
     FStream.CopyFrom(LStream, LStream.Size); 
     finally 
     LStream.Free; 
     end; 

    finally 
     LItem.Free; 
    end; 
end; 

end. 
+0

这将所有东西混合成一个大超级类。最好使用一个良好的固体阻塞队列。然后编写这个来建立你的生产者/消费者代码。 – 2013-02-22 17:30:48

+0

@DavidHeffernan多数民众赞成这是正确的,但这里的重点是一个简单的输出到文件的工作示例...只是为了显示线程如何与同步部分作为'事件'和'WaitForSingleObject'。我不打算为OP编写一个完整的日志记录框架:o) – 2013-02-22 17:35:45

+0

谢谢,但我已经拥有自己的机制来保存并且不保存到文件,它保存到Windows事件日志中,这与日志不相关我的信息包含在我的问题中。我确信这是其他场景的完美解决方案,但我的课程已经完成(除了我所问的一件事外)。 – 2013-02-22 18:04:08

5

这是我如何编写消费者线程。拼图的第一部分是一个阻塞队列。我看起来是这样的:

unit BlockingQueue; 

interface 

uses 
    Windows, SyncObjs, Generics.Collections; 

type 
    TBlockingQueue<T> = class 
    //see Duffy, Concurrent Programming on Windows, pp248 
    private 
    FCapacity: Integer; 
    FQueue: TQueue<T>; 
    FLock: TCriticalSection; 
    FNotEmpty: TEvent; 
    function DoEnqueue(const Value: T; IgnoreCapacity: Boolean): Boolean; 
    public 
    constructor Create(Capacity: Integer=-1);//default to unbounded 
    destructor Destroy; override; 
    function Enqueue(const Value: T): Boolean; 
    procedure ForceEnqueue(const Value: T); 
    function Dequeue: T; 
    end; 

implementation 

{ TBlockingQueue<T> } 

constructor TBlockingQueue<T>.Create(Capacity: Integer); 
begin 
    inherited Create; 
    FCapacity := Capacity; 
    FQueue := TQueue<T>.Create; 
    FLock := TCriticalSection.Create; 
    FNotEmpty := TEvent.Create(nil, True, False, ''); 
end; 

destructor TBlockingQueue<T>.Destroy; 
begin 
    FNotEmpty.Free; 
    FLock.Free; 
    FQueue.Free; 
    inherited; 
end; 

function TBlockingQueue<T>.DoEnqueue(const Value: T; IgnoreCapacity: Boolean): Boolean; 
var 
    WasEmpty: Boolean; 
begin 
    FLock.Acquire; 
    Try 
    Result := IgnoreCapacity or (FCapacity=-1) or (FQueue.Count<FCapacity); 
    if Result then begin 
     WasEmpty := FQueue.Count=0; 
     FQueue.Enqueue(Value); 
     if WasEmpty then begin 
     FNotEmpty.SetEvent; 
     end; 
    end; 
    Finally 
    FLock.Release; 
    End; 
end; 

function TBlockingQueue<T>.Enqueue(const Value: T): Boolean; 
begin 
    Result := DoEnqueue(Value, False); 
end; 

procedure TBlockingQueue<T>.ForceEnqueue(const Value: T); 
begin 
    DoEnqueue(Value, True); 
end; 

function TBlockingQueue<T>.Dequeue: T; 
begin 
    FLock.Acquire; 
    Try 
    while FQueue.Count=0 do begin 
     FLock.Release; 
     Try 
     FNotEmpty.WaitFor; 
     Finally 
     FLock.Acquire; 
     End; 
    end; 
    Result := FQueue.Dequeue; 
    if FQueue.Count=0 then begin 
     FNotEmpty.ResetEvent; 
    end; 
    Finally 
    FLock.Release; 
    End; 
end; 

end. 

它完全是线程安全的。任何线程都可以入队。任何线程都可以出队。如果队列为空,则出队功能将被阻塞。队列可以以有界模式或无界模式运行。

接下来,我们需要一个与这样的队列一起工作的线程。线程简单地将作业从队列中取出,直到它被告知终止。我的消费线程如下所示:

unit ConsumerThread; 

interface 

uses 
    SysUtils, Classes, BlockingQueue; 

type 
    TConsumerThread = class(TThread) 
    private 
    FQueue: TBlockingQueue<TProc>; 
    FQueueFinished: Boolean; 
    procedure SetQueueFinished; 
    protected 
    procedure TerminatedSet; override; 
    procedure Execute; override; 
    public 
    constructor Create(Queue: TBlockingQueue<TProc>); 
    end; 

implementation 

{ TConsumerThread } 

constructor TConsumerThread.Create(Queue: TBlockingQueue<TProc>); 
begin 
    inherited Create(False); 
    FQueue := Queue; 
end; 

procedure TConsumerThread.SetQueueFinished; 
begin 
    FQueueFinished := True; 
end; 

procedure TConsumerThread.TerminatedSet; 
begin 
    inherited; 
    //ensure that, if the queue is empty, we wake up the thread so that it can quit 
    FQueue.ForceEnqueue(SetQueueFinished); 
end; 

procedure TConsumerThread.Execute; 
var 
    Proc: TProc; 
begin 
    while not FQueueFinished do begin 
    Proc := FQueue.Dequeue(); 
    Proc(); 
    Proc := nil;//clear Proc immediately, rather than waiting for Dequeue to return since it blocks 
    end; 
end; 

end. 

这具有您正在寻找的属性。也就是说,当线程被销毁时,它会在完成析构函数之前处理所有挂起的任务。

要看到它的行动,这里是一个简短的演示程序:

unit Main; 

interface 

uses 
    Windows, SysUtils, Classes, Controls, Forms, StdCtrls, 
    BlockingQueue, ConsumerThread; 

type 
    TMainForm = class(TForm) 
    Memo1: TMemo; 
    TaskCount: TEdit; 
    Start: TButton; 
    Stop: TButton; 
    procedure StartClick(Sender: TObject); 
    procedure StopClick(Sender: TObject); 
    private 
    FQueue: TBlockingQueue<TProc>; 
    FThread: TConsumerThread; 
    procedure Proc; 
    procedure Output(const Msg: string); 
    end; 

implementation 

{$R *.dfm} 

procedure TMainForm.Output(const Msg: string); 
begin 
    TThread.Synchronize(FThread, 
    procedure 
    begin 
     Memo1.Lines.Add(Msg); 
    end 
); 
end; 

procedure TMainForm.Proc; 
begin 
    Output(Format('Consumer thread ID: %d', [GetCurrentThreadId])); 
    Sleep(1000); 
end; 

procedure TMainForm.StartClick(Sender: TObject); 
var 
    i: Integer; 
begin 
    Memo1.Clear; 
    Output(Format('Main thread ID: %d', [GetCurrentThreadId])); 
    FQueue := TBlockingQueue<TProc>.Create; 
    FThread := TConsumerThread.Create(FQueue); 
    for i := 1 to StrToInt(TaskCount.Text) do 
    FQueue.Enqueue(Proc); 
end; 

procedure TMainForm.StopClick(Sender: TObject); 
begin 
    Output('Stop clicked, calling thread destructor'); 
    FreeAndNil(FThread); 
    Output('Thread destroyed'); 
    FreeAndNil(FQueue); 
end; 

end. 

object MainForm: TMainForm 
    Caption = 'MainForm' 
    ClientHeight = 560 
    ClientWidth = 904 
    object Memo1: TMemo 
    Left = 0 
    Top = 96 
    Width = 904 
    Height = 464 
    Align = alBottom 
    end 
    object TaskCount: TEdit 
    Left = 8 
    Top = 8 
    Width = 121 
    Height = 21 
    Text = '10' 
    end 
    object Start: TButton 
    Left = 8 
    Top = 48 
    Width = 89 
    Height = 23 
    Caption = 'Start' 
    OnClick = StartClick 
    end 
    object Stop: TButton 
    Left = 120 
    Top = 48 
    Width = 75 
    Height = 23 
    Caption = 'Stop' 
    OnClick = StopClick 
    end 
end 
+1

这是一段很棒的代码,David。很简单。 – 2013-03-26 13:28:43

相关问题