2009-02-02 19 views
42

Questa è una domanda di dettaglio per C#.C# sicurezza filo con get/set

Supponiamo che io ho una classe con un oggetto e l'oggetto è protetto da una serratura:

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

Voglio un thread di polling per essere in grado di interrogare quella proprietà. Desidero inoltre che il thread aggiorni le proprietà di tale oggetto occasionalmente e talvolta l'utente può aggiornare tale proprietà e l'utente desidera poter vedere tale proprietà.

Il seguente codice blocca correttamente i dati?

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

Con 'correttamente', quello che voglio dire è, se voglio chiamare

MyProperty.Field1 = 2; 

o qualsiasi altra cosa, sarà il campo bloccato mentre faccio l'aggiornamento? L'impostazione eseguita dall'operatore di uguale all'interno dell'ambito della funzione 'get', o la funzione 'get' (e quindi il lock), terminano prima, e quindi l'impostazione, e quindi 'set' viene chiamato, bypassando così la serratura?

Modifica: Dal momento che questo a quanto pare non farà il trucco, quale sarà? Ho bisogno di fare qualcosa di simile:

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; 
     } 
    } 
} 

che più o meno fa solo in modo che io ho solo l'accesso a una copia, il che significa che se dovessi avere due thread chiamano un 'get', allo stesso tempo, inizierebbero ciascuno con lo stesso valore di Field1 (giusto?). C'è un modo per leggere e scrivere su una proprietà che ha senso? O dovrei limitarmi a bloccare sezioni di funzioni piuttosto che i dati stessi?

Proprio questo esempio ha senso: MyObject è un driver di periferica che restituisce lo stato in modo asincrono. Gli mando i comandi tramite una porta seriale, e poi il dispositivo risponde a quei comandi nel suo momento dolce. In questo momento, ho un thread che lo interroga per il suo stato ("Sei ancora lì? Puoi accettare i comandi?"), Un thread che aspetta risposte sulla porta seriale ("Just got status string 2, tutto va bene"), quindi il thread dell'interfaccia utente che accetta altri comandi ("Utente desidera che tu faccia questa cosa.") e posta le risposte dal driver ("Ho appena fatto la cosa, ora aggiorno l'interfaccia utente con quello"). Ecco perché voglio bloccare l'oggetto stesso, piuttosto che i campi dell'oggetto; sarebbe un numero enorme di blocchi, a, eb, non tutti i dispositivi di questa classe hanno lo stesso comportamento, solo un comportamento generale, quindi dovrei codificare molte finestre di dialogo individuali se ho individuato i blocchi.

risposta

34

No, il codice non bloccherà l'accesso ai membri dell'oggetto restituito da MyProperty. Blocca solo MyProperty stesso.

L'utilizzo esempio è davvero due operazioni riuniti in un unico, grosso modo equivalente a questo:

// 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 

In poche parole - se due thread accedono MyProperty simultaneamente, il getter bloccherà brevemente il secondo filo finché non ritorna la oggetto al primo thread, ma, quindi restituirà l'oggetto anche al secondo thread. Entrambi i thread avranno quindi accesso completo e sbloccato all'oggetto.

EDIT in risposta ad ulteriori dettagli sulla questione

Sono ancora certo al 100% quello che stai cercando di raggiungere, ma se si desidera solo l'accesso atomica all'oggetto poi non potresti avere il blocco del codice chiamante contro l'oggetto stesso?

// 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 

Non ideale, ma fino a quando tutto l'accesso all'oggetto è contenuto in lock (MyProperty) sezioni allora questo approccio sarà thread-safe.

+0

concordato. Ho fornito una soluzione di esempio per ciò che stai chiedendo in risposta a una domanda Singleton http://stackoverflow.com/questions/7095/is-the-c-static-constructor-thread-safe/7105#7105 – Zooba

+0

sì, io ' Probabilmente andremo a bloccare l'oggetto come hai descritto. Grazie. – mmr

+0

