C# Language
Lock Statement
Ricerca…
Sintassi
- lock (obj) {}
Osservazioni
Utilizzando l'istruzione lock
è possibile controllare l'accesso di diversi thread al codice all'interno del blocco di codice. Viene comunemente usato per prevenire condizioni di gara, ad esempio più thread che leggono e rimuovono oggetti da una collezione. Poiché il blocco delle forze thread in attesa di altri thread per uscire da un blocco di codice, può causare ritardi che potrebbero essere risolti con altri metodi di sincronizzazione.
MSDN
La parola chiave lock contrassegna un blocco di istruzioni come sezione critica ottenendo il blocco di esclusione reciproca per un dato oggetto, eseguendo un'istruzione e quindi rilasciando il blocco.
La parola chiave lock assicura che un thread non entri in una sezione critica del codice mentre un altro thread si trova nella sezione critica. Se un altro thread tenta di inserire un codice bloccato, attenderà, bloccherà, fino a quando l'oggetto non verrà rilasciato.
È consigliabile definire un oggetto privato da bloccare o una variabile oggetto statica privata per proteggere i dati comuni a tutte le istanze.
In C # 5.0 e versioni successive, l'istruzione lock
equivale a:
bool lockTaken = false;
try
{
System.Threading.Monitor.Enter(refObject, ref lockTaken);
// code
}
finally
{
if (lockTaken)
System.Threading.Monitor.Exit(refObject);
}
Per C # 4.0 e precedenti, l'istruzione lock
equivale a:
System.Threading.Monitor.Enter(refObject);
try
{
// code
}
finally
{
System.Threading.Monitor.Exit(refObject);
}
Uso semplice
L'uso comune del lock
è una sezione critica.
Nel seguente esempio si suppone che ReserveRoom
venga chiamato da diversi thread. La sincronizzazione con il lock
è il modo più semplice per prevenire le condizioni di gara qui. Il corpo del metodo è circondato da un lock
che garantisce che due o più thread non possano eseguirlo contemporaneamente.
public class Hotel
{
private readonly object _roomLock = new object();
public void ReserveRoom(int roomNumber)
{
// lock keyword ensures that only one thread executes critical section at once
// in this case, reserves a hotel room of given number
// preventing double bookings
lock (_roomLock)
{
// reserve room logic goes here
}
}
}
Se un thread raggiunge il lock
bloccato mentre un altro thread è in esecuzione al suo interno, il primo attenderà un altro per uscire dal blocco.
È consigliabile definire un oggetto privato da bloccare o una variabile oggetto statica privata per proteggere i dati comuni a tutte le istanze.
Eccezione di lancio in una dichiarazione di blocco
Il codice seguente rilascerà il blocco. Non ci saranno problemi L'istruzione di blocco dietro le quinte funziona come try finally
lock(locker)
{
throw new Exception();
}
Più può essere visto nella specifica C # 5.0 :
Una dichiarazione di lock
del modulo
lock (x) ...
dove x
è un'espressione di un tipo di riferimento , è esattamente equivalente a
bool __lockWasTaken = false;
try {
System.Threading.Monitor.Enter(x, ref __lockWasTaken);
...
}
finally {
if (__lockWasTaken) System.Threading.Monitor.Exit(x);
}
tranne che x
viene valutato solo una volta.
Ritorna in una dichiarazione di blocco
Il codice seguente rilascerà il blocco.
lock(locker)
{
return 5;
}
Per una spiegazione dettagliata, questa risposta SO è raccomandata.
Utilizzo di istanze di Object per il blocco
Quando si utilizza l'istruzione di lock
incorporata di C # è necessaria un'istanza di qualche tipo, ma il suo stato non ha importanza. Un'istanza di object
è perfetta per questo:
public class ThreadSafe {
private static readonly object locker = new object();
public void SomeThreadSafeMethod() {
lock (locker) {
// Only one thread can be here at a time.
}
}
}
NB istanze di Type
non dovrebbero essere utilizzate per questo (nel codice sopra typeof(ThreadSafe)
) perché le istanze di Type
sono condivise su AppDomains e quindi l'estensione del blocco può includere codice che non dovrebbe (ad esempio se ThreadSafe
è caricato in due AppDomain nello stesso processo quindi il blocco sulla sua istanza Type
si bloccherebbe reciprocamente).
Anti-pattern e trucchi
Blocco su una variabile allocata allo stack / locale
Uno degli errori durante l'utilizzo del lock
è l'utilizzo di oggetti locali come locker in una funzione. Poiché queste istanze di oggetti locali saranno diverse per ogni chiamata della funzione, il lock
non funzionerà come previsto.
List<string> stringList = new List<string>();
public void AddToListNotThreadSafe(string something)
{
// DO NOT do this, as each call to this method
// will lock on a different instance of an Object.
// This provides no thread safety, it only degrades performance.
var localLock = new Object();
lock(localLock)
{
stringList.Add(something);
}
}
// Define object that can be used for thread safety in the AddToList method
readonly object classLock = new object();
public void AddToList(List<string> stringList, string something)
{
// USE THE classLock instance field to achieve a
// thread-safe lock before adding to stringList
lock(classLock)
{
stringList.Add(something);
}
}
Supponendo che il blocco limiti l'accesso all'oggetto di sincronizzazione stesso
Se un thread chiama: lock(obj)
e un altro thread chiama obj.ToString()
secondo thread non verrà bloccato.
object obj = new Object();
public void SomeMethod()
{
lock(obj)
{
//do dangerous stuff
}
}
//Meanwhile on other tread
public void SomeOtherMethod()
{
var objInString = obj.ToString(); //this does not block
}
Aspettando sottoclassi per sapere quando bloccare
A volte le classi base sono progettate in modo tale che le loro sottoclassi sono obbligate a utilizzare un blocco quando accedono a determinati campi protetti:
public abstract class Base
{
protected readonly object padlock;
protected readonly List<string> list;
public Base()
{
this.padlock = new object();
this.list = new List<string>();
}
public abstract void Do();
}
public class Derived1 : Base
{
public override void Do()
{
lock (this.padlock)
{
this.list.Add("Derived1");
}
}
}
public class Derived2 : Base
{
public override void Do()
{
this.list.Add("Derived2"); // OOPS! I forgot to lock!
}
}
È molto più sicuro incapsulare il blocco utilizzando un metodo di modello :
public abstract class Base
{
private readonly object padlock; // This is now private
protected readonly List<string> list;
public Base()
{
this.padlock = new object();
this.list = new List<string>();
}
public void Do()
{
lock (this.padlock) {
this.DoInternal();
}
}
protected abstract void DoInternal();
}
public class Derived1 : Base
{
protected override void DoInternal()
{
this.list.Add("Derived1"); // Yay! No need to lock
}
}
Il blocco su una variabile ValueType in scatola non si sincronizza
Nell'esempio seguente, una variabile privata viene inserita in modo implicito in quanto viene fornita come argomento di un object
a una funzione, aspettandosi che una risorsa di monitoraggio si blocchi a. La boxe si verifica appena prima di chiamare la funzione IncInSync, quindi l'istanza boxed corrisponde a un oggetto heap diverso ogni volta che viene chiamata la funzione.
public int Count { get; private set; }
private readonly int counterLock = 1;
public void Inc()
{
IncInSync(counterLock);
}
private void IncInSync(object monitorResource)
{
lock (monitorResource)
{
Count++;
}
}
La boxe si verifica nella funzione Inc
:
BulemicCounter.Inc:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.0
IL_0003: ldfld UserQuery+BulemicCounter.counterLock
IL_0008: box System.Int32**
IL_000D: call UserQuery+BulemicCounter.IncInSync
IL_0012: nop
IL_0013: ret
Ciò non significa che non sia possibile utilizzare ValueType in scatola per il blocco del monitor:
private readonly object counterLock = 1;
Ora la boxe si verifica nel costruttore, che va bene per il blocco:
IL_0001: ldc.i4.1
IL_0002: box System.Int32
IL_0007: stfld UserQuery+BulemicCounter.counterLock
Utilizzare i blocchi inutilmente quando esiste un'alternativa più sicura
Un modello molto comune consiste nell'utilizzare un List
o un Dictionary
privato in una classe thread-safe e bloccarli ogni volta che si accede:
public class Cache
{
private readonly object padlock;
private readonly Dictionary<string, object> values;
public WordStats()
{
this.padlock = new object();
this.values = new Dictionary<string, object>();
}
public void Add(string key, object value)
{
lock (this.padlock)
{
this.values.Add(key, value);
}
}
/* rest of class omitted */
}
Se ci sono più metodi per accedere al dizionario dei values
, il codice può diventare molto lungo e, soprattutto, bloccare tutto il tempo oscura il suo intento . Il blocco è anche molto facile da dimenticare e la mancanza di un blocco adeguato può causare errori molto difficili da trovare.
Usando un ConcurrentDictionary
, possiamo evitare di bloccare completamente:
public class Cache
{
private readonly ConcurrentDictionary<string, object> values;
public WordStats()
{
this.values = new ConcurrentDictionary<string, object>();
}
public void Add(string key, object value)
{
this.values.Add(key, value);
}
/* rest of class omitted */
}
L'uso di raccolte simultanee migliora anche le prestazioni, perché tutte impiegano tecniche di lock-free in una certa misura.