Czy instrukcja powrotu powinna znajdować się wewnątrz czy na zewnątrz zamka?

142

Właśnie zdałem sobie sprawę, że w jakimś miejscu w moim kodzie instrukcja return znajduje się wewnątrz zamka, a czasem na zewnątrz. Która jest najlepsza?

1)

void example()
{
    lock (mutex)
    {
    //...
    }
    return myData;
}

2)

void example()
{
    lock (mutex)
    {
    //...
    return myData;
    }

}

Którego powinienem użyć?

Patrick Desjardins
źródło
Co powiesz na odpalenie Reflectora i porównanie IL ;-).
Pop Catalin
6
@Pop: gotowe - żadne z nich nie jest lepsze pod względem języka IL - obowiązuje tylko styl C #
Marc Gravell
1
Bardzo interesujące, wow, dzisiaj się czegoś nauczyłem!
Pokus

Odpowiedzi:

192

Zasadniczo, co kiedykolwiek czyni kod prostszym. Pojedynczy punkt wyjścia jest fajnym ideałem, ale nie wyginałbym kodu z kształtu tylko po to, aby to osiągnąć ... A jeśli alternatywą jest zadeklarowanie zmiennej lokalnej (poza zamkiem), zainicjowanie jej (wewnątrz zamka) i potem zwracając go (poza zamek), wtedy powiedziałbym, że proste „return foo” wewnątrz zamka jest o wiele prostsze.

Aby pokazać różnicę w IL, zakodujmy:

static class Program
{
    static void Main() { }

    static readonly object sync = new object();

    static int GetValue() { return 5; }

    static int ReturnInside()
    {
        lock (sync)
        {
            return GetValue();
        }
    }

    static int ReturnOutside()
    {
        int val;
        lock (sync)
        {
            val = GetValue();
        }
        return val;
    }
}

(zauważ, że z przyjemnością twierdzę, że ReturnInsidejest to prostszy / czystszy kawałek C #)

Spójrz na IL (tryb zwolnienia itp.):

.method private hidebysig static int32 ReturnInside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000,
        [1] object CS$2$0001)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
} 

method private hidebysig static int32 ReturnOutside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 val,
        [1] object CS$2$0000)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
}

Więc na poziomie IL są one [podaj lub przyjmij kilka nazw] identyczne (nauczyłem się czegoś ;-p). W związku z tym jedynym sensownym porównaniem jest (wysoce subiektywne) prawo lokalnego stylu kodowania ... Wolę ReturnInsideprostotę, ale też nie byłbym podekscytowany.

