Warum ist lock (this) {…} schlecht?


484

Die MSDN-Dokumentation besagt dies

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

ist "ein Problem, wenn auf die Instanz öffentlich zugegriffen werden kann". Ich frage mich warum? Liegt es daran, dass das Schloss länger als nötig gehalten wird? Oder gibt es einen heimtückischeren Grund?

Antworten:


508

Die Verwendung thisin Sperranweisungen ist eine schlechte Form, da Sie im Allgemeinen nicht die Kontrolle darüber haben, wer dieses Objekt sonst möglicherweise sperrt.

Um Paralleloperationen richtig planen zu können, sollte besonders darauf geachtet werden, mögliche Deadlock-Situationen zu berücksichtigen, und eine unbekannte Anzahl von Lock-Einstiegspunkten behindert dies. Zum Beispiel kann jeder, der auf das Objekt verweist, es sperren, ohne dass der Objektdesigner / -ersteller davon erfährt. Dies erhöht die Komplexität von Multithread-Lösungen und kann deren Richtigkeit beeinträchtigen.

Ein privates Feld ist normalerweise eine bessere Option, da der Compiler Zugriffsbeschränkungen erzwingt und den Sperrmechanismus kapselt. Die Verwendung thisverletzt die Kapselung, indem ein Teil Ihrer Sperrimplementierung der Öffentlichkeit zugänglich gemacht wird. Es ist auch nicht klar, dass Sie eine Sperre erwerben, thises sei denn, dies wurde dokumentiert. Selbst dann ist es nicht optimal, sich auf die Dokumentation zu verlassen, um ein Problem zu vermeiden.

Schließlich gibt es das verbreitete Missverständnis, dass lock(this)das als Parameter übergebene Objekt tatsächlich geändert und in gewisser Weise schreibgeschützt oder unzugänglich gemacht wird. Das ist falsch . Das als Parameter übergebene Objekt lockdient lediglich als Schlüssel . Wenn dieser Schlüssel bereits gesperrt ist, kann das Schloss nicht hergestellt werden. Andernfalls ist die Sperre zulässig.

Aus diesem Grund ist es schlecht, Zeichenfolgen als Schlüssel in lockAnweisungen zu verwenden, da sie unveränderlich sind und von Teilen der Anwendung gemeinsam genutzt werden können. Sie sollten stattdessen eine private Variable verwenden, eine ObjectInstanz funktioniert gut.

Führen Sie den folgenden C # -Code als Beispiel aus.

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

Konsolenausgabe

'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'.

2
Während ich grok: (1) Nancy ist in thread1 mit lock (this). (2) GLEICH Nancy befindet sich in Thread2-Alterung, während sie noch in Thread1 gesperrt ist. Dies beweist, dass ein gesperrtes Objekt nicht schreibgeschützt ist. AUCH (2a) In Thread 2 ist dieses Nancy-Objekt auch für Name gesperrt. (3) Erstellen Sie ein VERSCHIEDENES Objekt mit demselben Namen . (4) In Thread3 übergehen und versuchen, mit Name zu sperren. (großes Ende) ABER "Strings sind unveränderlich" bedeutet, dass jedes Objekt, das auf den String "Nancy Drew" verweist, buchstäblich dieselbe String-Instanz im Speicher betrachtet. Objekt2 kann also keine Sperre für eine Zeichenfolge erhalten, wenn Objekt1 für denselben Wert gesperrt ist
radarbob

Die Verwendung einer Standardvariablen anstelle von lock(this)ist eine Standardempfehlung. Es ist wichtig zu beachten, dass dies im Allgemeinen unmöglich macht, dass externer Code die mit dem Objekt verknüpfte Sperre zwischen Methodenaufrufen aufhält. Dies kann eine gute Sache sein oder auch nicht . Es besteht eine gewisse Gefahr, dass externer Code eine Sperre für eine beliebige Dauer hält, und Klassen sollten im Allgemeinen so gestaltet sein, dass eine solche Verwendung unnötig wird, aber es gibt nicht immer praktische Alternativen. Als einfaches Beispiel, es sei denn, eine Sammlung implementiert eine ToArrayoder ToListeine eigene Methode ...
Supercat

