2009-02-02 20 views
42

To jest pytanie szczegółowe dla C#.C# bezpieczeństwo wątków z get/set

Załóżmy Mam klasy z obiektu, a obiekt jest chroniony przez śluzy:

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     return property; 
    } 
    set { 
     property = value; 
    } 
} 

Chcę wątek odpytywania, aby móc kwerendy tę właściwość. Chcę również, aby wątek aktualizował właściwości tego obiektu sporadycznie, a czasami użytkownik może zaktualizować tę właściwość, a użytkownik chce mieć możliwość zobaczenia tej właściwości.

Czy poniższy kod poprawnie zablokuje dane?

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     lock (mLock){ 
      return property; 
     } 
    } 
    set { 
     lock (mLock){ 
       property = value; 
     } 
    } 
} 

Przez „właściwie”, co mam na myśli to, że jeśli chcę zadzwonić

MyProperty.Field1 = 2; 

czy coś, będzie pole zostać zablokowany podczas zrobić aktualizację? Czy ustawienie jest wykonywane przez operatora equals w zakresie funkcji get, czy też funkcja "get" (a więc blokada) zostanie zakończona jako pierwsza, a następnie ustawienie, a następnie "set" zostanie wywołane, a więc obejście zamek?

Edycja: Ponieważ to najwyraźniej nie wystarczy, co będzie? Czy muszę zrobić coś takiego:

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     MyObject tmp = null; 
     lock (mLock){ 
      tmp = property.Clone(); 
     } 
     return tmp; 
    } 
    set { 
     lock (mLock){ 
       property = value; 
     } 
    } 
} 

które bardziej lub mniej tylko upewnia się, że mam dostęp do kopii tylko, co oznacza, że ​​jeśli miałbym mieć dwa wątki nazwać „get” w tym samym czasie, każdy z nich zaczynałby od tej samej wartości pola 1 (prawda?). Czy istnieje sposób, aby wykonać blokowanie odczytu i zapisu dla właściwości, która ma sens? Czy powinienem po prostu ograniczyć się do blokowania sekcji funkcji, a nie samych danych?

Tak, aby ten przykład miał sens: MyObject to sterownik urządzenia, który zwraca stan asynchronicznie. Wysyłam komendy za pośrednictwem portu szeregowego, a następnie urządzenie odpowiada na te polecenia we własnym słodkim czasie. W tej chwili mam wątek, który wypytuje go o jego status ("Czy nadal tam jesteś? Czy potrafisz przyjmować polecenia?"), Wątek oczekujący odpowiedzi na porcie szeregowym ("Otrzymałem status 2, wszystko jest w porządku"), a następnie wątek interfejsu użytkownika, który przyjmuje inne polecenia ("Użytkownik chce, abyś to zrobił.") i publikuje odpowiedzi ze sterownika ("Właśnie to zrobiłem, teraz zaktualizuj ten interfejs"). Właśnie dlatego chcę zablokować obiekt, a nie pola obiektu; to byłaby ogromna liczba zamków, a, i b, nie każde urządzenie tej klasy ma takie samo zachowanie, tylko ogólne zachowanie, więc musiałbym zakodować wiele pojedynczych okien dialogowych, jeśli zindywidualizowałem zamki.

Odpowiedz

34

Nie, Twój kod nie zablokuje dostępu do członków obiektu zwróconych z MyProperty. Blokuje się tylko MyProperty.

Twój przykład użycie jest naprawdę dwie operacje w jednej osobie, z grubsza odpowiada to:

// object is locked and then immediately released in the MyProperty getter 
MyObject o = MyProperty; 

// this assignment isn't covered by a lock 
o.Field1 = 2; 

// the MyProperty setter is never even called in this example 

W skrócie - jeśli dwa wątki dostęp MyProperty jednocześnie getter krótko zablokować drugą nitkę dopóki nie zwraca obiekt do pierwszego wątku, , ale następnie zwróci obiekt również do drugiego wątku. Oba wątki będą wtedy miały pełny, odblokowany dostęp do obiektu.

