2 votes

.NET GC Accès à un objet synchronisé à partir d'un finaliseur

J'ai récemment lu cet article Synchronisation sécurisée des threads car je m'intéressais à la sûreté des threads des appels effectués à partir d'un finaliseur. J'ai écrit le code suivant pour tester l'accès à une collection statique thread-safe à partir d'un finaliseur.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GCThreadTest
{
    class Program
    {
        static class FinaliserCollection
        {
            private static Queue s_ItemQueue = new Queue();
            private static System.Object s_Lock = new System.Object();

            public static void AddItem(int itemValue)
            {
                lock(s_Lock)
                {
                    s_ItemQueue.Enqueue(itemValue);
                }
            }

            public static bool TryGetItem(out int item)
            {
                lock(s_Lock)
                {
                    if (s_ItemQueue.Count <= 0)
                    {
                        item = -1;
                        return false;
                    }

                    item = s_ItemQueue.Dequeue();
                    return true;
                }
            }
        }

        class FinaliserObject
        {
            private int m_ItemValue;

            public FinaliserObject(int itemValue)
            {
                m_ItemValue = itemValue;
            }

            ~FinaliserObject()
            {
                FinaliserCollection.AddItem(m_ItemValue);
            }
        }

        static void Main(string[] args)
        {
            int itemValueIn = 0;
            int itemValueOut = 0;

            while (itemValueOut < 10000)
            {
                System.Threading.ThreadPool.QueueUserWorkItem
                    (delegate(object value)
                    {
                        new FinaliserObject((int)value);

                        System.Threading.Thread.Sleep(5);

                    }, itemValueIn);

                itemValueIn = itemValueIn + 1;

                // This seems to stop finaliser from
                // being called?
                // System.Threading.Thread.Sleep(5);

                int tempItemValueOut = -1;
                if (FinaliserCollection.TryGetItem(out tempItemValueOut))
                    itemValueOut = tempItemValueOut;
            }

            System.Console.WriteLine("Finished after {0} items created", itemValueOut);
            System.Console.ReadLine();
        }
    }
}

Sans l'appel 'Sleep' dans la boucle while, ce code semble fonctionner correctement, mais est-il vraiment à l'abri du deadlocking? Serait-il jamais possible qu'un appel à un finaliseur soit effectué pendant qu'un élément de la file d'attente du pool de threads y accède? Pourquoi ajouter 'Sleep' à la boucle while du thread principal semble arrêter l'appel de tous les finaliseurs?

3voto

James King Points 3620

Wow. What the... This is the most bizarre piece of code I've ever seen. @.@

Tout d'abord, à quel appel de finaliseur faites-vous référence? Le seul finaliseur que je vois est le finaliseur pour FinaliserObject, qui sera appelé 10 000 fois, et peut être appelé indépendamment de ce qui se passe sur la collection statique. C'est-à-dire, oui, ces objets peuvent être détruits pendant que d'autres objets sont extraits de la collection. Ce n'est pas un problème.

La collection statique en elle-même ne sera pas nettoyée tant que l'application elle-même ne se termine pas.

Gardez à l'esprit qu'il n'y a absolument aucune garantie quant à la date ou au moment où ces finaliseurs seront appelés avant que l'application elle-même ne se termine. Votre collection statique pourrait être complètement vide lorsque vous quittez l'application.

Pire encore, vous attribuez itemValueOut à la dernière valeur que vous extrayez de la file d'attente... ce qui n'est PAS le nombre d'éléments créés, comme vous l'impliquez dans votre WriteLine(). Parce que ces destructeurs sont appelés dans n'importe quel ordre possible, vous pourriez théoriquement ajouter à la file d'attente 10 000, 9 999, 9 998, ... 2, 1, dans cet ordre.

Ce qui est encore plus problématique, car vous retirez de la file d'attente 10 000 fois, mais lors de la dernière boucle, il est très possible qu'il n'y ait pas d'objet à extraire, auquel cas vous êtes assuré d'obtenir -1 pour le nombre d'éléments retournés (même si les 9 999 autres éléments ont fonctionné avec succès).

Pour répondre à votre question, ce code ne peut pas entraîner de blocage. Un blocage se produirait si AddItem() appelait TryGetItem(), mais ces verrous sont presque garantis pour s'exclure mutuellement de la collection statique lors de l'ajout ou de la suppression d'éléments.

Vous vous exposez au danger que vous pourriez quitter votre application sans que tous les FinaliserObjects se soient ajoutés à la file d'attente. Cela signifie qu'un des finaliseurs pourrait s'exécuter et essayer de s'ajouter à la FinaliserCollection, mais la FinaliserCollection a déjà été libérée. Ce que vous faites dans le finaliseur est terrible.

Mais oui, un appel de finaliseur peut se produire pendant que vous appelez FinaliserCollection.TryGetItem(). Le finaliseur se bloquera et attendra que TryGetItem() sorte du lock(), moment auquel il ajoutera un autre élément. Ce n'est pas un problème.

Quant à la commande sleep(), vous perturbez probablement le moment où la collecte des déchets est effectuée. Rappelez-vous, vos objets ne seront pas collectés/finalisés tant que le GC ne décide pas qu'il a besoin des ressources.

Désolé d'être si catégorique... Je sais que vous essayez simplement de tester un concept mais je ne comprends vraiment pas pourquoi vous voudriez faire ce que vous essayez de faire dans le finaliseur. S'il y a vraiment un objectif légitime ici, le faire dans le finaliseur n'est pas la bonne réponse.

Modifier

D'après ce que je lis et ce que Sasha dit, non vous n'aurez pas de blocage. Le thread du finaliseur peut être bloqué en attendant le verrou, mais le GC n'attendra pas le finaliseur, et libérera donc les threads, permettant ainsi la libération des verrous.

En tout cas, c'est un argument très convaincant pour expliquer pourquoi vous ne devriez pas faire des appels comme celui-ci dans un finaliseur... le finaliseur est uniquement pour libérer les ressources non gérées. Tout le reste est jouer à la roulette.

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