2016-08-12 2 views
2

Je rencontre des problèmes lors de la gestion correcte de Dispose/Finalization avec un ConcurrentBag qui contient des objets non gérés. Exécuter le code ci-dessous (généralement) produit un ObjectDisposedException (Cannot access a disposed object.Object name: 'The ThreadLocal object has been disposed.'.) sur l'appel à TryTake(). Vraisemblablement, dans ce cas, le garbage collector détruit ConcurrentBag avant d'appeler le finaliseur de A. Je pensais que ce ne serait le cas que si le ConcurrentBag lui-même implémentait un finaliseur. Est-il vrai que l'on ne devrait jamais toucher les objets gérés pendant le processus de finalisation?Finalisation de ConcurrentBag contenant des objets non gérés

class A : IDisposable 
{ 
    private readonly ConcurrentBag<object> _collection = new ConcurrentBag<object>(); 

    public A(string value) 
    { 
     if (value == null) throw new ArgumentNullException(); 
    } 

    ~A() 
    { 
     Dispose(false); 
    } 

    public void Dispose() => Dispose(true); 

    private void Dispose(bool disposing) 
    { 
     if (disposing) {} 

     object value; 
     while (_collection.TryTake(out value)) 
     { 
      // Cleanup value 
     } 
    } 
} 

Trigger l'exception:

void Main() 
{ 
    var a = new A(null); 
} 

Ce qui suit semble contourner ce problème spécifique, mais je ne suis pas sûr si cela est sûr. Y a-t-il une implémentation parfaitement sûre pour ce scénario?

while (_collection.IsEmpty == false) 
{ 
    object value; 
    _collection.TryTake(out value); 
    // Cleanup value 
} 

Répondre

2

Lorsque le code est en cours d'exécution de la finaliseur (disposing est false) les seules choses que vous êtes autorisé à faire est d'utiliser des méthodes statiques sans état, les variables locales à la fonction et les champs héritant de CriticalFinalizerObject (sauf si vous êtes dans un finaliseur pour un CriticalFinalizerObject alors vous ne pouvez pas les utiliser).

Parce que ConcurrentBag n'hérite pas de CriticalFinalizerObject vous ne pouvez pas compter sur elle n'étant pas finalisé lorsque votre propre finaliseur est exécuté. Les deux this et la variable _collection.m_localsSign mentions in his answer sont toutes deux mises dans la file d'attente de finalisation en même temps lorsque this devient inaccessible. L'ordre dans lequel la file d'attente est traitée n'est pas déterministe.

Il existe un article génial "IDisposable: What Your Mother Never Told You About Resource Deallocation" qui approfondit ce qui se passe réellement lorsque quelque chose est finalisé et fournit de meilleurs modèles que le modèle private void Dispose(bool disposing) traditionnel recommandé par Microsoft.

+0

Votre explication est parfaitement logique.Selon l'article de Stephen Cleary, l'implémentation correcte serait d'envelopper les objets non gérés dans les wrappers IDisposable (ce qu'il appelle Level 0) qui gèrent le sale travail de considérer la finalisation. Cela permet au niveau supérieur, ma classe A (c'est-à-dire le niveau 1), de ne s'occuper que du chemin de Dispose et d'ignorer le chemin de finalisation. – Terrence

+0

La seule chose que vous avez incorrecte est que les objets de niveau 0 doivent dériver de 'SafeHandle' non seulement' IDisposeable', mais à part ça, oui. –

1

La trace de la pile complète de l'objet disposé exception est

at System.Threading.ThreadLocal`1.GetValueSlow() 
    at System.Threading.ThreadLocal`1.get_Value() 
    at System.Collections.Concurrent.ConcurrentBag`1.GetThreadList(Boolean forceCreate) 
    at System.Collections.Concurrent.ConcurrentBag`1.TryTakeOrPeek(T& result, Boolean take) 
    at System.Collections.Concurrent.ConcurrentBag`1.TryTake(T& result) 
    at A.Dispose(Boolean disposing) 
    at A.Finalize() 

Ce qui signifie que l'objet disposé à l'intérieur vit ConcurrentBag qui est impair depuis ConcurrentBag est pas IDisposable. Creuser à travers le source of ConcurrentBag montre qu'il a un ThreadLocal qui est IDisposable et est utilisé dans la méthode GetThreadList. Assez bizarrement si vous utilisez une vieille boucle foreach vous évitez le ThreadLocal et il semble que tout fonctionne comme vous le souhaitez. En dépit de cette fouille je n'ai pas

foreach (var value in _collection) 
{ 
    // Cleanup value 
} 

une explication de la façon dont la ThreadLocal mais j'ai disposé.

+0

Il a été résolu car '_collection' était considéré comme inaccessible lorsque' a' devenait inaccessible, ce qui a rendu le fichier '_collection.m_locals' inaccessible et a été placé dans la file d'attente du finaliseur. Le finalizer 'm_locals' a été exécuté avant que le finaliseur de' A' ait une chance de s'exécuter car 'ThreadLocal ' n'hérite pas de 'CriticalFinalizerObject'. –