4
(im Gegensatz zu den Erweiterungsmethoden "IEnumerable <T>") Die einzige Möglichkeit für einen Thread, der einen Snapshot der Sammlung erhalten möchte, besteht darin, ihn aufzulisten und alle Änderungen zu sperren . Dazu muss es Zugriff auf eine Sperre haben, die von jedem Code erfasst wird, der die Sammlung ändern würde. Wenn die Sperre nicht aufgehoben wird, kann es beispielsweise unmöglich sein, dass das Programm regelmäßig einen asynchronen Snapshot der Sammlung ausführt (z. B. um eine Benutzeroberfläche zum Durchsuchen von Sammlungen zu aktualisieren).
Supercat

there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false- Ich glaube, diese Gespräche beziehen sich auf das SyncBlock-Bit im CLR-Objekt, also ist dies formal richtig - sperren Sie das modifizierte Objekt selbst
sll

@Esteban, ich liebe dein Beispiel absolut, es ist großartig. Ich habe eine Frage an dich. Ihr Code der Methode NameChange (..) endet mit: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . } else Monitor.Exit (person.Name); </ code> Sollte es nicht enden mit: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . Monitor.Exit (person.Name); } </ code>
AviFarah

64

Denn wenn Personen auf Ihren Objektinstanzzeiger (dh Ihren thisZeiger) zugreifen können, können sie auch versuchen, dasselbe Objekt zu sperren. Jetzt sind sie sich möglicherweise nicht bewusst, dass Sie thisintern gesperrt sind, sodass dies zu Problemen führen kann (möglicherweise zu einem Deadlock).

Darüber hinaus ist es auch eine schlechte Praxis, weil es "zu viel" sperrt.

Beispielsweise könnten Sie eine Mitgliedsvariable von haben List<int>, und das einzige, was Sie tatsächlich sperren müssen, ist diese Mitgliedsvariable. Wenn Sie das gesamte Objekt in Ihren Funktionen sperren, werden andere Dinge, die diese Funktionen aufrufen, blockiert und warten auf die Sperre. Wenn diese Funktionen nicht auf die Mitgliederliste zugreifen müssen, wird anderer Code warten und Ihre Anwendung ohne Grund verlangsamen.


44
Der letzte Absatz dieser Antwort ist nicht korrekt. Die Sperre macht das Objekt in keiner Weise unzugänglich oder schreibgeschützt. Lock (this) verhindert nicht, dass ein anderer Thread das Objekt aufruft oder ändert, auf das hier verwiesen wird.
Esteban Brenes

3
Dies ist der Fall, wenn die anderen aufgerufenen Methoden ebenfalls eine Sperre ausführen (dies). Ich glaube, das ist der Punkt, den er gemacht hat. Beachten Sie die "Wenn Sie das gesamte Objekt in Ihren Funktionen sperren" ...
Herms

@Orion: Das ist klarer. @Herms: Ja, aber Sie müssen 'this' nicht verwenden, um diese Funktionalität zu erreichen. Die SyncRoot-Eigenschaft in Listen dient beispielsweise diesem Zweck, während klargestellt wird, dass die Synchronisierung für diesen Schlüssel erfolgen sollte.
Esteban Brenes

Betreff: "zu viel" sperren: Es ist ein feiner Balanceakt, zu entscheiden, was gesperrt werden soll. Beachten Sie, dass das Aufheben einer Sperre Cache-Flush-CPU-Vorgänge umfasst und etwas teuer ist. Mit anderen Worten: Sperren und aktualisieren Sie nicht jede einzelne Ganzzahl. :)
Zan Lynx

Der letzte Absatz macht immer noch keinen Sinn. Wenn Sie nur den Zugriff auf die Liste einschränken müssen, warum sollten die anderen Funktionen Sperren haben, wenn sie nicht auf die Liste zugreifen?
Joakim MH

44