EDIT w odpowiedzi na dalsze szczegóły w pytaniu

nadal nie jestem w 100% pewien, co starasz się osiągnąć, ale jeśli chcesz tylko atomowy dostęp do obiektu, a następnie czy nie mógłbyś zablokować kodu wywołującego przeciwko samemu obiektowi?

// quick and dirty example 
// there's almost certainly a better/cleaner way to do this 
lock (MyProperty) 
{ 
    // other threads can't lock the object while you're in here 
    MyProperty.Field1 = 2; 
    // do more stuff if you like, the object is all yours 
} 
// now the object is up-for-grabs again 

Nie idealne, ale tak długo, jak wszystkie Dostęp do obiektu jest zawarte w lock (MyProperty) sekcjach wtedy to podejście będzie bezpieczny wątku.

+0

Uzgodnione. Dostarczyłem przykładowe rozwiązanie dla tego, o co pytasz w odpowiedzi na pytanie Singletona http://stackoverflow.com/questions/7095/is-the-c-static-constructor-thread-safe/7105#7105 – Zooba

+0

tak, ja " Prawdopodobnie pójdę z blokowaniem obiektu, tak jak to opisałeś. Dzięki. – mmr

+0

Dobrze, gdyby jednak miał 'MyProperty' nazwany' p' i wywołał dwa wątki jednocześnie: 'p = new MyObject (12)' i 'p = new MyObject (5)' następnie * ten * dostęp byłby zsynchronizowany . Jednak członek 'Field1' nigdy * nie zostanie zablokowany. Jestem prawie pewien, że właśnie to mówisz, próbując zrozumieć to dla siebie. – Snoopy

1

W zamieszczonym przez Ciebie kodzie przykład nigdy nie jest wstępnie uformowany.

W bardziej skomplikowany przykład:

MyProperty.Field1 = MyProperty.doSomething() + 2; 

I oczywiście zakładając, że zrobił:

lock (mLock) 
{ 
    // stuff... 
} 

W doSomething() następnie wszystkich połączeń blokady byłoby nie być wystarczająca, aby zapewnić synchronizację ponad cały obiekt. Gdy tylko funkcja doSomething() powróci, blokada zostanie utracona, a następnie proces dodawania zostanie zakończony, a następnie nastąpi przypisanie, które zostanie ponownie zablokowane.

Albo napisać to w inny sposób można udawać jak zamki nie są wykonywane amutomatically i przepisać to bardziej jak „kod maszynowy” z jednej operacji na linię, a to staje się oczywiste:

lock (mLock) 
{ 
    val = doSomething() 
} 
val = val + 2 
lock (mLock) 
{ 
    MyProperty.Field1 = val 
} 
+0

hunh. Może nieporozumienia właściwości, ale debugger z pewnością wygląda jak wchodzi do funkcji get, zwłaszcza, gdy umieścić punkt przerwania, tylko proste zadanie. – mmr

+0

Przyznam, że nie jestem w 100% pewna, że ​​to nie wywoła efektu, ale nie widzę powodu, dla którego by to zrobił. Jeśli wywoła ono get, to samo, co powiedziałem, dotyczy, po prostu zamień doSomething() z twoją funkcją get. – SoapBox

+0

To wywołuje get, za każdym razem. W przeciwnym razie skąd wziąłby się odnośnik? –

1

piękno wielowątkowości polega na tym, że nie wiesz, w jakim porządku rzeczy się wydarzą. Jeśli ustawisz coś w jednym wątku, może się zdarzyć, że stanie się to po zdobyciu.

Kod, który opublikowałeś, blokuje członka podczas jego odczytu i zapisu. Jeśli chcesz obsłużyć przypadek, w którym wartość jest zaktualizowana, być może powinieneś zajrzeć do innych form synchronizacji, takich jak events. (Sprawdź wersje automatyczne/ręczne). Następnie możesz powiedzieć swojemu wątkowi "pollingowi", że wartość się zmieniła i jest gotowa do ponownego przeczytania.

