C# Language
Slotverklaring
Zoeken…
Syntaxis
- lock (obj) {}
Opmerkingen
Met de lock
kunt u de toegang van verschillende threads tot code binnen het codeblok beheren. Het wordt vaak gebruikt om raceomstandigheden te voorkomen, bijvoorbeeld dat meerdere threads items lezen en verwijderen uit een verzameling. Omdat vergrendeling threads dwingt te wachten tot andere threads een codeblok verlaten, kan dit vertragingen veroorzaken die kunnen worden opgelost met andere synchronisatiemethoden.
MSDN
Het vergrendelingssleutelwoord markeert een instructieblok als een kritieke sectie door de vergrendeling voor wederzijdse uitsluiting voor een bepaald object te verkrijgen, een instructie uit te voeren en vervolgens de vergrendeling op te heffen.
Het lock-sleutelwoord zorgt ervoor dat de ene thread geen kritisch gedeelte van de code binnengaat terwijl een andere thread zich in de kritieke sectie bevindt. Als een andere thread probeert een vergrendelde code in te voeren, wacht deze blok totdat het object wordt vrijgegeven.
Best practice is om een privé-object te vergrendelen op, of een private static object variabele te beschermen data voor alle gevallen te definiëren.
In C # 5.0 en hoger, het lock
statement is gelijk aan:
bool lockTaken = false;
try
{
System.Threading.Monitor.Enter(refObject, ref lockTaken);
// code
}
finally
{
if (lockTaken)
System.Threading.Monitor.Exit(refObject);
}
Voor C # 4.0 en eerdere versies, het lock
statement is gelijk aan:
System.Threading.Monitor.Enter(refObject);
try
{
// code
}
finally
{
System.Threading.Monitor.Exit(refObject);
}
Eenvoudig gebruik
Gemeenschappelijk gebruik van lock
is een kritieke sectie.
In het volgende voorbeeld zou ReserveRoom
vanuit verschillende threads worden aangeroepen. Synchronisatie met lock
is de eenvoudigste manier om racecondities hier te voorkomen. Methode lichaam is omgeven met lock
die ervoor zorgt dat twee of meer threads het niet tegelijkertijd kunnen uitvoeren.
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
}
}
}
Als een rode draad bereikt lock
-ed blok, terwijl een andere thread wordt uitgevoerd binnen het, zal de voormalige wachten nog om het blok te verlaten.
De beste praktijk is om een privéobject te vergrendelen of een statische privéobjectvariabele om gegevens te beschermen die voor alle instanties gelden.
Uitzondering gooien in een slotverklaring
De volgende code zal het slot ontgrendelen. Er zal geen probleem zijn. Achter de schermen werkt de slotverklaring als try finally
lock(locker)
{
throw new Exception();
}
Meer is te zien in de C # 5.0-specificatie :
Een lock
verklaring van het formulier
lock (x) ...
waar x
een uitdrukking van een referentietype is , is precies gelijk aan
bool __lockWasTaken = false;
try {
System.Threading.Monitor.Enter(x, ref __lockWasTaken);
...
}
finally {
if (__lockWasTaken) System.Threading.Monitor.Exit(x);
}
behalve dat x
slechts eenmaal wordt geëvalueerd.
Keer terug in een slotverklaring
De volgende code zal het slot ontgrendelen.
lock(locker)
{
return 5;
}
Voor een gedetailleerde uitleg wordt dit SO-antwoord aanbevolen.
Exemplaren van Object gebruiken voor vergrendeling
Bij gebruik van de ingebouwde lock
instructie van C # is een instantie van een bepaald type nodig, maar de status doet er niet toe. Een instantie van object
is hiervoor perfect:
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 . exemplaren van Type
moeten hiervoor niet worden gebruikt (in de bovenstaande code type van typeof(ThreadSafe)
) omdat exemplaren van Type
worden gedeeld tussen AppDomains en dus de omvang van het slot naar verwachting code kan bevatten die het niet zou moeten zijn (bijv. als ThreadSafe
is geladen in twee AppDomains in hetzelfde proces dan vergrendeling op de Type
instantie zouden elkaar vergrendelen).
Antipatronen en gotcha's
Vergrendelen op een stapel toegewezen / lokale variabele
Een van de denkfouten bij het gebruik van lock
is het gebruik van lokale objecten als locker in een functie. Aangezien deze lokale objectinstanties verschillen bij elke aanroep van de functie, werkt lock
niet zoals verwacht.
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);
}
}
Ervan uitgaande dat vergrendeling de toegang tot het synchronisatieobject zelf beperkt
Als een thread aanroept: lock(obj)
en een andere thread aanroept obj.ToString()
tweede thread wordt niet geblokkeerd.
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
}
Verwacht dat subklassen weten wanneer ze moeten worden vergrendeld
Soms zijn basisklassen zo ontworpen dat hun subklassen een slot moeten gebruiken bij toegang tot bepaalde beschermde velden:
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!
}
}
Het is veel veiliger om vergrendeling in te kapselen met behulp van een sjabloonmethode :
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
}
}
Vergrendelen op een in een vak geplaatste ValueType-variabele wordt niet gesynchroniseerd
In het volgende voorbeeld wordt een privévariabele impliciet in een doos geplaatst omdat deze als een object
aan een functie wordt geleverd, in de verwachting dat een monitorresource wordt vergrendeld. Het boksen gebeurt net voordat de IncInSync-functie wordt aangeroepen, dus de boxed-instantie komt overeen met een ander heap-object elke keer dat de functie wordt aangeroepen.
public int Count { get; private set; }
private readonly int counterLock = 1;
public void Inc()
{
IncInSync(counterLock);
}
private void IncInSync(object monitorResource)
{
lock (monitorResource)
{
Count++;
}
}
Boksen vindt plaats in de Inc
functie:
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
Het betekent niet dat een in de doos geplaatst ValueType helemaal niet kan worden gebruikt voor monitorvergrendeling:
private readonly object counterLock = 1;
Nu vindt boksen plaats in de constructor, wat prima is voor vergrendeling:
IL_0001: ldc.i4.1
IL_0002: box System.Int32
IL_0007: stfld UserQuery+BulemicCounter.counterLock
Onnodig gebruik van sloten wanneer er een veiliger alternatief bestaat
Een veel voorkomend patroon is om een List
of Dictionary
in een thread veilige klasse te gebruiken en te vergrendelen telkens wanneer deze wordt geopend:
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 */
}
Als er meerdere methoden toegang tot values
woordenboek, kan de code erg lang te krijgen en, nog belangrijker, het vergrendelen van de hele tijd verduistert haar intentie. Vergrendelen is ook heel gemakkelijk te vergeten en een gebrek aan juiste vergrendeling kan ervoor zorgen dat zeer moeilijk te vinden bugs zijn.
Door een ConcurrentDictionary
, kunnen we voorkomen dat ze volledig worden vergrendeld:
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 */
}
Het gebruik van gelijktijdige collecties verbetert ook de prestaties omdat ze allemaal tot op zekere hoogte lock-free technieken gebruiken.