安全地构建自定义线程作为后代的基础

时间:2012-02-21 18:20:26

标签: multithreading delphi inheritance delphi-xe2

我正在编写一个包含一些附加功能的自定义线程。我感到困惑的部分是如何处理Execute过程,同时仍然希望它可以下载到更多继承的实现中。

我的自定义线程覆盖了Execute过程并添加了一些我自己的内容,例如事件OnStartOnStopOnException,以及循环功能。我不确定如何以一种希望在进一步继承的线程中进一步使用它的方式来设计它。

如何在保持Execute功能的同时进一步继承此自定义线程?

这是执行程序,因为我已经覆盖了它......

procedure TJDThread.Execute;
begin
  Startup;
  try
    while not Terminated do begin
      if assigned(FOnExecute) then
        FOnExecute(Self);
      if not FRepeatExec then
        Terminate
      else
        if FExecDelay > 0 then
          Sleep(FExecDelay);
    end;
  finally
    Cleanup;
  end;
end;

我打算让FOnExecute实际上是线程的事件,这更像是继承Execute过程的替代 - 类似于服务的工作方式。我不认为这是正确的方法......我如何确保以安全的方式编码?我愿意接受另一种方法而不是事件的建议 - 只要它的目标是制作一个可以继承并进一步执行的自定义TThread

我正在制作的这个自定义线程包含一些原始TThread未附带的附加功能,但对未来的许多项目都非常有用。附加功能特别是OnStartOnStop事件(类似于服务的工作方式),内置CoInitialize(仅在被告知时使用,默认= false),重复执行(默认) = false),以及执行之间的延迟(默认= 0)。

3 个答案:

答案 0 :(得分:4)

不要担心如何安全地覆盖Execute。覆盖线程Execute方法的消费者将无法正常工作(因为他们将自己的操作围绕您的簿记代码而不是中)。为后代调用提供一种新的虚拟方法。您可以将其称为Run,例如,使用Indy的TIdThread作为指南。它做了很多你正在计划的事情。

答案 1 :(得分:4)

我同意罗布。不要使用事件,使用虚方法。但即使您使用该事件并使用其“已分配”来表示是否有工作要做,您仍需要保护FOnExecute成员,因为它可以从不同的线程设置。

在我们的一个线程类中,我们使用命令执行类似的操作:

procedure TCommandThread.SetCommand(const Value: ICommand);
begin
  Lock;
  try
    Assert(not IsAvailable, 'Command should only be set AFTER the thread has been claimed for processing');

    FCommand := Value;

    if Assigned(FCommand) then
      MyEvent.SetEvent;
  finally
    Unlock;
  end;
end;

由于可以从任何ol'线程调用SetCommand(Command的setter),因此设置FCommand成员受线程的临界区保护,该区段通过Lock和Unlock方法锁定和解锁。

完成信号MyEvent是因为我们的线程类使用TEvent成员等待工作。

procedure TCommandThread.Execute;
begin
  LogDebug1.SendFmtMsg('%s.Execute : Started', [ClassName]);

  // keep running until we're terminated
  while not Terminated do
  try
    // wait until we're terminated or cleared for take-off by the threadpool
    if WaitForNewCommand then
      if  Assigned(FCommand)
      and not Terminated then
        // process the command if we're told to do so
        CommandExecute;

  except
    LogGeneral.SendFmtError('%s.Execute : Exception occurred :', [ClassName]);
    LogGeneral.SendException;
  end;

  LogDebug1.SendFmtMsg('%s.Execute : Finished', [ClassName]);
end;

WaitENewCommand在发出MyEvent信号时返回。这是在分配命令时完成的,也是在取消(运行)命令,线程终止等时完成的。注意在调用CommandExecute之前再次检查Terminated。这样做是因为当WaitForNewCommand返回时,我们可能处于分配命令和终止调用的情况。毕竟,发出事件的信号可以从不同的线程完成两次,我们不知道发生了什么时间或顺序。

CommandExecute是一个不同线程类可以覆盖的虚方法。在默认实现中,它提供了围绕命令执行的所有状态处理,因此命令本身可以专注于自己的东西。

procedure TCommandThread.CommandExecute;
var
  ExceptionMessage: string;
begin
  Assert(Assigned(FCommand), 'A nil command was passed to a command handler thread.');
  Assert(Status = chsIdle, 'Attempted to execute non-idle command handler thread');

  // check if the thread is ready for processing
  if IsAvailable then // if the thread is available, there is nothing to do...
    Exit;

  try
    FStatus := chsInitializing;
    InitializeCommand;

    FStatus := chsProcessing;
    try
      ExceptionMessage := '';
      CallCommandExecute;
    except
      on E: Exception do begin
        ExceptionMessage := E.Message;
        LogGeneral.SendFmtError('%s.CommandExecute: Exception occurred during commandhandler thread execution:', [ClassName]);
        LogGeneral.SendException;
      end;
    end;

  finally
      FStatus := chsFinalizing;
      FinalizeCommand;

      FStatus := chsIdle;
      FCommand := nil;

      // Notify threadpool we're done, so it can terminate this thread if necessary :
      DoThreadFinished;

      // Counterpart to ClaimThreadForProcessing which is checked in IsAvailable.
      ReleaseThreadForProcessing; 
  end;
end;

CallCommandExecute是通过几个间接级别调用FCommand的Execute方法以及命令的实际工作完成的地方。这就是使用try-except块直接保护该调用的原因。除此之外,每个Command本身对其使用的资源负责线程安全。

ClaimThreadForProcessing和ReleaseThreadForProcessing用于声明和释放线程。为了速度,他们不使用线程的锁,而是使用互锁机制来更改类'FIsAvailable成员的值,该成员被声明为指针并用作布尔值:

TCommandThread = class(TThread)
  // ...
  FIsAvailable: Pointer;

function TCommandThread.ClaimThreadForProcessing: Boolean;
begin
  Result := Boolean(CompatibleInterlockedCompareExchange(FIsAvailable, Pointer(False), Pointer(True)));
  // (See InterlockedExchange help.)
end;

function TCommandThread.ReleaseThreadForProcessing: Boolean;
begin
  FIsAvailable := Pointer(True);
  Result := IsAvailable;
end;

如果CommandExecute方法中的任何“finally”处理需要完成,而不管该进程中其他调用引发的异常,您将必须使用嵌套的try-finally来确保这种情况。上面的方法从我们的实际代码中简化而且实际的finally块是一组嵌套的try finally,以确保调用DoThreadFinished等而不管FinalizeCommand中的异常(以及其间的其他调用)。

答案 2 :(得分:3)

不要调用Sleep(FExecDelay) - 这是后代可能不想做的内核调用,所以:

if (FExecDelay<>0) then Sleep(FExecDelay);

这使用户可以选择完全避免内核调用。

我遇到TThread.Synchronize问题 - 我不想强迫任何用户调用它。

TBH,我更习惯将代码放入一个不是来自TThread的对象类,即。一个'Ttask',它有一个从TThread调用的'work'方法。拥有一个单独的工作类比向TThread后代添加数据成员和方法更灵活,更安全 - 它很容易排队,排队,PostMessaged等。而且,无法访问TThread实例会阻止开发人员使用TThread。同步,TThread.WaitFor和TThread.OnTerminate,以提高应用程序的可靠性和性能。