Delphi распараллеливает мертвую блокировку

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

Проблема:

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

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

и так далее.

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

14
задан mghie 25 March 2010 в 20:45
поделиться

5 ответов

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

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

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

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

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

Редактировать:

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

Вызов 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 для вызова события уведомления в основном потоке, но не более нескольких десятков раз в секунду.

29
ответ дан 1 December 2019 в 07:06
поделиться

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

2
ответ дан 1 December 2019 в 07:06
поделиться

Рассмотрите возможность замены 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));

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

РЕДАКТИРОВАТЬ (Спасибо Serg за подсказку): обратите внимание, что передача строки описанным способом небезопасна, поскольку строка может быть уничтожена до того, как поток VCL использует ее. Как я уже говорил, это был только псевдокод.

7
ответ дан 1 December 2019 в 07:06
поделиться

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

0
ответ дан 1 December 2019 в 07:06
поделиться

Это просто:

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;
1
ответ дан 1 December 2019 в 07:06
поделиться
Другие вопросы по тегам:

Похожие вопросы: