Ostatnio przeglądając kod udało mi się znaleźć następujący fragment:
lock (this) { // Do something }
Do zsynchronizowania dostępu do współdzielonego zasobu używany jest obiekt, w którym występuje ten fragment kodu. Teoretycznie fragment ten jest poprawny. Co więcej w programie nie wystąpił żaden problem z zakleszczeniem.
Przeglądając literaturę możemy natrafić na następujące przykłady:
1. W O’Reilly Programming C# można spotkać następujący przykład użycia słowa kluczowego 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;">Oprócz powyższego przykładu w książce nie ma dyskusji nad jego wadami i zaletami.</p> <p style="text-align: justify;">2. Andrew Troelsen w Pro C# 2008 and the .NET 3.5 Platform zaleca użycie <em>lock (this)</em> w przypadku metod prywatnych:</p> [code lang="csharp"]private void SomePrivateMethod() { lock (this) { // do something } }
W obu przypadkach przemilczane jest zagrożenie, które niesie z sobą wykorzystanielock (this). Przeanalizujmy poniższy kod:
public class BadLock { public void Compute() { Console.WriteLine("Compute - Start"); lock (this) { Console.WriteLine("Compute - In Lock"); Thread.Sleep(5000); } Console.WriteLine("Compute - Stop"); } }
Klasa BadLock posiada w sobie metodę Compute, które symuluje proces obliczania oraz używa do synchronizacji lock (this). W większości przypadków wykorzystanie tego kodu nie powinno spowodować problemów. Istnieje jednak scenariusz, w którym może dojść do zakleszczenia. W sytuacji, gdy stworzymy obiekt typu BadLock i użyjemy jego do zapewnienia synchronizacji, a następnie wykonamy jakąś metodę z klasy BadLock może dojść do zakleszczenia. Przykład takiego scenariusza przedstawia poniższy kod:
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); } }
Po uruchomieniu aplikacji otrzymamy następujący wynik:
Bad Lock test 1 BackgroundMethod - Lock Compute – Start
Nie ważne jak długo będzie się czekać, dalsze komunikaty nie zostaną wyświetlone.
Rozwiązaniem tego problemu jest zastąpienie konstrukcji:
lock (this) { ... }
następującą konstrukcją:
private object synchronizationObject = new Object(); ... lock (synchronizationObject) { ... }
Dopiero po zastosowaniu tej konstrukcji mamy pewność, że nikt nie popsuje stanu obiektu i nie doprowadzi do zakleszczenia. Po wprowadzeniu zmian obiekt BadLock będzie wyglądał następująco:
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"); } }
i otrzymamy poprawne wyniki:
Good Lock test 1 BackgroundMethod - Lock Compute - Start Compute - In Lock Compute – Stop
Na koniec pozostaje jeszcze temat związany z metodą BackgroundMethod. Nie jestem zwolennikiem programowania defensywnego, ale w tym przypadku zamiast ustawiać lock na obiekcie, z którego korzystamy proponowałbym również stworzyć niezależny obiekt służący do synchronizacji.
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ł