Buon punto, tuttavia se ha avuto un 'MyProperty' chiamato' p' e ha chiamato due thread contemporaneamente: 'p = new MyObject (12)' e 'p = new MyObject (5)' allora * questo * accesso sarebbe stato sincronizzato . Comunque il membro 'Field1' non * resterà * mai bloccato. Sono abbastanza sicuro che questo è quello che stai dicendo, solo cercando di capirlo da solo. – Snoopy

1

Nell'esempio di codice che hai postato, un get non viene mai eseguito.

In un esempio più complesso:

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

E, naturalmente, supponendo che hai fatto un:

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

In doSomething() poi tutte le chiamate di blocco avrebbero non essere sufficiente a garantire la sincronizzazione over l'intero oggetto. Non appena viene ripristinata la funzione doSomething(), il blocco viene perso, quindi viene eseguita l'aggiunta e quindi viene eseguito l'assegnazione, che si blocca di nuovo.

Oppure, di scrivere in un altro modo si può far finta che le serrature non sono fatto amutomatically, e riscrivere questo più come "codice macchina" con una sola operazione per riga, e diventa evidente:

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

hunh. Forse sto fraintendendo le proprietà, ma sembra che il debugger stia entrando nella funzione 'get', specialmente quando inserisco un breakpoint lì, solo per un semplice compito. – mmr

+0

Devo ammettere che non sono sicuro al 100% che non invochi un riscatto, ma non vedo ragioni per cui lo farebbe. Se invoca un get, la stessa cosa che ho detto si applica, basta sostituire doSomething() con la funzione get. – SoapBox

+0

Invoca un get, ogni volta. Altrimenti da dove verrà il riferimento? –

1

Il La bellezza del multithreading è che non sai in quale ordine le cose accadranno. Se imposti qualcosa su un thread, potrebbe succedere prima, potrebbe succedere dopo il get.

Il codice che hai postato con il membro bloccato mentre viene letto e scritto. Se si desidera gestire il caso in cui il valore viene aggiornato, è consigliabile esaminare altre forme di sincronizzazione, ad esempio events. (Controlla le versioni auto/manuale). Quindi puoi dire al tuo thread "polling" che il valore è cambiato ed è pronto per essere riletto.

2

L'ambito del blocco nell'esempio si trova nel punto non corretto, deve trovarsi nell'ambito della proprietà della classe "MyObject" anziché nel contenitore.

