Czy zablokowany obiekt pozostaje zablokowany, jeśli wystąpi w nim wyjątek?

85

W aplikacji do obsługi wątków ac #, jeśli miałbym zablokować obiekt, powiedzmy, że jest to kolejka, a jeśli wystąpi wyjątek, czy obiekt pozostanie zablokowany? Oto pseudokod:

int ii;
lock(MyQueue)
{
   MyClass LclClass = (MyClass)MyQueue.Dequeue();
   try
   {
      ii = int.parse(LclClass.SomeString);
   }
   catch
   {
     MessageBox.Show("Error parsing string");
   }
}

Jak rozumiem, kod po haczyku nie jest wykonywany - ale zastanawiałem się, czy blokada zostanie zwolniona.

Khadaji
źródło
1
Na koniec (zobacz aktualizacje) - prawdopodobnie powinieneś przytrzymać blokadę tylko na czas usunięcia z kolejki ... wykonać przetwarzanie poza blokadą.
Marc Gravell
1
Kod po przechwyceniu jest wykonywany, ponieważ wyjątek jest obsługiwany
cjk
Dzięki, musiałem to przegapić, czy powinienem usunąć to pytanie?
Vort3x
4
Wygląda na to, że przykładowy kod nie nadaje się do tego pytania, ale pytanie jest całkiem poprawne.
SalvadorGomez,
Projektant C # - Lock & Exception
Jivan

Odpowiedzi:

88

Pierwszy; czy rozważałeś TryParse?

in li;
if(int.TryParse(LclClass.SomeString, out li)) {
    // li is now assigned
} else {
    // input string is dodgy
}

Zamek zostanie zwolniony z 2 powodów; po pierwsze, lockto zasadniczo:

Monitor.Enter(lockObj);
try {
  // ...
} finally {
    Monitor.Exit(lockObj);
}

Druga; łapiesz i nie rzucasz ponownie wewnętrznego wyjątku, więc tak locknaprawdę nigdy nie widzi wyjątku. Oczywiście blokujesz się na czas trwania MessageBox, co może być problemem.

Tak więc zostanie uwolniony we wszystkich, oprócz najbardziej fatalnych, katastrofalnych, niemożliwych do odzyskania wyjątków.

Marc Gravell
źródło
14
Zdaję sobie sprawę z tryparse, ale nie ma to związku z moim pytaniem. To był prosty kod wyjaśniający pytanie - nie był to prawdziwy problem dotyczący analizy. Zastąp parsowanie dowolnym kodem, który wymusi złapanie i zapewni Ci wygodę.
Khadaji
13
Co powiesz na zgłoszenie nowego wyjątku („w celach ilustracyjnych”); ;-p
Marc Gravell
1
Z wyjątkiem przypadku, gdy TheadAbortExceptionwystąpi między Monitor.Entera try: blogs.msdn.com/ericlippert/archive/2009/03/06/ ...
Igal Tabachnik
2
„fatalne, katastrofalne i nieodwracalne wyjątki”, takie jak przekraczanie strumieni.
Phil Cooper,
98

Zwracam uwagę, że nikt nie wspomniał w swoich odpowiedziach na to stare pytanie, że zwolnienie blokady w przypadku wyjątku jest niezwykle niebezpieczną rzeczą. Tak, instrukcje blokujące w języku C # mają semantykę „w końcu”; kiedy sterowanie wychodzi z blokady normalnie lub nienormalnie, blokada zostaje zwolniona. Wszyscy mówicie o tym, jakby to było dobre, ale to jest złe! Właściwą rzeczą do zrobienia, jeśli masz zablokowany region, który zgłasza nieobsługiwany wyjątek, jest przerwanie chorego procesu bezpośrednio przed zniszczeniem większej ilości danych użytkownika , a nie zwolnienie blokady i kontynuowanie .

Spójrz na to w ten sposób: przypuśćmy, że masz łazienkę z zamkiem w drzwiach i kolejką ludzi czekających na zewnątrz. Bomba w łazience wybucha, zabijając osobę w środku. Twoje pytanie brzmi: „czy w takiej sytuacji zamek zostanie automatycznie odblokowany, aby następna osoba mogła wejść do łazienki?” Tak, to będzie. To nie jest dobra rzecz. Właśnie tam wybuchła bomba i kogoś zabiła! Prawdopodobnie kanalizacja jest zniszczona, dom nie jest już w dobrym stanie konstrukcyjnym i może być tam kolejna bomba . Należy jak najszybciej wyprowadzić wszystkich i zburzyć cały dom.

To znaczy, przemyśl to: jeśli zablokowałeś region kodu, aby odczytać ze struktury danych bez mutacji w innym wątku, a coś w tej strukturze danych rzuciło wyjątek, szanse są dobre, ponieważ struktura danych jest uszkodzony . Dane użytkownika są teraz pomieszane; nie chcesz w tym momencie próbować zapisywać danych użytkownika, ponieważ zapisujesz uszkodzone dane. Po prostu zakończ proces.

Jeśli zablokowałeś region kodu w celu wykonania mutacji bez kolejnego wątku odczytującego stan w tym samym czasie, a mutacja wyrzuca, to jeśli dane nie były wcześniej uszkodzone, na pewno tak jest teraz . Przed którym dokładnie scenariuszem ma chronić zamek . Teraz kod, który czeka na odczytanie tego stanu, natychmiast uzyska dostęp do uszkodzonego stanu i prawdopodobnie sam się zawiesi. Ponownie, właściwą rzeczą jest zakończenie procesu.

Bez względu na to, jak go pokroisz, wyjątek w zamku to zła wiadomość . Właściwe pytanie nie brzmi: „czy mój zamek zostanie wyczyszczony w przypadku wyjątku?” Właściwe pytanie, które należy zadać, brzmi: „w jaki sposób mogę upewnić się, że wewnątrz blokady nigdy nie ma wyjątku? A jeśli tak, to w jaki sposób ustrukturyzować mój program, aby mutacje zostały przywrócone do poprzedniego dobrego stanu?”

Eric Lippert
źródło
18
Ten problem jest dość ortogonalny do blokowania IMO. Jeśli pojawi się oczekiwany wyjątek, chcesz wyczyścić wszystko, w tym blokady. A jeśli pojawi się nieoczekiwany wyjątek, masz problem z zamkami lub bez.
CodesInChaos
10
Myślę, że opisana powyżej sytuacja to generalizm. Czasami wyjątki opisują katastroficzne wydarzenia. Czasami nie. Każdy używa ich inaczej w kodzie. Jest całkowicie słuszne, że wyjątek jest sygnałem wyjątkowego, ale nie katastroficznego zdarzenia - założenie wyjątków = katastroficzne, przypadek zakończenia procesu jest zbyt specyficzny. Fakt, że może to być katastroficzne wydarzenie, nie umniejsza ważności pytania - ten sam tok myślenia może doprowadzić do tego, że nigdy nie zajmiesz się żadnym wyjątkiem, w którym to przypadku proces zostanie zakończony ...
Gerasimos R
1
@GerasimosR: Rzeczywiście. Dwie kwestie do podkreślenia. Po pierwsze, należy zakładać, że wyjątki są katastrofalne, dopóki nie zostaną określone jako łagodne. Po drugie, jeśli otrzymujesz łagodny wyjątek wyrzucony z zablokowanego regionu, prawdopodobnie zablokowany region jest źle zaprojektowany; prawdopodobnie wykonuje zbyt dużo pracy wewnątrz zamka.
Eric Lippert,
1
Z całym szacunkiem, ale brzmi to tak, jakbyś mówił, panie Lippert, że projekt bloku zamka w C #, który pod maską zawiera ostateczne oświadczenie, jest złą rzeczą. Zamiast tego powinniśmy całkowicie zawiesić każdą aplikację, która kiedykolwiek miała wyjątek w instrukcji blokady, która nie została złapana w blokadzie. Może nie o tym mówisz, ale jeśli tak, to bardzo się cieszę, że zespół poszedł tą samą drogą, a nie Twoją (ekstremistyczną z powodów purystycznych!) Trasą. Z całym szacunkiem.
Nicholas Petersen
2
W przypadku, gdy nie jest jasne, co mam na myśli przez niekomponowalne: Załóżmy, że mamy metodę "transfer", która przyjmuje dwie listy, sid, blokuje s, blokuje d, usuwa element z s, dodaje element do d, odblokowuje d, odblokowuje s. Metoda jest tylko poprawne , jeśli nikt nie próbuje przenieść z listy X do Y listy w tym samym czasie jak ktoś inny spróbuje przesłać od Y do X . Poprawność metody transferu nie pozwala na zbudowanie z niej poprawnego rozwiązania większego problemu, ponieważ zamki są niebezpiecznymi mutacjami stanu globalnego. Aby bezpiecznie „przenieść” musisz wiedzieć o każdej blokadzie w programie.
Eric Lippert
43

tak, to wyda się prawidłowo; lockdziała jak try/ finally, z Monitor.Exit(myLock)w końcu, więc bez względu na to, jak wyjdziesz, zostanie zwolniony. Na marginesie, catch(... e) {throw e;}najlepiej tego unikać, ponieważ uszkadza to ślad stosu e; lepiej go w ogóle nie łapać lub alternatywnie: używać throw;zamiastthrow e; który rzuca ponownie.

Jeśli naprawdę chcesz wiedzieć, blokada w C # 4 / .NET 4 to:

{
    bool haveLock = false;
    try {
       Monitor.Enter(myLock, ref haveLock);
    } finally {
       if(haveLock) Monitor.Exit(myLock);
    }
} 
Marc Gravell
źródło
14

„Instrukcja lock jest kompilowana do wywołania Monitor.Enter, a następnie blokuje try… last. W ostatnim bloku jest wywoływana Monitor.Exit.

Generowanie kodu JIT zarówno dla x86, jak i x64 zapewnia, że ​​nie może wystąpić przerwanie wątku między wywołaniem Monitor.Enter a blokiem try, który następuje bezpośrednio po nim. "

Zaczerpnięte z: tej witryny

GH.
źródło
1
Jest co najmniej jeden przypadek, w którym nie jest to prawdą: przerwanie wątku w trybie debugowania w wersjach .net starszych niż 4. Przyczyną jest to, że kompilator C # wstawia NOP między elementami Monitor.Entera try, tak że warunek „natychmiast następuje” w JIT jest naruszony.
CodesInChaos
6

Twój zamek zostanie prawidłowo zwolniony. A lockdziała tak:

try {
    Monitor.Enter(myLock);
    // ...
} finally {
    Monitor.Exit(myLock);
}

A finallybloki są na pewno wykonywane, niezależnie od tego, jak opuścisz tryblok.

Ry-
źródło
Właściwie żaden kod nie jest „gwarantowany” do wykonania (można na przykład wyciągnąć kabel zasilający) i nie jest to do końca tak, jak wygląda zamek w wersji 4.0 - patrz tutaj
Marc Gravell
1
@MarcGravell: Myślałem o umieszczeniu dwóch przypisów dotyczących dokładnie tych dwóch punktów. A potem doszedłem do wniosku, że to nie będzie miało większego znaczenia :)
Ry-
1
@MarcGravel: Myślę, że wszyscy zakładają, że zawsze nie mówi się o sytuacji „wyciągnij wtyczkę”, ponieważ programista nie ma nad tym żadnej kontroli :)
Vort3x
5

Żeby dodać trochę do doskonałej odpowiedzi Marca.

Takie sytuacje są przyczyną istnienia locksłowa kluczowego. Pomaga programistom upewnić się, że blokada została zwolniona w finallybloku.

Jeśli jesteś zmuszony używać Monitor.Enter/ Exitnp. Do obsługi limitu czasu, musisz upewnić się, że wywołanie jest umieszczone Monitor.Exitw finallybloku, aby zapewnić prawidłowe zwolnienie blokady w przypadku wyjątku.

Brian Rasmussen
źródło