5 votes

Fil de discussion empêchant la collecte des déchets du propriétaire

Dans une bibliothèque que j'ai créée, j'ai une classe, DataPort, qui met en œuvre une fonctionnalité similaire à la classe SerialPort de .NET. Elle communique avec un certain matériel et déclenche un événement chaque fois que des données arrivent par ce matériel. Pour mettre en œuvre ce comportement, DataPort fait tourner un thread qui doit avoir la même durée de vie que l'objet DataPort. Le problème est que lorsque le DataPort sort du champ d'application, il ne sont jamais collectés

Maintenant, comme DataPort communique avec le matériel (en utilisant pInvoke) et possède certaines ressources non gérées, il implémente IDisposable. Lorsque vous appelez Dispose sur l'objet, tout se passe correctement. Le DataPort se débarrasse de toutes ses ressources non gérées, tue le worker thread et s'en va. Cependant, si vous laissez le DataPort sortir du champ d'application, le ramasseur d'ordures n'appellera jamais le finaliseur et le DataPort restera en mémoire pour toujours. Je sais que cela se produit pour deux raisons :

  1. Un point d'arrêt dans le finalisateur n'est jamais atteint.
  2. SOS.dll me dit que le DataPort est toujours en vie.

Sidebar : Avant d'aller plus loin, je dirai que oui, je sais que la réponse est "Appelez Dispose() Dummy !" mais je pense que même si vous laissez toutes les références sortir de la portée, la bonne chose devrait se produire éventuellement et le collecteur d'ordures devrait se débarrasser du DataPort

Retour à la question : En utilisant SOS.dll, je peux voir que la raison pour laquelle mon DataPort n'est pas collecté est que le thread qu'il a lancé a toujours une référence à l'objet DataPort - à travers le paramètre implicite "this" de la méthode d'instance que le thread exécute. Le fil de travail en cours d'exécution ne seront pas collectés par les ordures ménagères Par conséquent, toutes les références qui se trouvent dans le champ d'application du thread de travail en cours d'exécution ne sont pas non plus admissibles à la collecte des déchets.

Le thread lui-même exécute essentiellement le code suivant :

public void WorkerThreadMethod(object unused)
{
  ManualResetEvent dataReady = pInvoke_SubcribeToEvent(this.nativeHardwareHandle);
  for(;;)
  {
    //Wait here until we have data, or we got a signal to terminate the thread because we're being disposed
    int signalIndex = WaitHandle.WaitAny(new WaitHandle[] {this.dataReady, this.closeSignal});
    if(signalIndex == 1) //closeSignal is at index 1
    {
      //We got the close signal.  We're being disposed!
      return; //This will stop the thread
    }
    else
    {
      //Must've been the dataReady signal from the hardware and not the close signal.
      this.ProcessDataFromHardware();
      dataReady.Reset()
    }
  }
}

La méthode Dispose contient le code suivant (pertinent) :

public void Dispose()
{
  closeSignal.Set();
  workerThread.Join();
}

Parce que le thread est un Root gc et qu'il détient une référence au DataPort, le DataPort n'est jamais éligible à la collecte des déchets. Parce que le finaliseur n'est jamais appelé, nous n'envoyons jamais le signal de fermeture au thread de travail. Comme ce dernier ne reçoit jamais le signal de fermeture, il continue à maintenir la référence à l'infini. ACK !

La seule solution à laquelle je pense pour résoudre ce problème est de supprimer le paramètre "this" de la méthode WorkerThread (voir les réponses ci-dessous). Quelqu'un d'autre peut-il penser à une autre solution ? Il doit y avoir un meilleur moyen de créer un objet avec un thread qui a la même durée de vie que l'objet ! Ou bien, est-ce que cela peut être fait sans un thread séparé ? J'ai choisi cette conception particulière pour les raisons suivantes ce poste sur les forums msdn qui décrivent certains des détails de l'implémentation interne de la classe de port série .NET ordinaire

Mise à jour un peu d'informations supplémentaires dans les commentaires :

  • Dans le fil de discussion en question, IsBackground est réglé sur true.
  • Les ressources non gérées mentionnées ci-dessus n'affectent pas le problème. Même si tout dans l'exemple utilisait des ressources gérées, je verrais toujours le même problème.

