C# Language
Déclaration de verrouillage
Recherche…
Syntaxe
- verrouiller (obj) {}
Remarques
À l'aide de l'instruction de lock
, vous pouvez contrôler l'accès des différents threads au code dans le bloc de code. Il est couramment utilisé pour empêcher les conditions de concurrence, par exemple, la lecture et la suppression d'éléments multiples dans une collection. Comme le verrouillage oblige les threads à attendre que d'autres threads quittent un bloc de code, cela peut entraîner des retards qui pourraient être résolus avec d'autres méthodes de synchronisation.
MSDN
Le mot-clé lock marque un bloc d'instructions en tant que section critique en obtenant le verrou d'exclusion mutuelle pour un objet donné, en exécutant une instruction, puis en libérant le verrou.
Le mot-clé lock garantit qu'un thread n'entre pas dans une section critique du code alors qu'un autre thread se trouve dans la section critique. Si un autre thread tente d'entrer un code verrouillé, il attendra, bloquera, jusqu'à ce que l'objet soit libéré.
La meilleure pratique consiste à définir un objet privé à verrouiller ou une variable d'objet statique privée pour protéger les données communes à toutes les instances.
Dans C # 5.0 et versions ultérieures, la déclaration de lock
est équivalente à:
bool lockTaken = false;
try
{
System.Threading.Monitor.Enter(refObject, ref lockTaken);
// code
}
finally
{
if (lockTaken)
System.Threading.Monitor.Exit(refObject);
}
Pour C # 4.0 et les versions antérieures, l'instruction de lock
est équivalente à:
System.Threading.Monitor.Enter(refObject);
try
{
// code
}
finally
{
System.Threading.Monitor.Exit(refObject);
}
Usage simple
L'utilisation commune du lock
est une section critique.
Dans l'exemple suivant, ReserveRoom
doit être appelé à partir de différents threads. La synchronisation avec le lock
est le moyen le plus simple de prévenir les conditions de course. Le corps de la méthode est entouré d'un lock
qui garantit que deux threads ou plus ne peuvent pas l'exécuter simultanément.
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
}
}
}
Si un thread atteint lock
bloc verrouillé pendant qu'un autre thread s'exécute dans celui-ci, le premier attendra un autre pour quitter le bloc.
La meilleure pratique consiste à définir un objet privé à verrouiller ou une variable d'objet statique privée pour protéger les données communes à toutes les instances.
Lancer une exception dans une déclaration de verrouillage
Le code suivant libère le verrou. Il n'y aura pas de problème. L'énoncé de verrouillage des coulisses fonctionne comme try finally
lock(locker)
{
throw new Exception();
}
Plus peut être vu dans la spécification C # 5.0 :
Une déclaration de lock
du formulaire
lock (x) ...
où x
est une expression d'un type de référence , est précisément équivalent à
bool __lockWasTaken = false;
try {
System.Threading.Monitor.Enter(x, ref __lockWasTaken);
...
}
finally {
if (__lockWasTaken) System.Threading.Monitor.Exit(x);
}
sauf que x
n'est évalué qu'une seule fois.
Retourner dans une déclaration de verrouillage
Le code suivant libère le verrou.
lock(locker)
{
return 5;
}
Pour une explication détaillée, cette réponse SO est recommandée.
Utilisation d'instances d'objet pour le verrouillage
Lorsque vous utilisez la déclaration de lock
intégrée à C #, une instance de quelque type est nécessaire, mais son état importe peu. Une instance d' object
est parfaite pour cela:
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 les instances de Type
ne doivent pas être utilisées pour cela (dans le code au-dessus de typeof(ThreadSafe)
) car les instances de Type
sont partagées entre AppDomains et l'étendue du verrou peut donc inclure du code (par exemple, si ThreadSafe
est chargé dans deux AppDomains dans le même processus puis verrouiller sur son instance Type
se verrouilleraient mutuellement).
Anti-Patterns et Gotchas
Verrouillage sur une variable allouée / locale
L'une des erreurs lors de l'utilisation de lock
est l'utilisation d'objets locaux comme casier dans une fonction. Étant donné que ces instances d'objet local différeront à chaque appel de la fonction, le lock
ne fonctionnera pas comme prévu.
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);
}
}
En supposant que le verrouillage restreint l'accès à l'objet de synchronisation lui-même
Si un thread appelle: lock(obj)
et un autre thread appelle obj.ToString()
deuxième thread ne sera pas bloqué.
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
}
Attend des sous-classes pour savoir quand verrouiller
Parfois, les classes de base sont conçues de telle sorte que leurs sous-classes doivent utiliser un verrou lors de l'accès à certains champs protégés:
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!
}
}
Il est beaucoup plus sûr d' encapsuler le verrouillage en utilisant une méthode de modèle :
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
}
}
Le verrouillage sur une variable ValueType en boîte ne se synchronise pas
Dans l'exemple suivant, une variable privée est implicitement encadrée car elle est fournie en tant qu'argument d' object
à une fonction, s'attendant à ce qu'une ressource de moniteur se verrouille. La boxe se produit juste avant d'appeler la fonction IncInSync, donc l'instance encadrée correspond à un objet de segment différent à chaque appel de la fonction.
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 se produit dans la fonction 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
Cela ne signifie pas qu'un ValueType en boîte ne peut pas être utilisé pour le verrouillage du moniteur:
private readonly object counterLock = 1;
Maintenant, la boxe se produit dans le constructeur, ce qui est bien pour le verrouillage:
IL_0001: ldc.i4.1
IL_0002: box System.Int32
IL_0007: stfld UserQuery+BulemicCounter.counterLock
Utiliser des verrous inutilement lorsqu'une alternative plus sûre existe
Un modèle très courant consiste à utiliser une List
ou un Dictionary
privé dans une classe de thread thread et à verrouiller chaque fois qu'il est accessible:
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 */
}
S'il y a plusieurs méthodes qui accèdent au dictionnaire de values
, le code peut devenir très long et, plus important encore, le verrouillage tout le temps en cache l' intention . Le verrouillage est également très facile à oublier et le manque de verrouillage peut rendre très difficile la recherche de bogues.
En utilisant un ConcurrentDictionary
, nous pouvons éviter de verrouiller complètement:
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'utilisation de collections simultanées améliore également les performances car toutes utilisent des techniques sans verrouillage dans une certaine mesure.