web-dev-qa-db-fra.com

Pourquoi est-ce que lock (this) {...} est mauvais?

La documentation MSDN dit que

public class SomeObject
{
  public void SomeOperation()
  {
    lock(this)
    {
      //Access instance variables
    }
  }
}

est "un problème si l’instance peut être consultée publiquement". Je me demande pourquoi Est-ce parce que le verrou sera tenu plus longtemps que nécessaire? Ou y a-t-il une raison plus insidieuse?

439
Anton

Il est mauvais d'utiliser this dans les instructions de verrouillage, car il est généralement hors de votre contrôle de savoir qui d'autre pourrait verrouiller cet objet.

Afin de planifier correctement les opérations parallèles, il convient de prendre un soin particulier pour envisager les éventuelles impasses, et le fait qu’un nombre inconnu de points d’entrée de verrouillage l’empêche Par exemple, n'importe qui avec une référence à l'objet peut se verrouiller sans que le concepteur/créateur d'objet le sache. Cela augmente la complexité des solutions multithreads et peut affecter leur exactitude.

Un champ privé est généralement une meilleure option car le compilateur appliquera des restrictions d'accès à celui-ci et encapsulera le mécanisme de verrouillage. L'utilisation de this enfreint l'encapsulation en exposant une partie de votre implémentation de verrouillage au public. Il n’est pas clair non plus que vous obtiendrez un verrou sur this à moins que cela n’ait été documenté. Même dans ce cas, s’appuyer sur la documentation pour éviter un problème reste sous-optimal.

Enfin, il existe une idée fausse commune selon laquelle lock(this) modifie réellement l'objet transmis en tant que paramètre et le rend en quelque sorte en lecture seule ou inaccessible. C'est faux. L'objet passé en tant que paramètre à lock sert simplement de clé. Si un verrou est déjà maintenu sur cette clé, il ne peut pas être verrouillé; sinon, le verrouillage est autorisé.

C'est pourquoi il est mauvais d'utiliser des chaînes en tant que clés dans les instructions lock, car elles sont immuables et sont partagées/accessibles à travers des parties de l'application. Vous devez utiliser une variable privée à la place, une instance Object fera l'affaire.

Exécutez le code C # suivant à titre d'exemple.

public class Person
{
    public int Age { get; set;  }
    public string Name { get; set; }

    public void LockThis()
    {
        lock (this)
        {
            System.Threading.Thread.Sleep(10000);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var nancy = new Person {Name = "Nancy Drew", Age = 15};
        var a = new Thread(nancy.LockThis);
        a.Start();
        var b = new Thread(Timewarp);
        b.Start(nancy);
        Thread.Sleep(10);
        var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 };
        var c = new Thread(NameChange);
        c.Start(anotherNancy);
        a.Join();
        Console.ReadLine();
    }

    static void Timewarp(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // A lock does not make the object read-only.
        lock (person.Name)
        {
            while (person.Age <= 23)
            {
                // There will be a lock on 'person' due to the LockThis method running in another thread
                if (Monitor.TryEnter(person, 10) == false)
                {
                    Console.WriteLine("'this' person is locked!");
                }
                else Monitor.Exit(person);
                person.Age++;
                if(person.Age == 18)
                {
                    // Changing the 'person.Name' value doesn't change the lock...
                    person.Name = "Nancy Smith";
                }
                Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
            }
        }
    }

    static void NameChange(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // You should avoid locking on strings, since they are immutable.
        if (Monitor.TryEnter(person.Name, 30) == false)
        {
            Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\".");
        }
        else Monitor.Exit(person.Name);

        if (Monitor.TryEnter("Nancy Drew", 30) == false)
        {
            Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
        }
        else Monitor.Exit("Nancy Drew");
        if (Monitor.TryEnter(person.Name, 10000))
        {
            string oldName = person.Name;
            person.Name = "Nancy Callahan";
            Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
        }
        else Monitor.Exit(person.Name);
    }
}

Sortie de la console

'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.
473
Esteban Brenes

