Безопасное создание собственного потока в качестве базы для потомков - PullRequest
0 голосов
/ 21 февраля 2012

Я пишу пользовательский поток, который включает некоторые дополнительные функции. Часть, в которой я запутался, заключается в том, как обрабатывать процедуру Execute, все еще ожидая, что она перейдет в более унаследованные реализации.

Мой пользовательский поток переопределяет процедуру Execute и добавляет некоторые из моих собственных вещей, такие как события OnStart, OnStop и OnException, а также возможности зацикливания. Я не уверен, как спроектировать это так, чтобы он мог использоваться в дальнейшем унаследованном потоке.

Как сделать возможным дальнейшее наследование этого пользовательского потока при сохранении функциональности Execute?

Вот процедура 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 и все же будут чрезвычайно полезны для многих будущих проектов. К дополнительным возможностям относятся, в частности, события OnStart и OnStop (аналогично тому, как работает служба), CoInitialize встроенные (и используемые только в случае указания, default = false), повторное выполнение (default = false) и задержка между исполнениями (по умолчанию = 0).

Ответы [ 3 ]

4 голосов
/ 21 февраля 2012

Я согласен с Робом.Не используйте событие, используйте виртуальный метод.Но даже если бы вы использовали событие и использовали его «назначенность», чтобы сигнализировать, есть ли работа, которую нужно сделать, вам нужно будет защитить элемент 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;

Поскольку SetCommand (установщик Команды) может быть вызван из любого старого потока, установка члена 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;

WaitForNewCommand возвращается, когда MyEvent сигнализируется.Это делается, когда команда назначена, но также и когда (текущая) команда отменяется, когда поток завершается и т. Д. Обратите внимание, что Termination проверяется снова перед вызовом CommandExecute.Это сделано потому, что, когда 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 - это место, где через несколько уровней косвенности вызывается метод Execute FCommand игде настоящая работа команды сделана.Вот почему этот вызов напрямую защищен блоком try-exc.Кроме того, каждая команда сама по себе отвечает за безопасность потока в отношении ресурсов, которые она использует.

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;

Если естьобработка "finally" в методе CommandExecute должна выполняться независимо от исключений, вызванных другими вызовами в этом процессе, вам придется использовать вложенные try-finally, чтобы убедиться в этом.Вышеупомянутый метод был упрощен от нашего реального кода, и фактический блок finally - это набор вложенных попыток finally, чтобы гарантировать, что DoThreadFinished и т. Д. Вызывается независимо от исключений в FinalizeCommand (и других промежуточных вызовах).

4 голосов
/ 21 февраля 2012

Не беспокойтесь о том, как сделать безопасным переопределение Execute. Потребители, которые переопределяют метод Execute вашего потока, не будут работать правильно (потому что они будут помещать свои собственные операции вокруг вашего бухгалтерского кода вместо в it). Предоставьте новый виртуальный метод для вызова потомков. Вы можете назвать это Run, например, используя Indy's TIdThread в качестве руководства. Он делает многое из того, что вы планируете.

3 голосов
/ 21 февраля 2012

Не вызывать Sleep (FExecDelay) - это вызов ядра, который потомок может не захотеть делать, поэтому:

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

Это дает пользователю возможность полностью отказаться от вызова ядра.

У меня проблемы с TThread.Synchronize - я бы не хотел заставлять какого-либо пользователя вызывать его.

TBH, я более привык помещать код в класс объектов, который не является потомком TThread, т.е. 'Ttask', который имеет метод 'work', который вызывается из TThread. Наличие отдельного класса для работы является гораздо более гибким и более безопасным, чем добавление элементов данных и методов к потомку TThread - он легко ставится в очередь, ставится в очередь, PostMessaged и т. Д. Это, а также отсутствие доступа к экземпляру TThread останавливает разработчиков, использующих TThread. Синхронизируйте, TThread.WaitFor и TThread.OnTerminate, что повышает надежность и производительность приложения.

...