Marc Gravell
źródło
15
Użyłem (darmowego i doskonałego) reflektora .NET firmy Red Gate (był: Reflektor .NET Lutza Roedera), ale ILDASM też się nada.
Marc Gravell
1
Jednym z najpotężniejszych aspektów Reflectora jest to, że możesz faktycznie zdemontować IL na preferowany język (C #, VB, Delphi, MC ++, Chrome itp.)
Marc Gravell
3
Dla twojego prostego przykładu IL pozostaje taka sama, ale to prawdopodobnie dlatego, że zwracasz tylko stałą wartość ?! Uważam, że w rzeczywistych scenariuszach wyniki mogą się różnić, a równoległe wątki mogą powodować problemy dla siebie nawzajem, modyfikując wartość przed jej zwróceniem, jeśli instrukcja return znajduje się poza blokiem blokady. Niebezpieczny!
Torbjørn
@MarcGravell: Właśnie natknąłem się na twój post, rozważając to samo i nawet po przeczytaniu twojej odpowiedzi nadal nie jestem pewien, co następuje: Czy są JAKIEKOLWIEK okoliczności, w których użycie podejścia zewnętrznego mogłoby złamać logikę bezpieczeństwa wątków. Pytam o to, ponieważ wolę pojedynczy punkt zwrotu i nie czuję się dobrze, jeśli chodzi o bezpieczeństwo nici. Chociaż, jeśli IL jest taki sam, moje obawy i tak powinny być dyskusyjne.
Raheel Khan
1
@RaheelKhan no, none; oni są tacy sami. Na poziomie IL nie możesz ret wejść do .tryregionu.
Marc Gravell
42

Nie robi to żadnej różnicy; oba są tłumaczone na to samo przez kompilator.

Aby wyjaśnić, jedno z nich jest skutecznie tłumaczone na coś z następującą semantyką:

T myData;
Monitor.Enter(mutex)
try
{
    myData= // something
}
finally
{
    Monitor.Exit(mutex);
}

return myData;
Greg Beech
źródło
1
Cóż, tak jest w przypadku próby / w końcu - jednak powrót poza blokadę nadal wymaga dodatkowych miejscowych, których nie można zoptymalizować - i wymaga więcej kodu ...
Marc Gravell
3
Nie możesz wrócić z bloku try; musi kończyć się kodem operacyjnym „.leave”. Tak więc wyemitowany CIL powinien być taki sam w obu przypadkach.
Greg Beech
3
Masz rację - właśnie spojrzałem na IL (zobacz zaktualizowany post). Nauczyłem się czegoś ;-p
Marc Gravell
2
Fajnie, niestety nauczyłem się z bolesnych godzin, próbując emitować kody operacyjne .ret w blokach try i gdy CLR odmawiał ładowania moich metod dynamicznych :-(
Greg Beech
Mogę się odnieść; Zrobiłem sporo Refleksji. Emituj, ale jestem leniwy; chyba że jestem czegoś bardzo pewien, piszę reprezentatywny kod w C #, a potem patrzę na IL. Ale jest zaskakujące, jak szybko zaczynasz myśleć w kategoriach IL (tj. Sekwencjonować stos).
Marc Gravell
28

Zdecydowanie umieściłbym powrót w zamku. W przeciwnym razie ryzykujesz, że inny wątek wejdzie do blokady i zmodyfikuje zmienną przed instrukcją return, co spowoduje, że oryginalny obiekt wywołujący otrzyma inną wartość niż oczekiwano.

Ricardo Villamil
źródło
4
To prawda, wydaje się, że brakuje tego punktu. Proste próbki, które wykonali, mogą dawać ten sam IL, ale tak nie jest w przypadku większości rzeczywistych scenariuszy.
Torbjørn
4
Jestem zaskoczony, że inne odpowiedzi nie mówią o tym
Akshat Agarwal
5
W tym przykładzie mówią o użyciu zmiennej stosu do przechowywania wartości zwracanej, czyli tylko instrukcji return poza blokadą i oczywiście deklaracji zmiennej. Kolejny wątek powinien mieć inny stos i nie mógł wyrządzić żadnej szkody, mam rację?
Guillermo Ruffino
3
Nie sądzę, aby to był prawidłowy punkt, ponieważ inny wątek mógłby zaktualizować wartość między wywołaniem zwrotnym a rzeczywistym przypisaniem wartości zwracanej do zmiennej w głównym wątku. Zwracanej wartości nie można zmienić ani zagwarantować spójności z bieżącą wartością rzeczywistą w żaden sposób. Dobrze?
Uroš Joksimović
Ta odpowiedź jest nieprawidłowa. Inny wątek nie może zmienić zmiennej lokalnej. Zmienne lokalne są przechowywane na stosie, a każdy wątek ma swój własny stos. Przy okazji domyślny rozmiar stosu wątku to 1 MB .
Theodor Zoulias
5

To zależy,

Mam zamiar iść pod prąd. Generalnie wracałbym do wnętrza zamka.

Zwykle zmienna mydata jest zmienną lokalną. Lubię deklarować zmienne lokalne podczas ich inicjalizacji. Rzadko mam dane do zainicjowania mojej wartości zwracanej poza blokadą.

Więc twoje porównanie jest błędne. Chociaż idealnie byłoby, gdyby różnica między tymi dwiema opcjami była taka, jak napisałeś, co wydaje się być ukłonem w stronę przypadku 1, w praktyce jest trochę brzydsza.

void example() { 
    int myData;
    lock (foo) { 
        myData = ...;
    }
    return myData
}

vs.

void example() { 
    lock (foo) {
        return ...;
    }
}

Uważam, że przypadek 2 jest znacznie łatwiejszy do odczytania i trudniejszy do zepsucia, szczególnie w przypadku krótkich fragmentów.

Edward KMETT
źródło
4

Jeśli uważasz, że zamek na zewnątrz wygląda lepiej, ale bądź ostrożny, jeśli w końcu zmienisz kod na:

return f(...)

Jeśli f () musi zostać wywołane z trzymaną blokadą, to oczywiście musi znajdować się wewnątrz zamka, ponieważ takie utrzymywanie zwrotów wewnątrz zamka ma sens.

Rob Walker
źródło
1

Na co warto zwrócić uwagę, dokumentacja na MSDN zawiera przykład powrotu z wnętrza zamka. Z innych odpowiedzi tutaj, wydaje się, że jest dość podobny do IL, ale wydaje mi się, że bezpieczniej jest wrócić z wnętrza blokady, ponieważ wtedy nie ryzykujesz, że zmienna zwracana zostanie nadpisana przez inny wątek.

greyseal96
źródło
0

Aby ułatwić innym programistom czytanie kodu, sugerowałbym pierwszą alternatywę.

Adam Asham
źródło
0

lock() return <expression> oświadczenia zawsze:

1) wprowadź zamek

2) tworzy lokalny (bezpieczny wątkowo) magazyn wartości określonego typu,

3) wypełnia sklep wartością zwracaną przez <expression>,

4) zamek wyjściowy

5) zwrócić sklep.

Oznacza to, że wartość, zwrócona z instrukcji lock, jest zawsze „gotowana” przed powrotem.

Nie martw się lock() return, nie słuchaj tutaj nikogo))

mshakurov
źródło
-2

Uwaga: uważam, że ta odpowiedź jest zgodna z faktami i mam nadzieję, że jest również pomocna, ale zawsze chętnie ją poprawiam na podstawie konkretnych opinii.

Podsumowując i uzupełniając istniejące odpowiedzi:

  • W Zaakceptowanych odpowiedź pokazuje, że, którego składnia forma niezależnie wybierzesz w C # kodu w kodzie IL - a więc w czasie wykonywania - w returnnie zdarza się aż po zwolnieniu blokady.

    • Nawet mimo umieszczenia return wewnątrzlock blok zatem ściśle mówiąc, wprowadza w błąd regulacji przepływu [1] , jest składniowo dogodne tym, że eliminuje potrzebę przechowywania wartość powrotną w AUX. zmienna lokalna (zadeklarowana poza blokiem, aby można jej było używać z returnpoza blokiem) - patrz odpowiedź Edwarda KMETT-a .
  • Osobno - i ten aspekt jest przypadkowy w stosunku do pytania, ale wciąż może być interesujący (odpowiedź Ricardo Villamila próbuje go rozwiązać, ale myślę, że błędnie) - łącząc lockstwierdzenie ze returnstwierdzeniem - tj. Uzyskanie wartości returnw bloku chronionym przed współbieżny dostęp - tylko sensownie „chroni” zwróconą wartość w zakresie wywołującego , jeśli faktycznie nie wymaga ochrony po uzyskaniu , co ma zastosowanie w następujących scenariuszach:

    • Jeśli zwracana wartość jest elementem z kolekcji, który wymaga tylko ochrony w zakresie dodawania i usuwania elementów, a nie modyfikacji samych elementów i / lub ...

    • ... jeśli zwracana wartość jest instancją typu wartości lub łańcuchem .

      • Należy zauważyć, że w tym przypadku wywołujący otrzymuje migawkę (kopię) [2] wartości - która do czasu, gdy wywołujący sprawdzi, może już nie być bieżącą wartością w strukturze danych pochodzenia.
    • W każdym innym przypadku blokowanie musi zostać wykonane przez obiekt wywołujący , a nie (tylko) wewnątrz metody.


[1] Theodor Zoulias wskazuje, że jest to technicznie również do umieszczania returnwewnątrz try, catch, using, if, while, for, ... wypowiedzi; jednakże jest to szczególny cel lockoświadczenia, który prawdopodobnie zachęci do zbadania prawdziwego przepływu kontroli, czego dowodem jest to, że zadano to pytanie i otrzymano wiele uwagi.

[2] Dostęp do instancji typu wartościowego niezmiennie tworzy kopię lokalną wątku na stosie; mimo że ciągi są technicznie instancjami typu referencyjnego, skutecznie zachowują się jak wystąpienia typu wartości.

mklement0
źródło
Jeśli chodzi o aktualny stan Twojej odpowiedzi (wersja 13), nadal spekulujesz na temat przyczyn istnienia lockinstrukcji zwrotu i wyprowadzasz jej znaczenie z umieszczenia instrukcji zwrotu. Która jest dyskusją niezwiązaną z tym pytaniem IMHO. Uważam również, że użycie „przekłamań” jest dość niepokojące. Jeśli powrocie z lockwprowadza w błąd regulacji przepływu, to samo można powiedzieć o powrocie z try, catch, using, if, while, for, i inne konstrukt języka. To tak, jakby powiedzieć, że język C # jest pełen błędnych reprezentacji przepływu sterowania. Jezu ...
Theodor Zoulias
„To tak, jakby powiedzieć, że C # jest pełen fałszywych interpretacji przepływu sterowania” - cóż, to jest technicznie prawda, a termin „fałszywe przedstawienie” jest tylko oceną wartości, jeśli zdecydujesz się wziąć to w ten sposób. Z try, if... Ja osobiście nie wydają się nawet myśleć o tym, ale w kontekście lock, w szczególności, pojawiło się pytanie dla mnie - a jeśli nie wynikają dla innych też to pytanie nigdy nie zostały poproszone , a zaakceptowana odpowiedź nie posunęłaby się zbytnio, aby zbadać prawdziwe zachowanie.
mklement0