Schauen Sie sich das MSDN Topic Thread Synchronization (C # -Programmierhandbuch) an.

Im Allgemeinen ist es am besten, das Sperren eines öffentlichen Typs oder von Objektinstanzen zu vermeiden, die außerhalb der Kontrolle Ihrer Anwendung liegen. Zum Beispiel kann lock (this) problematisch sein, wenn auf die Instanz öffentlich zugegriffen werden kann, da Code, den Sie nicht kontrollieren können, möglicherweise auch das Objekt sperrt. Dies kann zu Deadlock-Situationen führen, in denen zwei oder mehr Threads auf die Freigabe desselben Objekts warten. Das Sperren eines öffentlichen Datentyps im Gegensatz zu einem Objekt kann aus demselben Grund Probleme verursachen. Das Sperren von Literalzeichenfolgen ist besonders riskant, da Literalzeichenfolgen von der Common Language Runtime (CLR) interniert werden. Dies bedeutet, dass es für das gesamte Programm eine Instanz eines bestimmten Zeichenfolgenliteral gibt. Das exakt gleiche Objekt repräsentiert das Literal in allen ausgeführten Anwendungsdomänen in allen Threads. Infolgedessen sperrt eine Sperre für eine Zeichenfolge mit demselben Inhalt an einer beliebigen Stelle im Anwendungsprozess alle Instanzen dieser Zeichenfolge in der Anwendung. Daher ist es am besten, ein privates oder geschütztes Mitglied zu sperren, das nicht interniert ist. Einige Klassen bieten Mitglieder speziell zum Sperren. Der Array-Typ bietet beispielsweise SyncRoot. Viele Sammlungstypen bieten auch ein SyncRoot-Mitglied.


34

Ich weiß, dass dies ein alter Thread ist, aber da die Leute immer noch nachschlagen und sich darauf verlassen können, scheint es wichtig, darauf hinzuweisen, dass dies lock(typeof(SomeObject))erheblich schlimmer ist als lock(this). Nachdem ich das gesagt habe; Ein aufrichtiges Lob an Alan für den Hinweis, dass dies lock(typeof(SomeObject))eine schlechte Praxis ist.

Eine Instanz von System.Typeist eines der allgemeinsten, grobkörnigsten Objekte, die es gibt. Zumindest ist eine Instanz von System.Type für eine AppDomain global, und .NET kann mehrere Programme in einer AppDomain ausführen. Dies bedeutet, dass zwei völlig unterschiedliche Programme möglicherweise gegenseitig Interferenzen verursachen können, selbst wenn sie einen Deadlock verursachen, wenn beide versuchen, eine Synchronisationssperre für dieselbe Instanz zu erhalten.

Ist lock(this)also keine besonders robuste Form, kann Probleme verursachen und sollte aus allen genannten Gründen immer die Augenbrauen hochziehen. Es gibt jedoch weit verbreiteten, relativ angesehenen und anscheinend stabilen Code wie log4net, der das Sperrmuster (dieses Muster) ausgiebig verwendet, obwohl ich es persönlich vorziehen würde, wenn sich dieses Muster ändert.

Aber lock(typeof(SomeObject))eröffnet eine ganz neue und verbesserte Dose Würmer.

Für was es wert ist.


26

... und genau die gleichen Argumente gelten auch für dieses Konstrukt:

lock(typeof(SomeObject))

17
lock (typeof (SomeObject)) ist tatsächlich viel schlechter als lock (this) ( stackoverflow.com/a/10510647/618649 ).
Craig

1
Nun, lock (Application.Current) ist dann noch schlimmer. Aber wer würde sowieso eines dieser dummen Dinge ausprobieren? lock (this) scheint logisch und prägnant zu sein, diese anderen Beispiele jedoch nicht.
Zar Shardan

Ich stimme nicht zu, dass dies lock(this)besonders logisch und prägnant erscheint. Es ist eine schrecklich grobe Sperre, und jeder andere Code kann Ihr Objekt sperren und möglicherweise Störungen in Ihrem internen Code verursachen. Nehmen Sie detailliertere Schlösser und übernehmen Sie eine strengere Kontrolle. Was lock(this)es zu bieten hat, ist, dass es viel besser ist als lock(typeof(SomeObject)).
Craig

8

Stellen Sie sich vor, Sie haben eine qualifizierte Sekretärin in Ihrem Büro, die eine gemeinsame Ressource in der Abteilung ist. Hin und wieder eilen Sie auf sie zu, weil Sie eine Aufgabe haben, nur um zu hoffen, dass ein anderer Ihrer Mitarbeiter sie noch nicht beansprucht hat. Normalerweise müssen Sie nur eine kurze Zeit warten.

Da Pflege wichtig ist, entscheidet Ihr Manager, dass Kunden die Sekretärin auch direkt nutzen können. Dies hat jedoch einen Nebeneffekt: Ein Kunde kann sie sogar beanspruchen, während Sie für diesen Kunden arbeiten, und Sie benötigen sie auch, um einen Teil der Aufgaben auszuführen. Ein Deadlock tritt auf, weil das Beanspruchen keine Hierarchie mehr ist. Dies hätte insgesamt vermieden werden können, indem Kunden überhaupt nicht in Anspruch genommen werden konnten.

lock(this)ist schlecht wie wir gesehen haben. Ein externes Objekt kann das Objekt sperren, und da Sie nicht steuern, wer die Klasse verwendet, kann jeder es sperren ... Dies ist das genaue Beispiel wie oben beschrieben. Wieder besteht die Lösung darin, die Belichtung des Objekts zu begrenzen. Wenn Sie jedoch ein private, protectedoder internalKlasse , die Sie konnten bereits kontrollieren , wer auf dem Objekt blockiert , weil Sie sicher sind , dass Sie den Code selbst geschrieben haben. Die Botschaft hier lautet also: Setzen Sie es nicht als aus public. Wenn Sie sicherstellen, dass in ähnlichen Szenarien eine Sperre verwendet wird, werden Deadlocks vermieden.

Das genaue Gegenteil davon ist das Sperren von Ressourcen, die in der gesamten App-Domäne gemeinsam genutzt werden - im schlimmsten Fall. Es ist, als würde man seine Sekretärin nach draußen bringen und allen da draußen erlauben, sie zu beanspruchen. Das Ergebnis ist völliges Chaos - oder in Bezug auf den Quellcode: Es war eine schlechte Idee; wirf es weg und fang von vorne an. Wie machen wir das?

Typen werden in der App-Domäne geteilt, wie die meisten Leute hier betonen. Aber es gibt noch bessere Dinge, die wir verwenden können: Strings. Der Grund ist, dass Zeichenfolgen zusammengefasst werden . Mit anderen Worten: Wenn Sie zwei Zeichenfolgen mit demselben Inhalt in einer App-Domäne haben, besteht die Möglichkeit, dass sie genau denselben Zeiger haben. Da der Zeiger als Sperrschlüssel verwendet wird, erhalten Sie im Grunde ein Synonym für "Vorbereitung auf undefiniertes Verhalten".

Ebenso sollten Sie WCF-Objekte, HttpContext.Current, Thread.Current, Singletons (im Allgemeinen) usw. nicht sperren. Der einfachste Weg, dies alles zu vermeiden? private [static] object myLock = new object();


3
Tatsächlich verhindert eine private Klasse das Problem nicht. Externer Code kann einen Verweis auf eine Instanz einer privaten Klasse erhalten ...
Rashack

1
@Rashack Während Sie technisch korrekt sind (+1, um darauf hinzuweisen), war mein Punkt, dass Sie die Kontrolle darüber haben sollten, wer die Instanz sperrt. Das Zurückgeben solcher Instanzen bricht das.
Atlaste

4

Das Sperren dieses Zeigers kann schlecht sein, wenn Sie eine gemeinsam genutzte Ressource sperren . Eine gemeinsam genutzte Ressource kann eine statische Variable oder eine Datei auf Ihrem Computer sein, dh etwas, das von allen Benutzern der Klasse gemeinsam genutzt wird. Der Grund dafür ist, dass dieser Zeiger bei jeder Instanziierung Ihrer Klasse einen anderen Verweis auf einen Speicherort enthält. Also, über Sperren dies in einmal Instanz einer Klasse ist anders als über Sperren dies in einer anderen Instanz einer Klasse.

Schauen Sie sich diesen Code an, um zu sehen, was ich meine. Fügen Sie Ihrem Hauptprogramm in einer Konsolenanwendung den folgenden Code hinzu:

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

Erstellen Sie eine neue Klasse wie die folgende.

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

Hier ist ein Durchlauf des Programms Verriegelungs auf diesem .

   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

Hier ist eine Ausführung des Programms, das myLock sperrt .

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

1
Was ist in Ihrem Beispiel zu beachten, wie was zeigen Sie, was falsch ist? Es ist schwer zu erkennen, was falsch ist, wenn Sie Random rand = new Random();NVM verwenden. Ich glaube, ich sehe das wiederholte Gleichgewicht
Seabizkit

3

Es gibt einen sehr guten Artikel darüber http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects von Rico Mariani, Leistungsarchitekt für die Microsoft® .NET-Laufzeit

Auszug:

Das Grundproblem hierbei ist, dass Sie das Typobjekt nicht besitzen und nicht wissen, wer sonst darauf zugreifen kann. Im Allgemeinen ist es eine sehr schlechte Idee, sich darauf zu verlassen, ein Objekt zu sperren, das Sie nicht erstellt haben, und nicht zu wissen, auf wen sonst möglicherweise zugegriffen wird. Dies führt zu einem Deadlock. Am sichersten ist es, nur private Objekte zu sperren.



2

Hier ist eine viel einfachere Darstellung (aus Frage 34 hier ), warum Sperren (dies) schlecht sind und zu Deadlocks führen können, wenn Verbraucher Ihrer Klasse auch versuchen, das Objekt zu sperren. Unten kann nur einer von drei Threads fortgesetzt werden, die anderen beiden sind blockiert.

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

Um dies zu umgehen, verwendete dieser Typ Thread.TryMonitor (mit Timeout) anstelle von 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


Soweit ich sehe, SomeClasserhalte ich immer noch den gleichen Deadlock , wenn ich die Sperre (diese) durch eine Sperre für ein privates Instanzmitglied von ersetze . Wenn die Sperre in der Hauptklasse für ein anderes privates Instanzmitglied von Program durchgeführt wird, tritt dieselbe Sperre auf. Ich bin mir also nicht sicher, ob diese Antwort nicht irreführend und falsch ist. Sehen Sie dieses Verhalten hier: dotnetfiddle.net/DMrU5h
Bartosz

1

Denn jeder Codeabschnitt, der die Instanz Ihrer Klasse sehen kann, kann diese Referenz auch sperren. Sie möchten Ihr Sperrobjekt ausblenden (kapseln), damit nur Code, der darauf verweisen muss, darauf verweisen kann. Das Schlüsselwort, auf das sich dies bezieht, bezieht sich auf die aktuelle Klasseninstanz, sodass eine beliebige Anzahl von Dingen darauf verweisen und es für die Thread-Synchronisierung verwenden kann.

Dies ist schlecht, da ein anderer Codeabschnitt die Klasseninstanz zum Sperren verwenden und möglicherweise verhindern kann, dass Ihr Code rechtzeitig gesperrt wird, oder andere Probleme bei der Thread-Synchronisierung verursachen kann. Bester Fall: Nichts anderes verwendet einen Verweis auf Ihre Klasse zum Sperren. Mittlerer Fall: Etwas verwendet einen Verweis auf Ihre Klasse, um Sperren durchzuführen, und es verursacht Leistungsprobleme. Der schlimmste Fall: Etwas verwendet eine Referenz Ihrer Klasse, um Sperren durchzuführen, und es verursacht wirklich schlimme, wirklich subtile, wirklich schwer zu debuggende Probleme.


1

Tut mir leid, aber ich kann dem Argument nicht zustimmen, dass das Sperren zu einem Deadlock führen könnte. Sie verwechseln zwei Dinge: Deadlocking und Hunger.

  • Sie können Deadlocks nicht abbrechen, ohne einen der Threads zu unterbrechen. Wenn Sie also in einen Deadlock geraten, können Sie nicht mehr raus
  • Das Verhungern endet automatisch, nachdem einer der Threads seine Arbeit beendet hat

Hier ist ein Bild, das den Unterschied veranschaulicht.

Fazit
Sie können es trotzdem sicher verwenden, lock(this)wenn der Thread-Mangel für Sie kein Problem darstellt. Sie müssen immer noch bedenken, dass wenn der Faden, der mit Fäden hungert, lock(this)in einem Schloss endet, in dem Ihr Objekt gesperrt ist, er schließlich in ewigem Hunger endet;)


