Заблокирован поток Delphi

У меня возникает проблема с тупиком при уничтожении некоторых потоков. Я пытался отладить проблему, но тупик никогда не существует при отладке в среде IDE, возможно, из-за низкой скорости событий в среде IDE.

Проблема:

Основной поток создает несколько потоков при запуске приложения. Нити всегда живы и синхронизируются с основным потоком. Никаких проблем. Нити уничтожаются, когда приложение заканчивается (mainform.onclose) следующим образом:

thread1.terminate;
thread1.waitfor;
thread1.free;

и т.д.

Но иногда один из потоков (который регистрирует некоторую строку в заметке, используя синхронизацию) блокирует все приложение при закрытии. Я подозреваю, что поток синхронизируется, когда я вызываю waitform, и происходит событие harmaggeddon, но это просто предположение, потому что тупик никогда не бывает при отладке (или я никогда не смог воспроизвести его в любом случае). Любые советы?

Ответы

Ответ 1

Запись сообщений - это только одна из тех областей, где Synchronize() не имеет никакого смысла. Вместо этого вы должны создать целевой объект журнала, который имеет список строк, защищенный критическим разделом, и добавить к нему свои сообщения журнала. Попросите основной поток VCL удалить сообщения журнала из этого списка и показать их в окне журнала. Это имеет несколько преимуществ:

  • Вам не нужно вызывать Synchronize(), что является просто плохой идеей. Хороший побочный эффект заключается в том, что ваши проблемы с остановкой исчезают.

  • Рабочие потоки могут продолжать свою работу без блокировки обработки событий основного потока или других потоков, пытающихся зарегистрировать сообщение.

  • Производительность увеличивается, так как несколько сообщений могут быть добавлены в окно журнала за один раз. Если вы используете BeginUpdate() и EndUpdate(), это ускорит процесс.

Нет никаких недостатков, которые я вижу - порядок сообщений журнала также сохраняется.

Edit:

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

Вызов Synchronize() из другого потока, кроме основного потока приложений в программе VCL, приведет к блокировке вызывающего потока, переданный код будет выполняться в контексте потока VCL, а затем вызывающий поток будет разблокирован и продолжать работать. Это может быть хорошей идеей во времена однопроцессорных машин, на которых в любом случае может работать только один поток за один раз, но с несколькими процессорами или ядрами это гигантские отходы, и их следует избегать любой ценой. Если у вас 8 рабочих потоков на 8-ядерном компьютере, их вызов Synchronize(), вероятно, ограничит пропускную способность до некоторой степени возможной.

На самом деле вызов Synchronize() никогда не был хорошей идеей, так как это может привести к взаимоблокировкам. Еще одна убедительная причина не использовать его, когда-либо.

Использование PostMessage() для отправки сообщений журнала будет заботиться о проблеме взаимоблокировки, но у нее есть свои проблемы:

  • Каждая строка журнала приведет к тому, что сообщение будет опубликовано и обработано, что приведет к большим накладным расходам. Невозможно обработать несколько сообщений журнала за один раз.

  • Сообщения Windows могут содержать только данные размера машинного слова в параметрах. Поэтому отправка строк невозможна. Отправка строк после приведения типа PChar небезопасна, так как строка может быть освобождена к моменту обработки сообщения. Выделение памяти в рабочем потоке и освобождение этой памяти в потоке VCL после обработки сообщения является выходом. Способ, который добавляет еще больше накладных расходов.

  • Очереди сообщений в Windows имеют конечный размер. Проводка слишком большого количества сообщений может привести к тому, что очередь будет заполнена, а сообщения будут удалены. Это не очень хорошо, и вместе с предыдущим пунктом это приводит к утечкам памяти.

  • Все сообщения в очереди будут обработаны до того, как будут созданы какие-либо сообщения таймера или краски. Таким образом, постоянный поток многих опубликованных сообщений может привести к тому, что программа перестанет реагировать.

Структура данных, которая собирает сообщения журнала, может выглядеть так:

type
  TLogTarget = class(TObject)
  private
    fCritSect: TCriticalSection;
    fMsgs: TStrings;
  public
    constructor Create;
    destructor Destroy; override;

    procedure GetLoggedMsgs(AMsgs: TStrings);
    procedure LogMessage(const AMsg: string);
  end;

constructor TLogTarget.Create;
begin
  inherited;
  fCritSect := TCriticalSection.Create;
  fMsgs := TStringList.Create;
end;

destructor TLogTarget.Destroy;
begin
  fMsgs.Free;
  fCritSect.Free;
  inherited;
end;

procedure TLogTarget.GetLoggedMsgs(AMsgs: TStrings);
begin
  if AMsgs <> nil then begin
    fCritSect.Enter;
    try
      AMsgs.Assign(fMsgs);
      fMsgs.Clear;
    finally
      fCritSect.Leave;
    end;
  end;
end;

procedure TLogTarget.LogMessage(const AMsg: string);
begin
  fCritSect.Enter;
  try
    fMsgs.Add(AMsg);
  finally
    fCritSect.Leave;
  end;
end;

Многие потоки могут одновременно вызывать LogMessage(), ввод критического раздела будет сериализовывать доступ к списку, а после добавления их сообщений потоки могут продолжать свою работу.

Это оставляет вопрос о том, как поток VCL знает, когда вызывать GetLoggedMsgs(), чтобы удалить сообщения из объекта и добавить их в окно. Версия для бедных - это таймер и опрос. Лучшим способом было бы вызвать PostMessage() при добавлении сообщения журнала:

