Question Threads Delphi impasse


J'ai parfois un problème avec une impasse lors de la destruction de certains threads. J'ai essayé de déboguer le problème mais le blocage ne semble jamais exister lors du débogage dans l'EDI, peut-être à cause de la faible vitesse des événements dans l'EDI.

Le problème:

Le thread principal crée plusieurs threads au démarrage de l'application. Les threads sont toujours actifs et synchronisés avec le thread principal. Pas de problèmes du tout. Les threads sont détruits lorsque l'application se termine (mainform.onclose) comme ceci:

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

etc.

Mais parfois, l'un des threads (qui enregistre une chaîne dans un mémo, en utilisant la synchronisation) verrouille l'application entière lors de la fermeture. Je soupçonne que le thread se synchronise lorsque j'appelle waitform et que harmaggeddon se produit, mais ce n'est qu'une supposition car le blocage ne se produit jamais lors du débogage (ou je n'ai jamais pu le reproduire de toute façon). Aucun conseil?


14
2018-03-25 11:35


origine


Réponses:


La journalisation des messages n’est que l’un des domaines où Synchronize() n'a aucun sens. Vous devez plutôt créer un objet cible de journal, qui comporte une liste de chaînes, protégée par une section critique, et y ajouter vos messages de journal. Demandez au thread VCL principal de supprimer les messages de journal de cette liste et de les afficher dans la fenêtre du journal. Cela présente plusieurs avantages:

  • Vous n'avez pas besoin d'appeler Synchronize(), qui est juste une mauvaise idée. L'effet secondaire agréable est que votre genre de problèmes d'arrêt disparaissent.

  • Les threads de travail peuvent continuer leur travail sans bloquer la gestion des événements du thread principal ou d'autres threads essayant de consigner un message.

  • Les performances augmentent, car plusieurs messages peuvent être ajoutés à la fenêtre du journal en une seule fois. Si tu utilises BeginUpdate() et EndUpdate() cela va accélérer les choses.

Je ne vois aucun inconvénient: l'ordre des messages de journal est également préservé.

Modifier:

Je vais ajouter un peu plus d'informations et un peu de code pour jouer, afin d'illustrer qu'il existe de meilleures façons de faire ce que vous devez faire.

Appel Synchronize() à partir d'un thread différent du thread d'application principale dans un programme VCL, le thread appelant sera bloqué, le code transmis sera exécuté dans le contexte du thread VCL, puis le thread appelant sera débloqué et continuera à s'exécuter. C’était peut-être une bonne idée à l’époque des machines monoprocesseurs, sur lesquelles un seul thread pouvait tourner à la fois, mais avec plusieurs processeurs ou cœurs, c’est un gaspillage géant qui devrait être évité à tout prix. Si vous avez 8 threads de travail sur une machine à 8 cœurs, les faire appeler Synchronize() limitera probablement le débit à une fraction de ce qui est possible.

En fait, appeler Synchronize() n'a jamais été une bonne idée, car cela peut conduire à des impasses. Une raison plus convaincante de ne pas l'utiliser, jamais.

En utilisant PostMessage() envoyer les messages de journal prendra en charge le problème de blocage, mais il a ses propres problèmes:

  • Chaque chaîne de journal provoquera l'envoi et le traitement d'un message, ce qui entraînera beaucoup de surcharge. Il n'y a aucun moyen de traiter plusieurs messages de journal en une seule fois.

  • Les messages Windows ne peuvent contenir que des données de taille mot-machine dans les paramètres. L'envoi de chaînes est donc impossible. Envoi de chaînes après un transtypage vers PChar est dangereux, car la chaîne peut avoir été libérée au moment du traitement du message. L'allocation de mémoire dans le thread de travail et la libération de cette mémoire dans le thread VCL après le traitement du message est une solution. Un moyen d'ajouter encore plus de frais généraux.

  • Les files d'attente de messages dans Windows ont une taille finie. La publication d'un trop grand nombre de messages peut entraîner un saturation de la file d'attente et la suppression des messages. Ce n'est pas une bonne chose et, avec le point précédent, cela conduit à des fuites de mémoire.

  • Tous les messages de la file d'attente seront traités avant que des messages de minuterie ou de peinture ne soient générés. Un flux régulier de nombreux messages postés peut donc entraîner le blocage du programme.

Une structure de données qui collecte des messages de journal pourrait ressembler à ceci:

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;

De nombreux threads peuvent appeler LogMessage() en même temps, la saisie de la section critique sérialisera l'accès à la liste, et après avoir ajouté leur message, les threads pourront continuer leur travail.

Cela laisse la question de savoir comment le thread VCL sait quand appeler GetLoggedMsgs() pour supprimer les messages de l'objet et les ajouter à la fenêtre. La version d'un homme pauvre serait d'avoir une minuterie et un sondage. Une meilleure façon serait d'appeler PostMessage() lorsqu'un message de journal est ajouté:

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

Cela a toujours le problème avec trop de messages postés. Un message ne doit être publié que lorsque le précédent a été traité:

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;

Cela peut encore être amélioré, cependant. L'utilisation d'une minuterie résout le problème des messages postés remplissant la file d'attente. Ce qui suit est une petite classe qui implémente ceci:

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;

Une instance de la classe peut être ajoutée à TLogTarget, pour appeler un événement de notification dans le thread principal, au plus quelques dizaines de fois par seconde.


28
2018-03-25 12:18



Envisager de remplacer Synchronize avec un appel à PostMessage et gérez ce message dans le formulaire pour ajouter un message de journal au mémo. Quelque chose du genre: (prenez-le comme pseudo-code)

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));

Cela évite tous les problèmes de blocage des deux threads qui s’attendent.

MODIFIER (Merci à Serg pour le conseil): Notez que la transmission de la chaîne de la manière décrite n'est pas sûre car la chaîne peut être détruite avant que le thread VCL ne l'utilise. Comme je l'ai mentionné - c'était uniquement destiné à être un pseudocode.


7
2018-03-25 12:01



Ajouter un objet mutex au thread principal. Obtenez mutex lorsque vous essayez de fermer la forme. Dans un autre thread, vérifiez le mutex avant la synchronisation dans la séquence de traitement.


2
2018-03-25 11:47



C'est simple:

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
2018-03-26 22:07



L'objet TThread de Delphi (et les classes héritières) appelle déjà WaitFor lors de la destruction, mais cela dépend si vous avez créé le thread avec CreateSuspended ou non. Si vous utilisez CreateSuspended = true pour effectuer une initialisation supplémentaire avant d'appeler le premier CV, vous devez envisager de créer votre propre constructeur (appel inherited Create(false);) qui effectue l'initialisation supplémentaire.


0
2018-03-27 10:41