2

Zasięg blokady w twoim przykładzie jest w niewłaściwym miejscu - musi znajdować się w zakresie właściwości klasy "MyObject", a nie w jej kontenerze.

Jeśli klasa MyObject mój obiekt jest po prostu używana do przechowywania danych, do których jeden wątek chce pisać, a innego (wątek UI) do odczytu z tego miejsca, to może nie być konieczne ustawienie setera i utworzenie go raz.

Należy również rozważyć, czy umieszczenie blokad na poziomie właściwości jest poziomem zapisu ziarnistości blokady; jeśli można zapisać więcej niż jedną właściwość w celu przedstawienia stanu transakcji (np. całkowita suma zamówień i łączna waga), lepiej byłoby mieć blokadę na poziomie MyObject (tj. blokada (myObject.SyncRoot). .)

0

W edytowanej wersji nadal nie zapewniasz sposobu aktualizacji MyObject w sposób bezpieczny dla wątków. Wszelkie zmiany we właściwościach obiektu będą musiały zostać wykonane wewnątrz bloku zsynchronizowanego/zablokowanego.

Możesz pisać poszczególne setery, żeby sobie z tym poradzić, ale zaznaczyłeś, że będzie to trudne ze względu na duże pola liczbowe.Jeśli rzeczywiście tak jest (i nie dostarczyłeś jeszcze wystarczających informacji, aby to ocenić), jedną z alternatyw jest napisanie setera, który używa refleksji; to pozwoliłoby ci przekazać ciąg znaków reprezentujący nazwę pola i mógłbyś dynamicznie wyszukać nazwę pola i zaktualizować wartość. To pozwoliłoby ci mieć jednego setera, który działałby na dowolnej liczbie pól. Nie jest to tak łatwe ani efektywne, ale pozwoliłoby ci radzić sobie z dużą liczbą klas i pól.

12

Jednoczesne programowanie byłoby całkiem proste, gdyby Twoje podejście mogło działać. Ale tak nie jest, że góra lodowa że Titanic tonie jest, na przykład, klient swojej klasie ten sposób:

objectRef.MyProperty += 1; 

read-modify-write wyścig jest dość oczywiste, nie są gorsze. Nie ma absolutnie nic, co mógłbyś zrobić, aby Twoja własność była bezpieczna dla wątków, poza tym, że stała się niezmienna. To twój klient musi poradzić sobie z bólem głowy. Bycie zmuszonym do delegowania tego rodzaju odpowiedzialności na programistę, który ma najmniejsze szanse, aby to naprawić, jest piętą achillesową programowania współbieżnego.

+0

Czy nie byłoby wspaniale, gdyby to było takie proste? Jestem pewien, że istnieją powody, dla których tak nie jest, ale po prostu nie znam szczegółów, aby wiedzieć, dlaczego kod nie działa w ten sposób. – mmr

+0

Ważne jest, aby to zrozumieć, nie próbuj jednocześnie programowania, jeśli tego nie robisz. Google "aktualizacje atomowe". –

+0

Rozumiem atomowość, ale jestem niejasny, gdzie rzeczy są atomowe w C#. Tylko podstawowe zadania, prawda? Określenie atomowości z czymś w rodzaju Interlocked może być przydatne ... – mmr

4

Jak zauważyli inni, po zwróceniu obiektu z gettera tracisz kontrolę nad tym, kto ma dostęp do obiektu i kiedy. Aby zrobić to, co chcesz zrobić, musisz umieścić blokadę wewnątrz samego obiektu.

