Dlaczego blokada (this) {…} jest zła?


484

Dokumentacja MSDN tak mówi

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

to „problem, jeśli instancja jest dostępna publicznie”. Zastanawiam się dlaczego? Czy to dlatego, że zamek będzie trzymany dłużej niż to konieczne? A może jest jakiś bardziej podstępny powód?

Odpowiedzi:


508

Jest to zła forma do użycia thisw instrukcjach blokady, ponieważ generalnie nie masz kontroli, kto jeszcze mógłby zablokować ten obiekt.

Aby właściwie zaplanować operacje równoległe, należy zwrócić szczególną uwagę na możliwe sytuacje zakleszczenia, a utrudnia to nieznana liczba punktów wejścia do śluzy. Na przykład każdy, kto ma odniesienie do obiektu, może się na nim zablokować bez wiedzy projektanta / twórcy obiektu. Zwiększa to złożoność rozwiązań wielowątkowych i może wpływać na ich poprawność.

Pole prywatne jest zwykle lepszą opcją, ponieważ kompilator będzie wymuszał ograniczenia dostępu do niego i obuduje mechanizm blokujący. Używanie thisnarusza enkapsulację, udostępniając publicznie część implementacji blokowania. Nie jest również jasne, że uzyskasz blokadę, thischyba że została udokumentowana. Nawet wtedy poleganie na dokumentacji, aby zapobiec problemowi, nie jest optymalne.

Wreszcie istnieje powszechne nieporozumienie, które lock(this)faktycznie modyfikuje obiekt przekazywany jako parametr i w pewien sposób czyni go tylko do odczytu lub niedostępnym. To jest fałsz . Obiekt przekazany jako parametr locksłuży jedynie jako klucz . Jeśli blokada jest już utrzymywana na tym kluczu, blokada nie może być wykonana; w przeciwnym razie blokada jest dozwolona.

Dlatego źle jest używać ciągów jako kluczy w lockinstrukcjach, ponieważ są one niezmienne i są udostępniane / dostępne w różnych częściach aplikacji. Zamiast tego powinieneś użyć zmiennej prywatnej, Objectinstancja dobrze się sprawdzi.

Uruchom następujący kod C # jako przykład.

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

Wyjście konsoli

'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
Jak ja grok: (1) Nancy jest w wątku 1 z zamkiem (this). (2) JEDNA Nancy jest w trakcie starzenia się wątku 2, podczas gdy wciąż jest zablokowana w wątku 1 - co dowodzi, że zablokowany obiekt nie jest tylko do odczytu. TAKŻE (2a), gdy w wątku 2, ten obiekt Nancy jest również zablokowany na nazwie. (3) Utwórz INNY obiekt o tej samej nazwie . (4) Przejdź do wątku 3 i spróbuj zablokować za pomocą nazwy. (duże wykończenie) ALE „ciągi są niezmienne”, co oznacza, że ​​każdy obiekt odwołujący się do ciągu „Nancy Drew” patrzy dosłownie na tę samą instancję ciągu w pamięci. Więc object2 nie może uzyskać blokady łańcucha, gdy object1 jest zablokowany na tej samej wartości
radarbob

Używanie zmiennej standardowej zamiast lock(this)standardowej porady; ważne jest, aby pamiętać, że takie postępowanie ogólnie uniemożliwi kod zewnętrzny, aby blokada związana z obiektem była wstrzymywana między wywołaniami metod. To może, ale nie musi, być dobrą rzeczą . Istnieje pewne niebezpieczeństwo zezwalające zewnętrznemu kodowi na blokadę na dowolny czas, a klasy powinny być ogólnie zaprojektowane tak, aby takie użycie było niepotrzebne, ale nie zawsze istnieją praktyczne alternatywy. Jako prosty przykład, chyba że kolekcja implementuje własną metodę ToArraylub ToListmetodę ...
supercat

