Recently reviewing the code I have found the following construction:
lock (this) { // Do something }
To synchronize access to shared resource object is used, in which this construction occur. Theoretically, this code is correct. What more, there was no problem with deadlock in the program.
Reviewing the literature we find the following examples:
1. In O’Reilly Programming C# we can found following example of using the keyword lock:
public void Incrementer() { try { while (counter < 1000) { int temp; lock (this) { temp = counter; temp++; Thread.Sleep(1); counter = temp; } Console.WriteLine(“{0}. Incrementer: {1}”, Thread.CurrentThread.Name, temp); } } }[/code] <p style="text-align: justify;">In addition to the above example, the book does not discuss its advantages and disadvantages.</p> <p style="text-align: justify;">2. Andrew Troelsen in Pro C# 2008 and the .NET 3.5 Platform recommends using <em>lock (this)</em> in the case of private methods:</p> [code lang="csharp"]private void SomePrivateMethod() { lock (this) { // do something } }
In both cases, the threat is ignored, which entails the use of lock (this). Let’s analyze the code below:
public class BadLock { public void Compute() { Console.WriteLine("Compute - Start"); lock (this) { Console.WriteLine("Compute - In Lock"); Thread.Sleep(5000); } Console.WriteLine("Compute - Stop"); } }
Class BadLock has in the method Compute, which simulates the process of calculation and the uses to synchronize lock (this). In most cases, the use of this code should not cause problems. However, there is a scenario, which can lead to deadlock. In a situation, when we create an object of type BadLock and we use it to ensure synchronization, and then execute a method of class BadLock can lead to deadlock. An example of such a scenario presented in the following code:
public static void BadLockTest() { Console.WriteLine("Bad Lock test 1"); BadLock badLock = new BadLock(); new Thread(new ParameterizedThreadStart(BackgroundMethod)).Start(badLock); Thread.Sleep(500); badLock.Compute(); } private static void BackgroundMethod(Object lockable) { lock (lockable) { Console.Out.WriteLine("BackgroundMethod - Lock"); Thread.Sleep(Timeout.Infinite); } }
When you start an application we obtain the following result:
Bad Lock test 1 BackgroundMethod - Lock Compute – Start
No matter how long we will wait, additional messages will not be displayed.
The solution to this problem is to replace the structure:
lock (this) { ... }
with following design:
private object synchronizationObject = new Object(); ... lock (synchronizationObject) { ... }
Only after you apply this construction we are sure, that no one will break the state of the object and will not lead to deadlock. After you change the object BadLock will look like this:
public class GoodLock { private readonly Object synchronizationObject = new Object(); public void Compute() { Console.WriteLine("Compute - Start"); lock (synchronizationObject) { Console.WriteLine("Compute - In Lock"); Thread.Sleep(5000); } Console.WriteLine("Compute - Stop"); } }
and get the correct results:
Good Lock test 1 BackgroundMethod - Lock Compute - Start Compute - In Lock Compute – Stop
At the end of the topic, still one thing remains associated with the method BackgroundMethod. I am not in favor of defensive programming, but in this case, instead of setting lock on the object, I would suggest we use also create an independent object used to synchronize.
No dobra, a jak w prototypie nie będzie obsługi błędów i jakiś programista to skopiuje to też będzie wina osoby tworzącej prototyp?
Po drugie: nie ma żadnego prawa które by łamało “lock(this)”, jest tylko luźny guideline “nie uzywania instancji poza wlasna kontrola do jako obiektów synchonizujących”. Konstrukcja jest błędo-genna, dlatego nie powinno się jej stosować w kodzie produkcyjnym i tyle.
Argument z kopiowaniem z prototypu jest nietrafiony, ponieważ kopiowanie kodu z prototypu jest o wiele większym błędem, niż ten luźny guideline dla locków.
I tak, prototyp się wyrzuca, inaczej to nie jest prototyp. Aczkolwiek rzadko stosujemy prototypy jako takie.
@Andrzej
Prototyp prototypem, ale po co stosować głupie praktyki? Ktoś na podstawie prototypu (np. mniej doświadczony programista) będzie opracowywał wersję produkcyjną -- i skopiuje, bo działa -- i nie dziwię się mu wcale. Jeśli dostanie za to burę, to źle, bo pisząc coś na podstawie prototypu nie powinno się znowu spędzać godziny na analizowaniu każdej linijki i rozmyślaniu, czy ta linijka nie jest bombą z opoźnionym zapłonem -- nie ma na to czasu skoro używa się prototypowania (nie uwierzę, że wyrzucacie kod prototypu i zaczynacie na nowo).
Myślę, że problem ze stosowaniem lock(this) leży w: po pierwsze -- jego nierozumieniu, po drugie prostocie użycia. Osoby nieznające tak naprawdę tej konstrukcji, a które gdzieś podejrzały tę konstrukcję w ciemno ją stosują mając nadzieję, że wykonuje ona jakąś MAGIĘ -- której nie robi.
Odnośnie tego co napisał Adam. A po co stosować tę konstrukcję w prototypie? Skorzystanie z obiektu synchronizującego to tak naprawdę 1 linijka więcej.
Może i jedna linijka, ale trzeba scroolować ekran na początek klasy 🙂
Ale tak na prawdę nie o to chodzi, jeżeli zrobię lock(this), a ktoś inny zrobić lock(instancja) tak jak w przykładzie, to jedna i druga osoba łamie pewną zasadę. Z czego tak na prawdę ta druga łamie tę zasadę “bardziej”. Powinny być artykuły “nie rób lock() na instancji poza twoją kontrolą” (bo taka jest zasada).
btw: jaki widzisz powód nie stosowania tej konstrukcji w prototypach (poza tym, że jest niepoprawna w kodzie produkcyjnym)? Prototyp jest do wyrzucenia, ma być napisany jak najszybciej. Jeżeli ktoś bez namysłu napisze lock(this) i to działa, to chwała mu za to.
Pomijając wszystkie wady lock(this) ma jedną zaletę (tak panowie puryści, zaletę!): jest szybkie do napisania. I nawet istnieje uzasadnienie powyższego podejścia: prototypy, PoC i małe aplikacje.
Co więcej: mówienie, że nie można robić lock(this) bo inny obiekt może zrobić lock na tej instancji też jest sensu, bo to samo w sobie jest złamaniem innej zasady “nie zakładać lock na obiektach poza twoja kontrolą”. (nota bene to ta zasada według mnie wyklucza lock(this), bo poza singeltonem żadna instancja nie tworzy siebie samej).
Ja w 99% przypadków stosuje lock na specjalnym obiekcie do tego przeznaczonym -- nigdy z tym nie ma problemów.
Ja mam jakoś złe doświadczenia z rozwiązaniami prowizorycznymi. Generalnie potrafią one bardzo długo żyć. Problemy z takim długiem technologicznym potrafią wystąpić zwykle w najmniej odpowiednim momencie.
Wg mnie jednym słusznym rozwiązanie jest stosowanie lock na specjalnym obiekcie.
Martwi mnie natomiast fakt, że 2 książki, które są bardzo często polecane do nauki programowania w C# zawierają konstrukcje, które nie powinny być używane.
Idea prototypu czy PoC zakłada wyrzucenie kodu na śmietnik. Jeżeli ktoś łamię tą zasadę to (uogólniam) konstrukcja lock(this) może być jego najmniejszym problemem.
“jednym słusznym rozwiązanie jest ”
-- bardzo nie lubię tego typu sformułowań. Zapewne, po wgłębieniu się w dyskusję, kilku przykładach, itd, po dłuższym czasie bronił byś się, że od początku chodziło ci “tylko” o jakieś konkretne warunki np >aplikacje biznesowe, od średnio do długoterminowe, pisane przez więcej niż 1 osobę, itdz share’owaniem lock’a już się nie kwalifikuje bo…<
Niemniej, gdybym w trakcie code review znalazł coś takiego na 101% kazałbym to zmienić, przynajmniej po to, żeby nie raziło po oczach i nie dawało złego przykładu innym (aż dziw bierze co dev potrafią kopiować z cudzego kodu :).
Summary: o co mi chodzi… konstrukcja jest zła, ale powinno się też pisać w jakich warunkach. To tak jakby napisać po prostu "goto jest złe".
Z tym „jednym słusznym rozwiązanie jest” masz rację.
Odpowiedzią programisty powinno być “to zależy” :).
Do zakleszczenia dochodzi tylko gdy metoda BackgroundMethod nigdy się nie zakończy. Więc zasadniczo a taki kod jeżeli nawet sam jest poprawny to nie powinien być zamykany w locku. Powodem dla którego konstrukcja lock(this) jest jest niepoprawna jest fakt że w powyższym przykładzie CallStack wskaże na naszą metodę a nie na metodę która tak naprawdę jest przyczyną błędu. I my jako twórcy metody będziemy musieli przez jakiś czas się nią zajmować a nie osoba która napisała metodę BackgroundMethod.
Przykład jest bardzo uproszczony. Nie udało mi się znaleźć innego prostego rozwiązania ilustrującego ten przykład.
Natomiast słusznie zauważyłeś, że powoduje to duży problem, jeśli dojdzie gdzieś do zakleszczenia i będzie trzeba znaleźć błąd. Dodatkowo trudność będzie jeszcze większa jeśli korzystamy z obcych dll.
Tytuł posta powinien brzmieć -- lock(this) -- przeciw. Nie ma żadnych za.
Paweł