web-dev-qa-db-de.com

Warum ist lock (this) {...} schlecht?

Die MSDN-Dokumentation sagt das

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 die Sperre länger als nötig gehalten wird? Oder gibt es einen hinterlistigeren Grund?

439
Anton

Die Verwendung von this in Sperranweisungen ist nicht geeignet, da Sie normalerweise nicht die Kontrolle darüber haben, wer sonst das Objekt sperren kann.

Um einen parallelen Betrieb richtig zu planen, sollte besonders auf mögliche Deadlock-Situationen geachtet werden. Eine unbekannte Anzahl von Sperreneintrittspunkten behindert dies. Zum Beispiel kann jeder, der einen Verweis auf das Objekt hat, darauf zugreifen, ohne dass der Objektentwickler/Ersteller davon Kenntnis hat. Dies erhöht die Komplexität von Multithread-Lösungen und kann deren Korrektheit beeinträchtigen.

Ein privates Feld ist normalerweise eine bessere Option, da der Compiler Zugriffsbeschränkungen für dieses Feld erzwingt und den Sperrmechanismus kapselt. Die Verwendung von this verstößt gegen die Kapselung, indem ein Teil Ihrer Sperrimplementierung der Öffentlichkeit zugänglich gemacht wird. Es ist auch nicht klar, dass Sie eine Sperre für this erwerben, sofern diese nicht dokumentiert ist. Selbst dann ist es nicht optimal, sich auf die Dokumentation zu verlassen, um ein Problem zu vermeiden.

Schließlich gibt es die weit verbreitete falsche Vorstellung, dass lock(this) das als Parameter übergebene Objekt tatsächlich ändert und in gewisser Weise schreibgeschützt oder nicht zugänglich ist. Das ist false. Das an lock als Parameter übergebene Objekt dient lediglich als key. Wenn bereits eine Sperre für diesen Schlüssel gehalten wird, kann die Sperre nicht ausgeführt werden. Andernfalls ist die Sperre zulässig.

Aus diesem Grund ist es schlecht, Strings als Schlüssel in lock-Anweisungen zu verwenden, da sie unveränderlich sind und von Teilen der Anwendung gemeinsam genutzt werden. Sie sollten stattdessen eine private Variable verwenden, eine Object-Instanz eignet sich 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'.
473
Esteban Brenes

Wenn Personen auf Ihre Objektinstanz zugreifen können (z. B. Ihre this), können sie auch versuchen, dasselbe Objekt zu sperren. Nun ist ihnen möglicherweise nicht bewusst, dass Sie intern this sperren, sodass dies Probleme verursachen kann (möglicherweise ein Deadlock).

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

Beispielsweise haben Sie möglicherweise eine Membervariable von List<int>, und das einzige, was Sie wirklich sperren müssen, ist diese Membervariable. Wenn Sie das gesamte Objekt in Ihren Funktionen sperren, werden andere Funktionen, die diese Funktionen aufrufen, gesperrt, wenn Sie auf die Sperre warten. Wenn diese Funktionen nicht auf die Mitgliederliste zugreifen müssen, müssen Sie anderen Code warten und Ihre Anwendung ohne Grund verlangsamen.

60
Orion Edwards