Być może nie rozumiem pełnego obrazu, ale na podstawie Twojego opisu nie brzmi to tak, jakbyś musiał mieć blokadę dla każdego pola. Jeśli masz zestaw pól, które są po prostu odczytywane i zapisywane przez moduły pobierające i ustawiające, prawdopodobnie możesz uciec z pojedynczą blokadą dla tych pól. Istnieje oczywiście potencjał, że niepotrzebnie serializuje się operowanie wątkami w ten sposób. Ale znowu, w oparciu o Twój opis, nie brzmi to tak, jak agresywnie korzystasz z obiektu.

Proponuję również użycie zdarzenia zamiast używania wątku do odpytywania stanu urządzenia. Dzięki mechanizmowi odpytywania będziesz uderzał w blokadę za każdym razem, gdy wątek zapyta urządzenie. Dzięki mechanizmowi zdarzeń, po zmianie statusu, obiekt będzie powiadamiał dowolnych słuchaczy. W tym momencie wątek "odpytywania" (który nie byłby już odpytywaniem) obudziłby się i uzyskałby nowy status. Będzie to znacznie bardziej wydajne.

Jako przykład ...

public class Status 
{ 
    private int _code; 
    private DateTime _lastUpdate; 
    private object _sync = new object(); // single lock for both fields 

    public int Code 
    { 
     get { lock (_sync) { return _code; } } 
     set 
     { 
      lock (_sync) { 
       _code = value; 
      } 

      // Notify listeners 
      EventHandler handler = Changed; 
      if (handler != null) { 
       handler(this, null); 
      } 
     } 
    } 

    public DateTime LastUpdate 
    { 
     get { lock (_sync) { return _lastUpdate; } } 
     set { lock (_sync) { _lastUpdate = value; } } 
    } 

    public event EventHandler Changed; 
} 

Twój 'odpytywania' gwint będzie wyglądać następująco.

Status status = new Status(); 
ManualResetEvent changedEvent = new ManualResetEvent(false); 
Thread thread = new Thread(
    delegate() { 
     status.Changed += delegate { changedEvent.Set(); }; 
     while (true) { 
      changedEvent.WaitOne(Timeout.Infinite); 
      int code = status.Code; 
      DateTime lastUpdate = status.LastUpdate; 
      changedEvent.Reset(); 
     } 
    } 
); 
thread.Start(); 
+0

Interesujące. Muszę to rzucić. Moim pierwszym wrażeniem jest to, że nie zadziała, ponieważ urządzenie, z którego korzystam, nie wyświetli komunikatu o statusie, chyba że zostanie poproszony. Więc jeśli jest w trybie "prep", nie ma wskazań, chyba że zapytasz. – mmr

+0

Tak, jeśli musisz prod urządzenia, aby zgłosić stan, a następnie odpytywanie jest prawdopodobnie jedyną rzeczą, którą możesz zrobić. Przez przypadek, czy istnieje jakiś mechanizm wywołania zwrotnego, który powoduje, że urządzenie okresowo raportuje status? Nienawidzę głosowania, ponieważ jest to nieefektywne, ale czasami nie masz wyboru. –

+0

Niestety, jest to prawdziwe urządzenie szeregowe - odpowiada tylko na odpytywanie. To nie jest USB, to jest 9-pinowe. Stara szkoła w stylu retro, tak myślę. – mmr

-1

Czy w C# zamki nie cierpią z powodu tych samych problemów blokujących jak innych językach wtedy?

E.G.

var someObj = -1; 

// Thread 1 

if (someObj = -1) 
    lock(someObj) 
     someObj = 42; 

// Thread 2 

if (someObj = -1) 
    lock(someObj) 
     someObj = 24; 

Może to spowodować problem z uzyskaniem przez oba wątki blokady i zmianą wartości. Może to prowadzić do dziwnych błędów. Jednak nie chcesz niepotrzebnie blokować obiektu, chyba że musisz. W takim przypadku należy wziąć pod uwagę podwójne sprawdzanie blokady.

// Threads 1 & 2 

if (someObj = -1) 
    lock(someObj) 
     if(someObj = -1) 
      someObj = {newValue}; 

Po prostu o czym należy pamiętać.

Powiązane problemy