4voto

Pete Baughman Points 1679

Pour se débarrasser du paramètre implicite "This", j'ai modifié quelque peu la méthode du fil de travail et j'ai fait passer la référence "this" comme paramètre :

public static void WorkerThreadMethod(object thisParameter)
{
  //Extract the things we need from the parameter passed in (the DataPort)
  //dataReady used to be 'this.dataReady' and closeSignal used to be
  //'this.closeSignal'
  ManualResetEvent dataReady = ((DataPort)thisParameter).dataReady;
  WaitHandle closeSignal = ((DataPort)thisParameter).closeSignal;

  thisParameter = null; //Forget the reference to the DataPort

  for(;;)
  {
    //Same as before, but without "this" . . .
  }
}

C'est choquant, cela n'a pas résolu le problème !

En retournant dans SOS.dll, j'ai vu qu'il y avait toujours une référence à mon DataPort détenue par un objet ThreadHelper. Apparemment, lorsque vous lancez un thread de travail en faisant Thread.Start(this); il crée un objet privé ThreadHelper avec la même durée de vie que le thread qui conserve la référence que vous avez passée à la méthode Start (j'en déduis). Cela nous laisse avec le même problème. Quelque chose retient une référence à DataPort. Essayons encore une fois :

//Code that starts the thread:
  Thread.Start(new WeakReference(this))
//. . .
public static void WorkerThreadMethod(object weakThisReference)
{
  DataPort strongThisReference= (DataPort)((WeakReference)weakThisReference).Target;

  //Extract the things we need from the parameter passed in (the DataPort)
  ManualResetEvent dataReady = strongThisReferencedataReady;
  WaitHandle closeSignal = strongThisReference.closeSignal;

  strongThisReference= null; //Forget the reference to the DataPort.

  for(;;)
  {
    //Same as before, but without "this" . . .
  }
}

Maintenant, tout va bien. Le ThreadHelper qui est créé conserve une WeakReference, ce qui n'affecte pas la collecte des déchets. Nous n'extrayons du DataPort que les données dont nous avons besoin au début du fil de travail, puis nous perdons intentionnellement toutes les références au DataPort. Cela ne pose pas de problème dans cette application car les parties que nous saisissons ne changent pas au cours de la durée de vie du DataPort. Maintenant, lorsque l'application de niveau supérieur perd toutes les références au DataPort, elle est éligible à la collecte des déchets. Le GC exécutera le finaliseur qui appellera la méthode Dispose, ce qui tuera le worker thread. Tout va bien.

Cependant, c'est un véritable casse-tête à réaliser (ou du moins à réussir) ! Existe-t-il un meilleur moyen de créer un objet qui possède un thread avec la même durée de vie que cet objet ? Sinon, existe-t-il un moyen de faire cela sans le thread ?

Epilogue : Ce serait génial si au lieu d'avoir un thread qui passe la plupart de son temps à faire WaitHandle.WaitAny(), vous pouviez avoir une sorte de handle d'attente qui n'a pas besoin de son propre thread, mais qui déclenche une continuation sur un thread Threadpool une fois qu'il est déclenché. Par exemple, si la DLL du matériel pouvait simplement appeler un délégué à chaque fois qu'il y a de nouvelles données au lieu de signaler un événement, mais je ne contrôle pas cette dll.

0voto

Ulrich Eckhardt Points 5381

Je pense que le problème ne se situe pas dans le code que vous avez montré mais dans le code utilisant cette classe wrapper de port série. Si vous n'avez pas d'instruction "using" à cet endroit, voir http://msdn.microsoft.com/en-us/library/yh598w02.aspx vous n'avez pas un comportement déterministe pour le nettoyage. Au lieu de cela, vous vous en remettez au ramasseur d'ordures, mais celui-ci ne récoltera jamais un objet qui est toujours référencé, et toutes les variables de pile d'un thread (que ce soit en tant que paramètre normal ou ce-pointeur) comptent comme des références.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X