C# Language
Oświadczenie blokady
Szukaj…
Składnia
- lock (obj) {}
Uwagi
Za pomocą instrukcji lock
możesz kontrolować dostęp różnych kodów do kodu w bloku kodu. Jest powszechnie stosowany w celu zapobiegania warunkom wyścigu, na przykład odczytywanie wielu wątków i usuwanie elementów z kolekcji. Ponieważ blokowanie zmusza wątki do czekania, aż inne wątki wyjdą z bloku kodu, może to powodować opóźnienia, które można rozwiązać za pomocą innych metod synchronizacji.
MSDN
Słowo kluczowe lock oznacza blok instrukcji jako sekcję krytyczną, uzyskując blokadę wzajemnego wykluczenia dla danego obiektu, wykonując instrukcję, a następnie zwalniając blokadę.
Słowo kluczowe lock zapewnia, że jeden wątek nie wchodzi w krytyczną sekcję kodu, podczas gdy inny wątek znajduje się w sekcji krytycznej. Jeśli inny wątek spróbuje wprowadzić zablokowany kod, zaczeka, zablokuje, aż obiekt zostanie zwolniony.
Najlepszą praktyką jest zdefiniowanie prywatnego obiektu do zablokowania lub prywatnej zmiennej obiektu statycznego w celu ochrony danych wspólnych dla wszystkich instancji.
W C # 5.0 i późniejszych instrukcja lock
jest równoważna z:
bool lockTaken = false;
try
{
System.Threading.Monitor.Enter(refObject, ref lockTaken);
// code
}
finally
{
if (lockTaken)
System.Threading.Monitor.Exit(refObject);
}
W przypadku C # 4.0 i wcześniejszych instrukcja lock
jest równoważna z:
System.Threading.Monitor.Enter(refObject);
try
{
// code
}
finally
{
System.Threading.Monitor.Exit(refObject);
}
Proste użycie
Wspólne stosowanie lock
jest krytyczną częścią.
W poniższym przykładzie ReserveRoom
ma być wywoływany z różnych wątków. Synchronizacja z lock
jest tutaj najprostszym sposobem na uniknięcie warunków wyścigu. Korpus metody jest otoczony lock
która zapewnia, że dwa lub więcej wątków nie może wykonać go jednocześnie.
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
}
}
}
Jeśli wątek osiągnie lock
blok, podczas gdy inny wątek w nim działa, ten pierwszy poczeka, aż inny opuści blok.
Najlepszą praktyką jest zdefiniowanie prywatnego obiektu do zablokowania lub prywatnej zmiennej obiektu statycznego w celu ochrony danych wspólnych dla wszystkich instancji.
Zgłaszanie wyjątku w instrukcji lock
Poniższy kod zwolni blokadę. Nie będzie problemu. Za kulisami instrukcja lock działa jak try finally
lock(locker)
{
throw new Exception();
}
Więcej można zobaczyć w specyfikacji C # 5.0 :
Instrukcja lock
formularza
lock (x) ...
gdzie x
jest wyrażeniem typu odniesienia , jest dokładnie równoważne z
bool __lockWasTaken = false;
try {
System.Threading.Monitor.Enter(x, ref __lockWasTaken);
...
}
finally {
if (__lockWasTaken) System.Threading.Monitor.Exit(x);
}
z wyjątkiem tego, że x
jest obliczany tylko raz.
Zwróć w instrukcji blokady
Poniższy kod zwolni blokadę.
lock(locker)
{
return 5;
}
Aby uzyskać szczegółowe wyjaśnienie, ta SO odpowiedź jest zalecana.
Używanie instancji Object do blokady
Podczas korzystania z wbudowanej instrukcji lock
C # potrzebna jest instancja pewnego typu, ale jej stan nie ma znaczenia. Instancja object
jest do tego idealna:
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 instancje Type
nie powinny być do tego używane (w kodzie powyżej typeof(ThreadSafe)
), ponieważ instancje Type
są współużytkowane przez AppDomains, a zatem zakres blokady może obejmować kod, którego nie powinien (np. jeśli ThreadSafe
jest załadowany do dwie domeny aplikacji w tym samym procesie, a następnie zablokowanie instancji Type
spowoduje wzajemne zablokowanie).
Anty-Wzory i gotchas
Blokowanie zmiennej lokalnej / przydzielonej do stosu
Jednym z błędów podczas używania lock
jest użycie obiektów lokalnych jako szafki w funkcji. Ponieważ te lokalne wystąpienia obiektów będą się różnić przy każdym wywołaniu funkcji, lock
nie będzie działać zgodnie z oczekiwaniami.
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);
}
}
Zakładając, że blokowanie ogranicza dostęp do samego obiektu synchronizującego
Jeśli jeden wątek obj.ToString()
: lock(obj)
a inny wątek obj.ToString()
drugi wątek nie zostanie zablokowany.
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
}
Oczekiwanie, by podklasy wiedziały, kiedy zablokować
Czasami klasy podstawowe są zaprojektowane w taki sposób, że ich podklasy muszą używać blokady podczas uzyskiwania dostępu do niektórych chronionych pól:
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!
}
}
O wiele bezpieczniej jest zamknąć enkapsulację przy użyciu metody szablonu :
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
}
}
Blokowanie zmiennej pudełkowej ValueType nie synchronizuje się
W poniższym przykładzie zmienna prywatna jest niejawnie zapakowana, ponieważ jest dostarczana jako argument object
do funkcji, oczekując, że zasób monitora zostanie zablokowany. Boksowanie następuje tuż przed wywołaniem funkcji IncInSync, więc instancja pudełkowa odpowiada innemu obiektowi sterty za każdym razem, gdy funkcja jest wywoływana.
public int Count { get; private set; }
private readonly int counterLock = 1;
public void Inc()
{
IncInSync(counterLock);
}
private void IncInSync(object monitorResource)
{
lock (monitorResource)
{
Count++;
}
}
Boks występuje w funkcji 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
Nie oznacza to, że nie można w ogóle użyć blokowanego parametru ValueType do blokowania monitora:
private readonly object counterLock = 1;
Teraz boks występuje w konstruktorze, co jest dobre do blokowania:
IL_0001: ldc.i4.1
IL_0002: box System.Int32
IL_0007: stfld UserQuery+BulemicCounter.counterLock
Używanie zamków niepotrzebnie, gdy istnieje bezpieczniejsza alternatywa
Bardzo częstym wzorcem jest używanie prywatnej List
lub Dictionary
w klasie bezpiecznej dla wątków i blokowanie za każdym razem, gdy jest dostępne:
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 */
}
Jeśli istnieje wiele metod uzyskiwania dostępu do values
słownika, kod może stać się bardzo długo i, co ważniejsze, zamykając cały czas zaciemnia jego intencją. Blokowanie jest również bardzo łatwe do zapomnienia, a brak odpowiedniego blokowania może powodować bardzo trudne do znalezienia błędy.
Używając ConcurrentDictionary
, możemy uniknąć całkowitego zablokowania:
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 */
}
Korzystanie z równoczesnych kolekcji również poprawia wydajność, ponieważ wszystkie z nich w pewnym stopniu stosują techniki bez blokady .