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 .



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow