Domanda

Sono ancora un po 'poco chiaro e quando avvolgere un blocco attorno ad un codice. La mia regola generale è quella di avvolgere un'operazione in un lucchetto quando legge o scrive su una variabile statica. Ma quando viene letta SOLO una variabile statica (ad es. È di sola lettura impostata durante l'inizializzazione del tipo), l'accesso ad essa non deve essere racchiuso in un'istruzione lock, giusto? Di recente ho visto del codice simile al seguente esempio e mi ha fatto pensare che potrebbero esserci delle lacune nella mia conoscenza del multithreading:

class Foo
{
    private static readonly string bar = "O_o";

    private bool TrySomething()
    {
        string bar;

        lock(Foo.objectToLockOn)
        {
            bar = Foo.bar;          
        }       

        // Do something with bar
    }
}

Non ha proprio senso per me - perché per problemi di concorrenza con READING un registro?

Inoltre, questo esempio solleva un'altra domanda. Uno di questi è migliore dell'altro? (Ad esempio, l'esempio due tiene il blocco per meno tempo?) Suppongo che potrei smontare il MSIL ...

class Foo
{
    private static string joke = "yo momma";

    private string GetJoke()
    {
        lock(Foo.objectToLockOn)
        {
            return Foo.joke;
        }
    }
}

vs.

class Foo
{
    private static string joke = "yo momma";

        private string GetJoke()
        {
            string joke;

            lock(Foo.objectToLockOn)
            {
                joke = Foo.joke;
            }

            return joke;
        }
}
È stato utile?

Soluzione

Poiché nessuno dei codici scritti modifica il campo statico dopo l'inizializzazione, non è necessario alcun blocco. La sola sostituzione della stringa con un nuovo valore non richiede neanche la sincronizzazione, a meno che il nuovo valore non dipenda dai risultati di una lettura del vecchio valore.

I campi statici non sono le uniche cose che necessitano di sincronizzazione, qualsiasi riferimento condiviso che potrebbe essere modificato è vulnerabile ai problemi di sincronizzazione.

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        count++;
    }
}

Potresti supporre che due thread che eseguono il metodo TrySomething vadano bene. Ma non lo è.

  1. Il thread A legge il valore di count (0) in un registro in modo che possa essere incrementato.
  2. Cambio di contesto! Lo scheduler del thread decide che il thread A ha avuto un tempo di esecuzione sufficiente. Il prossimo in linea è Discussione B.
  3. Il thread B legge il valore di count (0) in un registro.
  4. Il thread B incrementa il registro.
  5. Il thread B salva il risultato (1) per il conteggio.
  6. Contesto torna ad A.
  7. Il thread A ricarica il registro con il valore di count (0) salvato nel suo stack.
  8. La discussione A incrementa il registro.
  9. Il thread A salva il risultato (1) per il conteggio.

Quindi, anche se abbiamo chiamato count ++ due volte, il valore di count è appena passato da 0 a 1. Consente di rendere il codice thread-safe:

class Foo
{
    private int count = 0;
    private readonly object sync = new object();
    public void TrySomething()    
    {
        lock(sync)
            count++;
    }
}

Ora, quando il thread A viene interrotto, il thread B non può scherzare con il conteggio perché colpirà l'istruzione lock e quindi bloccherà fino a quando il thread A non avrà rilasciato la sincronizzazione.

A proposito, c'è un modo alternativo per rendere incrementali i thread Int32s e Int64s:

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        System.Threading.Interlocked.Increment(ref count);
    }
}

Per quanto riguarda la seconda parte della tua domanda, penso che andrei semplicemente con quello che è più facile da leggere, qualsiasi differenza di prestazioni ci sarà trascurabile. L'ottimizzazione precoce è la radice di tutti i mali, ecc.

Perché il threading è difficile

Altri suggerimenti

La lettura o la scrittura di un campo a 32 bit o inferiore è un'operazione atomica in C #. Non c'è bisogno di un lucchetto nel codice che hai presentato, per quanto posso vedere.

Mi sembra che il blocco non sia necessario nel tuo primo caso. L'uso dell'inizializzatore statico per inizializzare la barra è garantito come thread-safe. Dal momento che hai sempre letto il valore, non è necessario bloccarlo. Se il valore non cambierà mai, non ci sarà mai alcuna contesa, perché bloccare affatto?

Letture sporche?

A mio avviso, dovresti cercare di non mettere le variabili statiche in una posizione in cui devono essere lette / scritte da thread diversi. In questo caso sono essenzialmente variabili globali gratuite per tutti, e i globi sono quasi sempre una brutta cosa.

Detto questo, se mettete una variabile statica in una tale posizione, potreste voler bloccare durante una lettura, per ogni evenienza - ricordate, un altro thread potrebbe essersi interrotto e aver cambiato il valore durante the read, e se lo fa, potresti finire con dati corrotti. Le letture non sono necessariamente operazioni atomiche a meno che non si assicuri che lo siano bloccando. Lo stesso vale per le scritture: non sono neppure operazioni atomiche.

Modifica: Come ha sottolineato Mark, per alcune primitive in C # le letture sono sempre atomiche. Ma fai attenzione con altri tipi di dati.

Se stai semplicemente scrivendo un valore su un puntatore, non è necessario bloccare, poiché quell'azione è atomica. Generalmente, dovresti bloccare ogni volta che devi fare una transazione che coinvolge almeno due azioni atomiche (letture o scritture) che dipendono dallo stato che non cambia tra l'inizio e la fine.

Detto questo & # 8211; Vengo dalla terra di Java, dove tutte le letture e le scritture di variabili sono azioni atomiche. Altre risposte qui suggeriscono che .NET è diverso.

Quanto al tuo " che è meglio " domanda, sono uguali poiché l'ambito della funzione non viene utilizzato per nient'altro.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top