Schauen Sie sich das MSDN-Thema Thread-Synchronisierung (C # -Programmierhandbuch) an

Im Allgemeinen ist es am besten, das Sperren zu vermeiden auf einem öffentlichen Typ oder auf einem Objekt Instanzen außerhalb der Kontrolle Ihres Anwendung. Zum Beispiel sperren (this) kann problematisch sein, wenn die Instanz öffentlich zugänglich sein, weil Code außerhalb Ihrer Kontrolle kann sich der Objekt auch. Dadurch könnte Deadlock-Situationen, in denen zwei oder mehr Threads warten auf die Freigabe von gleiches Objekt. Sperren einer öffentlichen Datentyp, im Gegensatz zu einem Objekt, kann zu Problemen führen Grund. Für literale Zeichenfolgen gilt: besonders riskant, weil wörtlich Zeichenketten werden von der allgemeinen .__ interniert. Sprachlaufzeit (CLR). Das heisst dass es eine Instanz von .__ gibt. gegebenes String-Literal für das gesamte Programm, dasselbe Objekt repräsentiert das Literal in allen laufenden Anwendungsdomänen auf allen Threads . Infolgedessen wurde eine Sperre auf eine Zeichenfolge gesetzt mit den gleichen Inhalten überall in der Anwendungsprozess sperrt alle Instanzen dieser Zeichenfolge in der Anwendung. Daher ist es am besten um ein privates oder geschütztes Mitglied zu sperren das ist nicht interniert. Einige Klassen Mitglieder speziell für Verriegelung Der Array-Typ, zum Beispiel bietet SyncRoot. Viele sammlung Types stellen einen SyncRoot-Member als .__ bereit. Gut.

42
crashmstr

Ich weiß, dass dies ein alter Faden ist, aber da die Leute immer noch nachschlagen und sich darauf verlassen können, scheint es wichtig zu wissen, dass lock(typeof(SomeObject)) wesentlich schlechter ist als lock(this). Das gesagt haben; Ein aufrichtiges Lob an Alan für den Hinweis, dass lock(typeof(SomeObject)) schlechte Praxis ist.

Eine Instanz von System.Type ist eines der generischsten und grobkörnigsten Objekte, die es gibt. Zumindest eine Instanz von System.Type ist 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 ein Deadlock erstellt wird, wenn beide versuchen, eine Synchronisationssperre für dieselbe Instanz des Typs zu erhalten.

Daher ist lock(this) nicht besonders robust, kann Probleme verursachen und sollte aus allen genannten Gründen immer die Augenbrauen hochziehen. Es gibt jedoch weit verbreiteten, relativ respektierten und scheinbar stabilen Code wie log4net, der das Lock-Muster (dieses) ausgiebig verwendet, obwohl ich es persönlich vorziehen würde, dieses Muster zu ändern.

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

Für was es wert ist.

32
Craig

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

lock(typeof(SomeObject))
25
Alan

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

Da die Fürsorge geteilt wird, entscheidet Ihr Manager, dass Kunden die Sekretärin auch direkt nutzen können. Dies hat jedoch einen Nebeneffekt: Ein Kunde beansprucht sie möglicherweise sogar, 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 die Kunden diese überhaupt nicht beanspruchen konnten.

lock(this) ist schlecht, wie wir gesehen haben. Ein externes Objekt kann das Objekt sperren, und da Sie nicht steuern können, wer die Klasse verwendet, kann jeder das Objekt sperren. Dies ist genau das oben beschriebene Beispiel. Auch hier besteht die Lösung darin, die Exposition des Objekts zu begrenzen. Wenn Sie jedoch eine Klasse private, protected oder internal haben, können Sie könnte bereits steuern, wer Ihr Objekt sperrt, weil Sie sicher sind, dass Sie Haben Sie Ihren Code selbst geschrieben? Die Meldung hier lautet also: Machen Sie es nicht als public verfügbar. Durch die Verwendung einer Sperre in ähnlichen Szenarien werden außerdem Deadlocks vermieden.

Das genaue Gegenteil davon ist, sich auf Ressourcen zu beschränken, 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 jedem erlauben, sie zu fordern. Das Ergebnis ist völliges Chaos - oder was den Quellcode betrifft: Es war eine schlechte Idee; wirf es weg und fang von vorne an. Wie machen wir das?

Typen werden in der App-Domain geteilt, wie die meisten Leute hier betonen. Aber es gibt noch bessere Dinge, die wir verwenden können: Streicher. Der Grund dafür ist, dass Zeichenfolgen zusammengefasst werden. Mit anderen Worten: Wenn Sie zwei Zeichenfolgen haben, die denselben 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, ist das, was Sie im Grunde erhalten, ein Synonym für "Auf undefiniertes Verhalten vorbereiten".

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

7
atlaste

Das Sperren des Zeigers this kann bad sein, wenn Sie eine shared-Ressource sperren. Eine freigegebene Ressource kann eine statische Variable oder eine Datei auf Ihrem Computer sein, d. H. Etwas, das von allen Benutzern der Klasse gemeinsam genutzt wird. Der Grund ist, dass dieser Zeiger bei jeder Instanziierung der Klasse einen anderen Verweis auf einen Speicherplatz enthält. Das Sperren von this in einer Instanz einer Klasse unterscheidet sich also vom Sperren von this in einer anderen Instanz einer Klasse.

Überprüfen Sie diesen Code, 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 unten.

 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 läuft das Programm auf 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

Hier läuft das Programm auf 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

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

Auszug:

Das grundlegende Problem hier ist, dass Sie nicht das Typobjekt besitzen und Sie Ich weiß nicht, wer sonst darauf zugreifen könnte. Im Allgemeinen ist das eine sehr schlechte Idee sich auf das Sperren eines Objekts verlassen, das Sie nicht erstellt haben, und nicht wissen, wer sonst noch könnte zugreifen. Dies führt zu einem Stillstand. Der sicherste Weg ist zu Nur private Objekte sperren.

3
Vikrant

Darüber gibt es auch einige gute Diskussionen: Ist dies die richtige Verwendung eines Mutex?

2
Bob Nadler

Hier ist ein Beispielcode, der einfacher zu befolgen ist (IMO): (Funktioniert in LinqPad , verweist auf folgende Namespaces: System.Net und 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

Jeder Codeabschnitt, der die Instanz Ihrer Klasse sehen kann, kann diese Referenz auch sperren. Sie möchten Ihr Sperrobjekt ausblenden (kapseln), sodass nur Code, der referenziert werden muss, darauf verweisen kann. Das Schlüsselwort this bezieht sich auf die aktuelle Klasseninstanz, sodass eine beliebige Anzahl von Dingen einen Verweis darauf haben und für die Thread-Synchronisierung verwenden kann.

Um es klar zu sagen, das ist schlecht, da einige andere Codestücke die Klasseninstanz zum Sperren verwenden könnten und verhindern könnten, dass Ihr Code eine zeitliche Sperre erhält, oder dass andere Thread-Synchronisierungsprobleme auftreten. Bester Fall: Nichts anderes verwendet einen Verweis auf Ihre Klasse zum Sperren. Mittlerer Fall: Etwas verwendet einen Verweis auf Ihre Klasse, um Sperren auszuführen, und verursacht Leistungsprobleme. Schlechtester Fall: Etwas verwendet einen Verweis auf Ihre Klasse, um Sperren auszuführen, und führt zu wirklich schlimmen, wirklich subtilen, wirklich schwer zu debuggenden Problemen.

1
Jason Jackson

Hier ist eine viel einfachere Illustration (aus Frage 34 hier ), warum lock (this) schlecht ist und zu Deadlocks führen kann, wenn Benutzer Ihrer Klasse auch versuchen, das Objekt zu sperren Thread kann fortfahren, die beiden anderen 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 dieses Problem zu umgehen, verwendete dieser Typ Thread.TryMonitor (mit Timeout) anstelle der Sperre:

            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

Bitte beachten Sie den folgenden Link, der erklärt, warum das Sperren (dies) keine gute Idee ist.

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

Die Lösung besteht also darin, ein privates Objekt, z. B. lockObject, zur Klasse hinzuzufügen und den Codebereich innerhalb der lock-Anweisung wie folgt zu platzieren:

lock (lockObject)
{
...
}
1

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

  • Sie können den Deadlock nicht abbrechen, ohne einen der Threads zu unterbrechen, sodass Sie nach dem Deadlock nicht mehr herauskommen können
  • Das Verhungern wird automatisch beendet, nachdem einer der Threads seine Arbeit beendet hat

Hier ist ein Bild, das den Unterschied veranschaulicht.

Fazit
Sie können lock(this) sicher verwenden, wenn der Thread-Hunger kein Problem für Sie darstellt. Sie müssen immer noch bedenken, dass der Thread, der mit lock(this) ausgehungert ist, in einem Schloss endet und Ihr Objekt gesperrt ist, endet er schließlich mit ewigem Hunger.

0
SOReader

Sie können eine Regel erstellen, die besagt, dass eine Klasse über einen Code verfügen kann, der "this" oder jedes Objekt blockiert, das der Code in der Klasse instanziiert. Es ist also nur 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. Wenn das Muster jedoch befolgt wird, ist das kein Problem.

Der Vorteil des Schlosses (dieses) 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. Durch die Erstellung eines privaten Synchronisationsobjekts nur zum Sperren haben Sie die Größe des Objekts im Wesentlichen verdoppelt und die Anzahl der Zuordnungen verdoppelt. Wenn Leistung zählt, ist dies ein Vorteil.

Wenn Sie sich nicht für die Anzahl der Zuweisungen oder den Speicherbedarf interessieren, sollten Sie das Sperren (dies) aus den in den anderen Antworten angegebenen Gründen vermeiden.

0
zumalifeguard