9
Es gibt einen Unterschied, aber er ist für diese Diskussion völlig irrelevant. Und der erste Satz Ihrer Schlussfolgerung ist völlig falsch.
Ben Voigt

1
Um es klar zu sagen: Ich verteidige nicht lock(this)- diese Art von Code ist einfach falsch. Ich denke nur, es als Deadlocking zu bezeichnen, ist ein wenig missbräuchlich.
SOReader

2
Der Link zum Bild ist nicht mehr verfügbar. :( Gibt es eine Chance, dass Sie es erneut referenzieren können? Thx
VG1


1

Hier ist ein Beispielcode, der einfacher zu befolgen ist (IMO): (Funktioniert in LinqPad und verweist auf folgende Namespaces: System.Net und System.Threading.Tasks)

Zu beachten ist, dass lock (x) im Grunde genommen syntaktischer Zucker ist und Monitor.Enter verwendet und dann try, catch, finally block verwendet, um Monitor.Exit aufzurufen. Siehe: https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (Abschnitt "Anmerkungen")

Oder verwenden Sie die C # -Sperranweisung (SyncLock-Anweisung in Visual Basic), die die Enter- und Exit-Methoden in einem try… finally-Block umschließt.

void Main()
{
    //demonstrates why locking on THIS is BADD! (you should never lock on something that is publicly accessible)
    ClassTest test = new ClassTest();
    lock(test) //locking on the instance of ClassTest
    {
        Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Parallel.Invoke(new Action[]
        {
            () => {
                //this is there to just use up the current main thread. 
                Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
                },
            //none of these will enter the lock section.
            () => test.DoWorkUsingThisLock(1),//this will dead lock as lock(x) uses Monitor.Enter
            () => test.DoWorkUsingMonitor(2), //this will not dead lock as it uses Montory.TryEnter
        });
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        lock(this) //this can be bad if someone has locked on this already, as it will cause it to be deadlocked!
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
        }
        Console.WriteLine($"End ClassTest.DoWorkUsingThisLock Done {i}  CurrentThread {Thread.CurrentThread.ManagedThreadId}");
    }

    public void DoWorkUsingMonitor(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        if (Monitor.TryEnter(this))
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            Monitor.Exit(this);
        }
        else
        {
            Console.WriteLine($"Skipped lock section!  {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        }

        Console.WriteLine($"End ClassTest.DoWorkUsingMonitor Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine();
    }
}

Ausgabe

CurrentThread 15
CurrentThread 15
Start ClassTest.DoWorkUsingMonitor 2 CurrentThread 13
Start ClassTest.DoWorkUsingThisLock 1 CurrentThread 12
Skipped lock section!  2 CurrentThread 13
End ClassTest.DoWorkUsingMonitor Done 2 CurrentThread 13

Beachten Sie, dass Thread Nr. 12 niemals endet, wenn er tot gesperrt ist.


1
scheint, dass der zweite DoWorkUsingThisLockThread nicht notwendig ist, um das Problem zu veranschaulichen?
Jack Lu

Meinst du nicht die Outter-Sperre in der Hauptleitung, ein Thread würde einfach warten, bis der andere abgeschlossen ist? was dann die Parallele ungültig machen würde ... Ich glaube, wir brauchen bessere Beispiele aus der realen Welt.
Seabizkit

@Seabizkit, hat den Code aktualisiert, um ihn etwas klarer zu gestalten. Die Parallele dient nur dazu, einen neuen Thread zu erstellen und den Code asynchron auszuführen. In Wirklichkeit könnte der 2. Thread auf verschiedene Arten aufgerufen worden sein (Klick auf eine Schaltfläche, separate Anfrage usw.).
Raj Rao

0

Sie können eine Regel festlegen, die besagt, dass eine Klasse Code haben kann, der 'this' oder jedes Objekt sperrt, das der Code in der Klasse instanziiert. Es ist also nur dann ein Problem, wenn das Muster nicht befolgt wird.

Wenn Sie sich vor Code schützen möchten, der diesem Muster nicht folgt, ist die akzeptierte Antwort korrekt. Aber wenn das Muster befolgt wird, ist es kein Problem.

Der Vorteil von lock (this) ist die Effizienz. Was ist, wenn Sie ein einfaches "Wertobjekt" haben, das einen einzelnen Wert enthält? Es ist nur ein Wrapper und wird millionenfach instanziiert. Indem Sie die Erstellung eines privaten Synchronisierungsobjekts nur zum Sperren benötigen, haben Sie die Größe des Objekts und die Anzahl der Zuordnungen verdoppelt. Wenn es auf die Leistung ankommt, ist dies ein Vorteil.

Wenn Sie sich nicht um die Anzahl der Zuordnungen oder den Speicherbedarf kümmern, ist es aus den in anderen Antworten angegebenen Gründen vorzuziehen, die Sperre (dies) zu vermeiden.


-1

Es tritt ein Problem auf, wenn auf die Instanz öffentlich zugegriffen werden kann, da möglicherweise andere Anforderungen dieselbe Objektinstanz verwenden. Es ist besser, private / statische Variablen zu verwenden.


5
Ich bin mir nicht sicher, was das dem Mann hinzufügt. Es gibt bereits detaillierte Antworten, die dasselbe sagen.
Andrew Barber
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.