procedure TLogTarget.LogMessage(const AMsg: string);
begin
  fCritSect.Enter;
  try
    fMsgs.Add(AMsg);
    PostMessage(fNotificationHandle, WM_USER, 0, 0);
  finally
    fCritSect.Leave;
  end;
end;

У этой проблемы еще слишком много сообщений. Сообщение должно быть отправлено только после обработки предыдущего:

procedure TLogTarget.LogMessage(const AMsg: string);
begin
  fCritSect.Enter;
  try
    fMsgs.Add(AMsg);
    if InterlockedExchange(fMessagePosted, 1) = 0 then
      PostMessage(fNotificationHandle, WM_USER, 0, 0);
  finally
    fCritSect.Leave;
  end;
end;
Тем не менее, это может быть улучшено. Использование таймера решает проблему отправки сообщений, заполняющих очередь. Ниже приведен небольшой класс, который реализует это:
type
  TMainThreadNotification = class(TObject)
  private
    fNotificationMsg: Cardinal;
    fNotificationRequest: integer;
    fNotificationWnd: HWND;
    fOnNotify: TNotifyEvent;
    procedure DoNotify;
    procedure NotificationWndMethod(var AMsg: TMessage);
  public
    constructor Create;
    destructor Destroy; override;

    procedure RequestNotification;
  public
    property OnNotify: TNotifyEvent read fOnNotify write fOnNotify;
  end;

constructor TMainThreadNotification.Create;
begin
  inherited Create;
  fNotificationMsg := RegisterWindowMessage('thrd_notification_msg');
  fNotificationRequest := -1;
  fNotificationWnd := AllocateHWnd(NotificationWndMethod);
end;

destructor TMainThreadNotification.Destroy;
begin
  if IsWindow(fNotificationWnd) then
    DeallocateHWnd(fNotificationWnd);
  inherited Destroy;
end;

procedure TMainThreadNotification.DoNotify;
begin
  if Assigned(fOnNotify) then
    fOnNotify(Self);
end;

procedure TMainThreadNotification.NotificationWndMethod(var AMsg: TMessage);
begin
  if AMsg.Msg = fNotificationMsg then begin
    SetTimer(fNotificationWnd, 42, 10, nil);
    // set to 0, so no new message will be posted
    InterlockedExchange(fNotificationRequest, 0);
    DoNotify;
    AMsg.Result := 1;
  end else if AMsg.Msg = WM_TIMER then begin
    if InterlockedExchange(fNotificationRequest, 0) = 0 then begin
      // set to -1, so new message can be posted
      InterlockedExchange(fNotificationRequest, -1);
      // and kill timer
      KillTimer(fNotificationWnd, 42);
    end else begin
      // new notifications have been requested - keep timer enabled
      DoNotify;
    end;
    AMsg.Result := 1;
  end else begin
    with AMsg do
      Result := DefWindowProc(fNotificationWnd, Msg, WParam, LParam);
  end;
end;

procedure TMainThreadNotification.RequestNotification;
begin
  if IsWindow(fNotificationWnd) then begin
    if InterlockedIncrement(fNotificationRequest) = 0 then
     PostMessage(fNotificationWnd, fNotificationMsg, 0, 0);
  end;
end;

Экземпляр класса можно добавить в TLogTarget, чтобы вызвать событие уведомления в основном потоке, но не более нескольких десятков раз в секунду.

Ответ 2

Рассмотрим замену Synchronize на вызов PostMessage и обработайте это сообщение в форме, чтобы добавить сообщение журнала в записку. Что-то по строкам: (воспринимайте это как псевдокод)

WM_LOG = WM_USER + 1;
...
MyForm = class (TForm)
  procedure LogHandler (var Msg : Tmessage); message WM_LOG;
end;
...
PostMessage (Application.MainForm.Handle, WM_LOG, 0, PChar (LogStr));

Это позволяет избежать всех проблем взаимоблокировки двух потоков, ожидающих друг друга.

EDIT (спасибо Serg за подсказку): Обратите внимание, что передача строки описанным способом небезопасна, так как строка может быть уничтожена до того, как поток VCL использует ее. Как я уже упоминал, это было предназначено только для псевдокода.

Ответ 3

Добавить объект mutex в основной поток. Получите мьютексы при попытке закрыть форму. В другом потоке проверить мьютекс перед синхронизацией в последовательности обработки.

Ответ 4

Это просто:

TMyThread = class(TThread)
protected
  FIsIdle: boolean; 
  procedure Execute; override;
  procedure MyMethod;
public
  property IsIdle : boolean read FIsIdle write FIsIdle; //you should use critical section to read/write it
end;

procedure TMyThread.Execute;
begin
  try
    while not Terminated do
    begin
      Synchronize(MyMethod);
      Sleep(100);
    end;
  finally
    IsIdle := true;
  end;
end;

//thread destroy;
lMyThread.Terminate;
while not lMyThread.IsIdle do
begin
  CheckSynchronize;
  Sleep(50);
end;

Ответ 5

Объект Delphi TThread (и наследующий классы) уже вызывает WaitFor при уничтожении, но зависит от того, создан ли поток с помощью CreateSuspended или нет. Если вы используете CreateSuspended = true для выполнения дополнительной инициализации перед вызовом первого Resume, вам следует подумать о создании собственного конструктора (вызывающего inherited Create(false);), который выполняет дополнительную инициализацию.