Se la classe oggetto MyObject my viene semplicemente utilizzata per contenere i dati su cui un thread desidera scrivere e un altro (il thread dell'interfaccia utente) da cui leggere, potrebbe non essere necessario un setter e non costruirlo una volta.

Considerare inoltre se posizionare blocchi a livello di proprietà è il livello di scrittura della granularità del blocco; se più di una proprietà può essere scritta per rappresentare lo stato di una transazione (es .: ordini totali e peso totale) allora potrebbe essere meglio avere il blocco a livello MyObject (es. lock (myObject.SyncRoot) .. .)

0

Nella versione modificata, non si sta ancora fornendo un modo sicuro per aggiornare MyObject. Eventuali modifiche alle proprietà dell'oggetto dovranno essere eseguite all'interno di un blocco sincronizzato/bloccato.

È possibile scrivere singoli setter per gestirlo, ma è stato indicato che ciò sarà difficile a causa dei campi numerici di grandi dimensioni.Se davvero il caso (e non hai ancora fornito abbastanza informazioni per valutarlo), un'alternativa è scrivere un setter che usa la riflessione; questo ti permetterebbe di passare una stringa che rappresenta il nome del campo, e potresti cercare in modo dinamico il nome del campo e aggiornare il valore. Questo ti permetterebbe di avere un unico setter che possa funzionare su qualsiasi numero di campi. Questo non è così facile o efficiente, ma ti permetterebbe di gestire un gran numero di classi e campi.

12

La programmazione simultanea sarebbe piuttosto semplice se il tuo approccio potesse funzionare. Ma non è così, l'iceberg che affonda che Titanic è, per esempio, il client della classe a fare questo:

objectRef.MyProperty += 1; 

La gara di lettura-modifica-scrittura è abbastanza evidente, ci sono quelli peggiori. Non c'è assolutamente nulla che tu possa fare per rendere la tua proprietà thread-safe, oltre a renderla immutabile. È il tuo cliente che ha bisogno di affrontare il mal di testa. Essere costretti a delegare questo tipo di responsabilità a un programmatore che ha meno probabilità di farlo bene è il tallone d'Achille della programmazione concorrente.

+0

non sarebbe fantastico se potesse essere così facile? Sono sicuro che ci sono dei motivi per cui non lo è, ma non conosco abbastanza i dettagli per sapere perché il codice non funziona in questo modo. – mmr

+0

È importante capire questo, non provare la programmazione concorrente se non lo fai. Google "aggiornamenti atomici". –

+0

Capisco l'atomicità, ma sono confuso su dove le cose sono atomiche in C#. Solo i compiti di base, giusto? Specificare l'atomicità con qualcosa come Interlocked potrebbe essere utile ... – mmr

4

Come altri hanno sottolineato, una volta restituito l'oggetto dal getter, si perde il controllo su chi accede all'oggetto e quando. Per fare ciò che vuoi fare, dovrai mettere un lucchetto all'interno dell'oggetto stesso.

Forse non capisco l'immagine completa, ma in base alla descrizione, non sembra che sia necessario avere un blocco per ogni singolo campo. Se hai una serie di campi che sono semplicemente letti e scritti tramite i getter e setter, potresti probabilmente ottenere un blocco singolo per questi campi. Esiste ovviamente la possibilità che serializzi inutilmente l'operazione dei tuoi thread in questo modo. Ma ancora, in base alla tua descrizione, non sembra che tu stia accedendo aggressivamente all'oggetto.

Vorrei anche suggerire di utilizzare un evento anziché utilizzare una discussione per eseguire il polling dello stato del dispositivo. Con il meccanismo di polling, stai andando a colpire il lucchetto ogni volta che il thread interroga il dispositivo. Con il meccanismo degli eventi, una volta modificato lo stato, l'oggetto dovrebbe notificare qualsiasi listener. A quel punto, il thread 'polling' (che non avrebbe più richiesto il polling) si sarebbe svegliato e avrebbe ottenuto il nuovo stato. Questo sarà molto più efficiente.

Per fare un esempio ...

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; 
} 

tuo thread 'polling' sarebbe simile a questa.

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

Interessante. Dovrò dare un'occhiata a questo. La mia prima impressione è che non funzionerà, perché il dispositivo che sto usando non darà un messaggio di stato a meno che non venga richiesto. Quindi, se è in modalità 'prep', non c'è indicazione a meno che tu non chieda. – mmr

+0

Sì, se si deve spingere il dispositivo per segnalare lo stato, il polling è probabilmente l'unica cosa che si può fare. Per caso, esiste un meccanismo di callback per far sì che il dispositivo riporti periodicamente lo stato? Odio il polling perché è inefficiente, ma a volte non hai scelta. –

+0

Sfortunatamente, è un vero dispositivo seriale, risponde solo al polling. Questo non è USB, questo è 9 pin. La vecchia scuola è retrò, credo. – mmr

-1

non C# blocchi non soffrono degli stessi problemi di blocco come altre lingue allora?

E.G.

var someObj = -1; 

// Thread 1 

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

// Thread 2 

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

Questo potrebbe avere il problema di entrambi i thread che potrebbero ottenere i blocchi e modificare il valore. Questo potrebbe portare ad alcuni bug strani. Tuttavia non si desidera bloccare inutilmente l'oggetto a meno che non sia necessario. In questo caso dovresti considerare il doppio blocco controllato.

// Threads 1 & 2 

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

Solo qualcosa da tenere a mente.