Parce que si les gens peuvent accéder à votre pointeur d'instance d'objet (c'est-à-dire votre this), ils peuvent également essayer de verrouiller ce même objet. Maintenant, ils ne savent peut-être pas que vous verrouillez this en interne, donc cela peut causer des problèmes (peut-être une impasse)

En plus de cela, c'est aussi une mauvaise pratique, car cela verrouille "trop"

Par exemple, vous pouvez avoir une variable membre de List<int> et la seule chose que vous devez réellement verrouiller est cette variable membre. Si vous verrouillez l'intégralité de l'objet dans vos fonctions, les autres appels qui appellent ces fonctions seront bloqués dans l'attente du verrouillage. Si ces fonctions n'ont pas besoin d'accéder à la liste des membres, un autre code risque d'attendre et de ralentir votre application sans aucune raison.

60
Orion Edwards

Consultez le sujet Synchronisation des threads MSDN (Guide de programmation C #)

En règle générale, il est préférable d'éviter le verrouillage sur un type public, ou sur un objet instances indépendantes de la volonté de votre application. Par exemple, verrouillez (this) peut être problématique si l'instance peut être accessible publiquement, car code au-delà de votre contrôle peut verrouiller le objet aussi bien. Cela pourrait créer situations de blocage où deux ou plus Les threads attendent la sortie du fichier même objet. Verrouillage sur un public type de données, par opposition à un objet, peut causer des problèmes pour le même raison. Verrouiller des chaînes littérales est particulièrement risqué parce que littéral les chaînes sont internées par le commun Langage d'exécution (CLR). Ça signifie qu'il n'y a qu'une seule instance de donné chaîne littérale pour l'ensemble programme, exactement le même objet représente le littéral dans tout en cours d'exécution domaines d'application, sur tous les threads . En conséquence, un verrou est placé sur une chaîne avec le même contenu n'importe où dans le le processus de demande verrouille tout instances de cette chaîne dans le application. En conséquence, il vaut mieux verrouiller un membre privé ou protégé ce n'est pas interné. Quelques classes fournir des membres spécifiquement pour verrouillage. Le type Array, par exemple, fournit SyncRoot. Beaucoup de collection les types fournissent un membre SyncRoot en tant que bien.

42
crashmstr

Je sais que c’est un vieux fil conducteur, mais comme les gens peuvent toujours le rechercher et s’y fier, il semble important de souligner que lock(typeof(SomeObject)) est nettement pire que lock(this). Ayant dit cela; Félicitations sincères à Alan pour avoir souligné que lock(typeof(SomeObject)) est une mauvaise pratique.

Une instance de System.Type est l’un des objets les plus génériques et à grains grossiers qui soient. À tout le moins, une instance de System.Type est globale pour un AppDomain et .NET peut exécuter plusieurs programmes dans un AppDomain. Cela signifie que deux programmes entièrement différents peuvent potentiellement causer des interférences, voire créer un blocage, s'ils essayaient tous les deux d'obtenir un verrou de synchronisation sur la même instance de type.

Donc, lock(this) n’est pas une forme particulièrement robuste, peut causer des problèmes et devrait toujours lever les sourcils pour toutes les raisons citées. Pourtant, il existe un code largement utilisé, relativement bien respecté et apparemment stable, comme log4net, qui utilise le modèle de verrouillage (ce) de manière intensive, bien que je préfère personnellement voir ce modèle changer.

Mais lock(typeof(SomeObject)) ouvre une boîte de Pandore complètement nouvelle et améliorée.

Pour ce que ça vaut.

32
Craig

... et les mêmes arguments s'appliquent également à cette construction:

lock(typeof(SomeObject))
25
Alan

Imaginez que votre bureau compte une secrétaire compétente qui constitue une ressource partagée au sein du ministère. De temps en temps, vous vous précipitez vers eux parce que vous avez une tâche, seulement pour espérer qu'un autre de vos collègues ne les a pas déjà réclamés. Habituellement, vous ne devez attendre que quelques instants.

Parce que partager signifie partager le même intérêt, votre responsable décide que les clients peuvent également utiliser directement la secrétaire. Mais cela a un effet secondaire: un client peut même les réclamer pendant que vous travaillez pour ce client et vous avez également besoin d'eux pour exécuter une partie des tâches. Un blocage se produit, car réclamer n'est plus une hiérarchie. Cela aurait pu être évité tous ensemble en ne permettant pas aux clients de les réclamer en premier lieu.

lock(this) est mauvais comme nous l'avons vu. Un objet extérieur peut se verrouiller sur l'objet et puisque vous ne contrôlez pas qui utilise la classe, n'importe qui peut verrouiller dessus. Quel est exactement l'exemple décrit ci-dessus. Là encore, la solution consiste à limiter l'exposition de l'objet. Cependant, si vous avez une classe private, protected ou internal, vous pouvez déjà contrôler qui verrouille votre objet, car vous êtes sûr que vous ' J'ai écrit votre code vous-même. Le message est donc le suivant: ne l'exposez pas sous la forme public. En outre, le fait d’utiliser un verrou dans des scénarios similaires évite les blocages.

L'opposé complet consiste à verrouiller les ressources partagées dans le domaine d'application - le pire des cas. C'est comme mettre votre secrétaire à l'extérieur et permettre à tout le monde de les réclamer. Le résultat est un chaos total - ou en termes de code source: c'était une mauvaise idée; jetez-le et recommencez. Alors, comment faisons-nous cela?

Les types sont partagés dans le domaine d'application, comme le soulignent la plupart des gens. Mais il y a encore mieux à utiliser: les chaînes. La raison en est que les chaînes sont mises en commun. En d'autres termes: si vous avez deux chaînes ayant le même contenu dans un domaine d'application, il est possible qu'elles aient exactement le même pointeur. Puisque le pointeur est utilisé comme clé de verrouillage, vous obtenez un synonyme de "préparez-vous pour un comportement indéfini".

De même, vous ne devriez pas verrouiller les objets WCF, HttpContext.Current, Thread.Current, Singletons (en général), etc. Le moyen le plus simple d'éviter tout cela? private [static] object myLock = new object();

7
atlaste

Le verrouillage sur le pointeur this peut être bad si vous verrouillez une ressource shared. Une ressource partagée peut être une variable statique ou un fichier sur votre ordinateur - c’est-à-dire quelque chose qui est partagé entre tous les utilisateurs de la classe. La raison en est que le pointeur this contiendra une référence différente à un emplacement en mémoire chaque fois que votre classe sera instanciée. Ainsi, verrouiller this dans une instance unique d'une classe est différent du verrouillage sur this dans une autre instance d'une classe.

Consultez ce code pour voir ce que je veux dire. Ajoutez le code suivant à votre programme principal dans une application console:

    static void Main(string[] args)
    {
         TestThreading();
         Console.ReadLine();
    }

    public static void TestThreading()
    {
        Random Rand = new Random();
        Thread[] threads = new Thread[10];
        TestLock.balance = 100000;
        for (int i = 0; i < 10; i++)
        {
            TestLock tl = new TestLock();
            Thread t = new Thread(new ThreadStart(tl.WithdrawAmount));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
        Console.Read();
    }

Créez une nouvelle classe comme ci-dessous.

 class TestLock
{
    public static int balance { get; set; }
    public static readonly Object myLock = new Object();

    public void Withdraw(int amount)
    {
      // Try both locks to see what I mean
      //             lock (this)
       lock (myLock)
        {
            Random Rand = new Random();
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
            }
            else
            {
                Console.WriteLine("Can't process your transaction, current balance is :  " + balance + " and you tried to withdraw " + amount);
            }
        }

    }
    public void WithdrawAmount()
    {
        Random Rand = new Random();
        Withdraw(Rand.Next(1, 100) * 100);
    }
}

Voici une exécution du programme bloquant sur this.

   Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  94400
    Balance before Withdrawal :  100000
    Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  88800
    Withdraw        : -5600
    Balance after Withdrawal  :  83200
    Balance before Withdrawal :  83200
    Withdraw        : -9100
    Balance after Withdrawal  :  74100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance after Withdrawal  :  55900
    Balance after Withdrawal  :  65000
    Balance before Withdrawal :  55900
    Withdraw        : -9100
    Balance after Withdrawal  :  46800
    Balance before Withdrawal :  46800
    Withdraw        : -2800
    Balance after Withdrawal  :  44000
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  41200
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  38400

Voici une exécution du programme bloquant sur myLock.

Balance before Withdrawal :  100000
Withdraw        : -6600
Balance after Withdrawal  :  93400
Balance before Withdrawal :  93400
Withdraw        : -6600
Balance after Withdrawal  :  86800
Balance before Withdrawal :  86800
Withdraw        : -200
Balance after Withdrawal  :  86600
Balance before Withdrawal :  86600
Withdraw        : -8500
Balance after Withdrawal  :  78100
Balance before Withdrawal :  78100
Withdraw        : -8500
Balance after Withdrawal  :  69600
Balance before Withdrawal :  69600
Withdraw        : -8500
Balance after Withdrawal  :  61100
Balance before Withdrawal :  61100
Withdraw        : -2200
Balance after Withdrawal  :  58900
Balance before Withdrawal :  58900
Withdraw        : -2200
Balance after Withdrawal  :  56700
Balance before Withdrawal :  56700
Withdraw        : -2200
Balance after Withdrawal  :  54500
Balance before Withdrawal :  54500
Withdraw        : -500
Balance after Withdrawal  :  54000
4
ItsAllABadJoke

Il existe un très bon article à ce sujet http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects de Rico Mariani, architecte de la performance du moteur d'exécution Microsoft® .NET

Extrait:

Le problème fondamental ici est que vous ne possédez pas l’objet type, et vous Je ne sais pas qui d'autre pourrait y accéder. En général, c'est une très mauvaise idée compter sur le verrouillage d'un objet que vous n'avez pas créé et ne savez pas qui d'autre peut être accéder. Cela invite à une impasse. Le moyen le plus sûr est de verrouiller uniquement les objets privés.

3
Vikrant

Il y a aussi une bonne discussion à ce sujet ici: Est-ce l'utilisation appropriée d'un mutex?

2
Bob Nadler

Voici un exemple de code plus simple à suivre (IMO): (Travaillera dans LinqPad , référence les espaces de noms suivants: System.Net et System.Threading.Tasks)

void Main()
{
    ClassTest test = new ClassTest();
    lock(test)
    {
        Parallel.Invoke (
            () => test.DoWorkUsingThisLock(1),
            () => test.DoWorkUsingThisLock(2)
        );
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine("Before ClassTest.DoWorkUsingThisLock " + i);
        lock(this)
        {
            Console.WriteLine("ClassTest.DoWorkUsingThisLock " + i);
            Thread.Sleep(1000);
        }
        Console.WriteLine("ClassTest.DoWorkUsingThisLock Done " + i);
    }
}
1
Raj Rao

Parce que tout morceau de code pouvant voir l'instance de votre classe peut également se verrouiller sur cette référence. Vous souhaitez masquer (encapsuler) votre objet de verrouillage afin que seul le code devant le référencer puisse le référencer. Le mot-clé this fait référence à l'instance de classe actuelle. Ainsi, vous pouvez y faire référence et l'utiliser pour effectuer la synchronisation des threads.

En clair, cela est mauvais, car un autre bloc de code pourrait utiliser l’instance de classe pour verrouiller et empêcher votre code d’obtenir un verrou opportun ou pourrait créer d’autres problèmes de synchronisation de threads. Meilleur cas: rien d'autre n'utilise une référence à votre classe à verrouiller. Cas moyen: quelque chose utilise une référence à votre classe pour faire des verrous et cela pose des problèmes de performances. Le pire des cas: quelque chose utilise une référence de votre classe pour faire des verrous et cela cause des problèmes vraiment graves, très subtils, vraiment difficiles à déboguer.

1
Jason Jackson

Voici une illustration beaucoup plus simple (tirée de Question 34 ici ) Pourquoi verrouiller (ceci) est mauvais et peut entraîner des blocages lorsque le consommateur de votre classe tente également de verrouiller l'objet .. le fil peut continuer, les deux autres sont dans l’impasse.

class SomeClass
{
    public void SomeMethod(int id)
    {
        **lock(this)**
        {
            while(true)
            {
                Console.WriteLine("SomeClass.SomeMethod #" + id);
            }
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        SomeClass o = new SomeClass();

        lock(o)
        {
            for (int threadId = 0; threadId < 3; threadId++)
            {
                Thread t = new Thread(() => {
                    o.SomeMethod(threadId);
                        });
                t.Start();
            }

            Console.WriteLine();
        }

Pour contourner le problème, ce gars-là a utilisé Thread.TryMonitor (avec expiration du délai) au lieu de lock:

            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }

https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks

1
user3761555

Veuillez vous référer au lien suivant qui explique pourquoi verrouiller (ceci) n'est pas une bonne idée.

http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx

La solution consiste donc à ajouter un objet privé, par exemple lockObject, à la classe et à placer la région de code dans l'instruction lock, comme indiqué ci-dessous:

lock (lockObject)
{
...
}
1
Dhruv Rangunwala

Désolé les gars, mais je ne peux pas être d'accord avec l'argument voulant que verrouiller ceci puisse provoquer une impasse. Vous confondez deux choses: l'impasse et la faim.

  • Vous ne pouvez pas annuler une impasse sans interrompre l’un des threads afin qu’une fois l’impasse perdue, vous ne puissiez plus sortir.
  • Starving se terminera automatiquement lorsque l'un des threads aura terminé son travail

Ici est une image qui illustre la différence.

Conclusion
Vous pouvez toujours utiliser lock(this) en toute sécurité si la privation de threads ne vous pose pas problème. Vous devez toujours garder à l'esprit que lorsque le fil qui est en train de mourir de faim en utilisant lock(this) se termine par un verrou ayant l'objet verrouillé, il finira par se terminer par une famine éternelle;)

0
SOReader

Vous pouvez établir une règle qui dit qu'une classe peut avoir un code qui verrouille 'ceci' ou tout objet que le code de la classe instancie. Donc, ce n'est un problème que si le modèle n'est pas suivi.

Si vous souhaitez vous protéger du code qui ne suivra pas ce modèle, la réponse acceptée est correcte. Mais si le modèle est suivi, ce n'est pas un problème.

L'avantage de la serrure (ceci) est l'efficacité. Que faire si vous avez un "objet de valeur" simple qui contient une seule valeur. C'est juste un emballage, et il est instancié des millions de fois. En exigeant la création d'un objet de synchronisation privé uniquement pour le verrouillage, vous avez pratiquement doublé la taille de l'objet et le nombre d'allocations. Quand la performance compte, c'est un avantage.

Lorsque vous ne vous préoccupez pas du nombre d'allocations ou de l'encombrement de la mémoire, il est préférable d'éviter le verrouillage (ceci) pour les raisons indiquées dans d'autres réponses.

0
zumalifeguard