4
(w przeciwieństwie do metod rozszerzenia `IEnumerable <T>), jedynym sposobem dla wątku, który chce uzyskać migawkę kolekcji, może być jej wyliczenie podczas blokowania wszystkich zmian . W tym celu musi mieć dostęp do blokady uzyskanej przez dowolny kod, który zmieniłby kolekcję. Brak ujawnienia blokady może uniemożliwić np. Okresowe wykonywanie asynchronicznej migawki kolekcji (np. W celu aktualizacji interfejsu użytkownika do przeglądania kolekcji).
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- Uważam, że te rozmowy dotyczą bitu SyncBlock w obiekcie CLR, więc formalnie jest to słuszne - sam blokuj zmodyfikowany obiekt
sll

@Esteban, uwielbiam twój przykład, jest niesamowity. Mam pytanie do ciebie. Twój kod metody NameChange (..) kończy się na: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . } else Monitor.Exit (person.Name); </code> Czy nie powinien kończyć się na: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . Monitor.Exit (person.Name); } </code>
AviFarah

64

Ponieważ jeśli ludzie mogą dostać się do thiswskaźnika instancji obiektu (tj. Twojego ), mogą także spróbować zablokować ten sam obiekt. Teraz mogą nie wiedzieć, że blokujesz się thiswewnętrznie, więc może to powodować problemy (być może impas)

Oprócz tego jest to również zła praktyka, ponieważ blokuje „za dużo”

Na przykład możesz mieć zmienną składową List<int>, a jedyną rzeczą, którą musisz zablokować, jest zmienna składowa. Jeśli zablokujesz cały obiekt w swoich funkcjach, wówczas inne rzeczy wywołujące te funkcje zostaną zablokowane w oczekiwaniu na blokadę. Jeśli te funkcje nie będą musiały uzyskiwać dostępu do listy członków, spowoduje to, że inny kod będzie czekać i spowolnić aplikację bez żadnego powodu.


44
Ostatni akapit tej odpowiedzi jest nieprawidłowy. Blokada w żaden sposób nie czyni obiektu niedostępnym ani tylko do odczytu. Blokada (this) nie uniemożliwia wywołania lub modyfikacji innego wątku, do którego odwołuje się ten wątek.
Esteban Brenes

3
Dzieje się tak, jeśli inne wywoływane metody również wykonują blokadę (this). Myślę, że o to mu chodziło. Zwróć uwagę na „Jeśli zablokujesz cały obiekt w swoich funkcjach” ...
Herms,

@Orion: To wyraźniejsze. @Herms: Tak, ale nie musisz używać „tego”, aby osiągnąć tę funkcjonalność, właściwość SyncRoot na listach służy temu celowi, na przykład, jednocześnie zapewniając wyraźną synchronizację tego klucza.
Esteban Brenes

Re: blokowanie „za dużo”: jest to dobra równowaga decydująca o tym, co zablokować. Należy pamiętać, że wykonanie blokady wymaga operacji procesora na pamięci podręcznej i jest nieco kosztowne. Innymi słowy: nie blokuj i nie aktualizuj poszczególnych liczb całkowitych. :)
Zan Lynx

Ostatni akapit wciąż nie ma sensu. Jeśli musisz ograniczyć dostęp do listy, dlaczego inne funkcje miałyby blokady, jeśli nie miałyby dostępu do listy?
Joakim MH,

44

Rzuć okiem na temat synchronizacji wątków MSDN (Podręcznik programowania w języku C #)

Zasadniczo najlepiej jest unikać blokowania typu publicznego lub instancji obiektów poza kontrolą aplikacji. Na przykład blokada (ta) może być problematyczna, jeśli instancja jest dostępna publicznie, ponieważ kod poza twoją kontrolą może również blokować obiekt. Może to powodować sytuacje zakleszczenia, w których dwa lub więcej wątków czeka na zwolnienie tego samego obiektu. Zablokowanie publicznego typu danych, w przeciwieństwie do obiektu, może powodować problemy z tego samego powodu. Blokowanie ciągów literalnych jest szczególnie ryzykowne, ponieważ ciągi literalne są internowane przez środowisko uruchomieniowe języka wspólnego (CLR). Oznacza to, że istnieje jedno wystąpienie dowolnego literału ciągowego dla całego programu, dokładnie ten sam obiekt reprezentuje literał we wszystkich działających domenach aplikacji, we wszystkich wątkach. W rezultacie blokada umieszczona na ciągu o tej samej zawartości w dowolnym miejscu w procesie aplikacji blokuje wszystkie wystąpienia tego ciągu w aplikacji. W rezultacie najlepiej jest zablokować członka prywatnego lub chronionego, który nie jest internowany. Niektóre klasy zapewniają członków specjalnie do blokowania. Na przykład typ macierzy zapewnia SyncRoot. Wiele typów kolekcji zapewnia również element SyncRoot.


34

Wiem, że to stary wątek, ale ponieważ ludzie wciąż mogą to sprawdzić i polegać na nim, wydaje się ważne, aby zauważyć, że lock(typeof(SomeObject))jest znacznie gorzej niż lock(this). Powiedziawszy to; szczere podziękowania dla Alana za wskazanie, że lock(typeof(SomeObject))jest to zła praktyka.

Instancja System.Typejest jednym z najbardziej ogólnych, gruboziarnistych obiektów. Przynajmniej instancja System.Type jest globalna dla AppDomain, a .NET może uruchamiać wiele programów w AppDomain. Oznacza to, że dwa zupełnie różne programy mogą potencjalnie powodować wzajemne zakłócenia, nawet w stopniu powodującym zakleszczenie, jeśli oba będą próbowały uzyskać blokadę synchronizacji dla tego samego typu wystąpienia.

Nie lock(this)jest to więc szczególnie mocna forma, może powodować problemy i zawsze powinna podnosić brwi z wszystkich wymienionych powodów. Istnieje jednak powszechnie stosowany, względnie szanowany i pozornie stabilny kod, taki jak log4net, który intensywnie używa wzorca blokady (tego), chociaż osobiście wolałbym zobaczyć zmianę tego wzorca.

Ale lock(typeof(SomeObject))otwiera się zupełnie nowa i ulepszona puszka robaków.

Tyle ile jest warte.


26

... i dokładnie te same argumenty dotyczą również tego konstruktu:

lock(typeof(SomeObject))

17
lock (typeof (SomeObject)) jest w rzeczywistości znacznie gorszy niż lock (this) ( stackoverflow.com/a/10510647/618649 ).
Craig

1
cóż, lock (Application.Current) jest jeszcze gorszy, ale kto by spróbował jednej z tych głupich rzeczy? lock (this) wydaje się logiczny i pomocny, ale te inne przykłady nie.
Zar Shardan

Nie zgadzam się, że lock(this)wydaje się to szczególnie logiczne i zwięzłe. Jest to strasznie zgrubna blokada, a każdy inny kod może zablokować Twój obiekt, potencjalnie powodując zakłócenia w twoim wewnętrznym kodzie. Weź więcej granularnych zamków i przejmij lepszą kontrolę. To, lock(this)co się dzieje, jest o wiele lepsze niż lock(typeof(SomeObject)).
Craig

8

Wyobraź sobie, że masz wykwalifikowanego sekretarza w swoim biurze, który jest wspólnym zasobem w dziale. Raz na jakiś czas spieszycie się do nich, ponieważ macie zadanie, tylko po to, aby mieć nadzieję, że inny z waszych współpracowników jeszcze ich nie odebrał. Zwykle trzeba tylko chwilę poczekać.

Ponieważ opieka jest dzieleniem się, kierownik decyduje, że klienci mogą również bezpośrednio korzystać z sekretarki. Ma to jednak efekt uboczny: klient może nawet przejąć je, gdy pracujesz dla tego klienta i potrzebujesz go również do wykonania części zadań. Występuje impas, ponieważ roszczenia nie są już hierarchią. Można było tego uniknąć razem, nie pozwalając klientom w pierwszej kolejności dochodzić ich roszczeń.

lock(this)jest zły, jak widzieliśmy. Obiekt zewnętrzny może zablokować obiekt, a ponieważ nie kontrolujesz, kto używa klasy, każdy może go zablokować ... To jest dokładny przykład, jak opisano powyżej. Ponownie rozwiązaniem jest ograniczenie ekspozycji obiektu. Jednakże, jeśli masz private, protectedlub internalklasa ty mógł już kontrolować, kto jest blokowanie na obiekcie , bo jesteś pewien, że masz napisany kod siebie. Oto wiadomość: nie ujawniaj tego jako public. Ponadto zapewnienie użycia blokady w podobnym scenariuszu pozwala uniknąć zakleszczeń.

Całkowitym przeciwieństwem tego jest blokowanie zasobów współdzielonych w domenie aplikacji - najgorszy scenariusz. To tak, jakbyś wyrzucił swoją sekretarkę na zewnątrz i pozwolił wszystkim jej domagać się. Rezultatem jest całkowity chaos - lub jeśli chodzi o kod źródłowy: był to zły pomysł; wyrzuć to i zacznij od nowa. Jak to robimy?

Typy są udostępniane w domenie aplikacji, jak wskazuje większość osób tutaj. Ale są jeszcze lepsze rzeczy, których możemy użyć: łańcuchy. Powodem jest to, że ciągi są łączone . Innymi słowy: jeśli masz dwa ciągi, które mają tę samą zawartość w domenie aplikacji, istnieje szansa, że ​​mają dokładnie taki sam wskaźnik. Ponieważ wskaźnik jest używany jako klucz blokady, w zasadzie otrzymujesz synonim „przygotuj się na niezdefiniowane zachowanie”.

Podobnie nie powinieneś blokować obiektów WCF, HttpContext.Current, Thread.Current, Singletons (ogólnie) itp. Najłatwiejszym sposobem na uniknięcie tego wszystkiego? private [static] object myLock = new object();


3
W rzeczywistości posiadanie prywatnej klasy nie zapobiega temu problemowi. Kod zewnętrzny może uzyskać odwołanie do wystąpienia klasy prywatnej ...
Rashack

1
@Rashack, gdy technicznie masz rację (+1 za wskazanie tego), miałem na myśli, że powinieneś kontrolować, kto blokuje instancję. Powracanie takich instancji to psuje.
atlaste

4

Blokowanie tego wskaźnika może być złe, jeśli blokujesz udostępniony zasób . Współużytkowanym zasobem może być zmienna statyczna lub plik na twoim komputerze - tj. Coś, co jest współużytkowane przez wszystkich użytkowników klasy. Powodem jest to, że wskaźnik ten będzie zawierał inne odwołanie do lokalizacji w pamięci za każdym razem, gdy twoja instancja zostanie utworzona. Zatem zablokowanie tego w jednym wystąpieniu klasy różni się od zablokowania tego w innym wystąpieniu klasy.

Sprawdź ten kod, aby zobaczyć, co mam na myśli. Dodaj następujący kod do głównego programu w aplikacji konsoli:

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

Utwórz nową klasę jak poniżej.

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

Oto przebieg programu blokującego na tym .

   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

Oto uruchomienie programu blokującego na 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

1
co należy zwrócić uwagę na przykład, na przykład to, co pokazujesz, co jest niepoprawne. trudno jest zauważyć, co jest nie tak, gdy używasz Random rand = new Random();NVM, myślę, że widzę jego powtarzające się saldo
Seabizkit

3

Jest na ten temat bardzo dobry artykuł http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects autorstwa Rico Mariani, architekta wydajności dla środowiska uruchomieniowego Microsoft® .NET

Fragment:

Podstawowym problemem jest to, że nie jesteś właścicielem obiektu typu i nie wiesz, kto jeszcze mógłby uzyskać do niego dostęp. Ogólnie rzecz biorąc, bardzo złym pomysłem jest poleganie na blokowaniu obiektu, którego nie utworzyłeś i nie wiesz, kto jeszcze może uzyskać do niego dostęp. Takie postępowanie powoduje impas. Najbezpieczniejszym sposobem jest zamknięcie tylko prywatnych obiektów.



2

Oto o wiele prostsza ilustracja (zaczerpnięta z pytania 34 tutaj ), dlaczego blokada (ta) jest zła i może powodować zakleszczenia, gdy konsument twojej klasy również próbuje zablokować na obiekcie. Poniżej tylko jeden z trzech wątków może być kontynuowany, pozostałe dwa są zakleszczone.

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

Aby obejść ten problem, ten facet użył Thread.TryMonitor (z limitem czasu) zamiast blokady:

            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


O ile widzę, kiedy zamieniam zamek (ten) na zamek na prywatnej instancji członka SomeClass, nadal mam ten sam impas. Ponadto, jeśli blokada w klasie głównej zostanie wykonana na innym prywatnym wystąpieniu członka Programu, nastąpi taka sama blokada. Nie jestem pewien, czy ta odpowiedź nie wprowadza w błąd i jest nieprawidłowa. Zobacz to zachowanie tutaj: dotnetfiddle.net/DMrU5h
Bartosz

1

Ponieważ każdy fragment kodu, który widzi instancję klasy, może również zablokować to odwołanie. Chcesz ukryć (obudować) obiekt blokujący, aby mógł odwoływać się do niego tylko kod, który musi się do niego odwoływać. Słowo kluczowe odnosi się do bieżącej instancji klasy, więc dowolna liczba rzeczy może mieć do niej odniesienie i może być używana do synchronizacji wątków.

Żeby było jasne, jest to złe, ponieważ jakiś inny fragment kodu może użyć instancji klasy do zablokowania i może uniemożliwić uzyskanie w odpowiednim czasie blokady kodu lub może spowodować inne problemy z synchronizacją wątków. Najlepszy przypadek: nic innego nie używa odniesienia do twojej klasy do zablokowania. Środkowy przypadek: coś korzysta z odwołania do twojej klasy, aby robić blokady i powoduje problemy z wydajnością. Najgorszy przypadek: coś korzysta z referencji twojej klasy do robienia blokad i powoduje naprawdę złe, bardzo subtelne, bardzo trudne do debugowania problemy.


1

Przykro mi, ale nie mogę zgodzić się z argumentem, że zablokowanie tego może spowodować impas. Mylisz dwie rzeczy: impas i głód.

  • Nie możesz anulować impasu bez przerywania jednego z wątków, więc po wejściu w impas nie możesz się wydostać
  • Głodzenie zakończy się automatycznie po zakończeniu pracy jednego z wątków

Oto zdjęcie ilustrujące różnicę.

Wniosek
Możesz nadal bezpiecznie używać, lock(this)jeśli głód nici nie stanowi dla ciebie problemu. Nadal musisz pamiętać, że kiedy nić głodująca wykorzystująca nić lock(this)kończy się w zamku, w którym twój obiekt jest zablokowany, ostatecznie skończy się wiecznym głodem;)


9
Jest różnica, ale jest całkowicie nieistotna w tej dyskusji. A pierwsze zdanie twojego wniosku jest całkowicie błędne.
Ben Voigt,

1
Żeby było jasne: nie bronię lock(this)- ten rodzaj kodu jest po prostu zły. Myślę, że nazwanie tego zakleszczeniem jest trochę obelżywe.
SOReader,

2
Link do obrazu nie jest już dostępny. :( Czy jest
jakaś


1

Oto przykładowy kod, który jest prostszy do naśladowania (IMO): ( Działa w LinqPad , do następujących przestrzeni nazw: System.Net i System.Threading.Tasks)

Należy pamiętać, że lock (x) to po prostu cukier składniowy, a to, co robi, to użycie Monitor.Enter, a następnie skorzystanie z try, catch, wreszcie blok do wywołania Monitor.Exit. Zobacz: https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (sekcja uwag)

lub użyj instrukcji C # lock (instrukcja SyncLock w języku Visual Basic), która otacza metody Enter i Exit blokiem try… wreszcie.

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

Wynik

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

Zauważ, że Wątek nr 12 nigdy się nie kończy, ponieważ jest martwy.


1
wydaje się, że drugi DoWorkUsingThisLockwątek nie jest konieczny do zilustrowania problemu?
Jack Lu

nie masz na myśli blokady zewnętrznej w jednym, jeden wątek po prostu czekałby na zakończenie drugiego? co wtedy unieważniłoby Parallel ... czuję, że potrzebujemy lepszych przykładów z prawdziwego świata.
Seabizkit

@Seabizkit, zaktualizowałem kod, aby był nieco jaśniejszy. Parallel jest po to, aby utworzyć nowy wątek i uruchomić kod asynchronicznie. W rzeczywistości drugi wątek mógł zostać wywołany na wiele sposobów (kliknięcie przycisku, osobne żądanie itp.).
Raj Rao

0

Możesz ustanowić regułę, która mówi, że klasa może mieć kod blokujący „to” lub dowolny obiekt, który tworzy kod w klasie. Jest to problem tylko wtedy, gdy wzór nie jest przestrzegany.

Jeśli chcesz uchronić się przed kodem, który nie będzie zgodny z tym wzorcem, zaakceptowana odpowiedź jest poprawna. Ale jeśli ten wzór jest przestrzegany, nie stanowi to problemu.

Zaletą blokady (to) jest wydajność. Co jeśli masz prosty „obiekt wartości” zawierający jedną wartość. To tylko opakowanie, które pojawia się miliony razy. Wymagając utworzenia prywatnego obiektu synchronizacji tylko do blokowania, w zasadzie podwoiłeś rozmiar obiektu i podwoiłeś liczbę przydziałów. Gdy liczy się wydajność, jest to zaleta.

Jeśli nie obchodzi Cię liczba przydziałów lub ilość pamięci, lepiej unikać blokady (z tego powodu) z powodów wskazanych w innych odpowiedziach.


-1

Wystąpi problem, jeśli instancja będzie dostępna publicznie, ponieważ mogą istnieć inne żądania, które mogą korzystać z tej samej instancji obiektu. Lepiej jest użyć zmiennej prywatnej / statycznej.


5
Nie jestem pewien, co to dodaje człowiekowi, szczegółowe odpowiedzi już istnieją, które mówią to samo.
Andrew Barber
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.