Поиск…


Синтаксис

  • lock (obj) {}

замечания

Используя оператор lock вы можете управлять доступом разных потоков к коду внутри блока кода. Он обычно используется для предотвращения условий гонки, например, для чтения и удаления элементов из коллекции. Поскольку блокировка заставляет потоки ждать, пока другие потоки покинут блок кода, это может вызвать задержки, которые могут быть решены другими методами синхронизации.

MSDN

Ключевое слово lock указывает блок оператора как критический раздел, получая блокировку взаимного исключения для данного объекта, выполняя инструкцию, а затем освобождая блокировку.

Ключевое слово lock гарантирует, что один поток не войдет в критический раздел кода, а другой поток находится в критическом разделе. Если другой поток пытается ввести заблокированный код, он будет ждать, блокировать, пока объект не будет выпущен.

Лучшая практика заключается в определении частного объекта для блокировки или частной переменной статического объекта для защиты данных, общих для всех экземпляров.


В C # 5.0 и более поздних версиях оператор lock эквивалентен:

bool lockTaken = false;
try 
{
    System.Threading.Monitor.Enter(refObject, ref lockTaken);
    // code 
}
finally 
{
    if (lockTaken)
        System.Threading.Monitor.Exit(refObject);
}

Для C # 4.0 и ранее оператор lock эквивалентен:

System.Threading.Monitor.Enter(refObject);
try 
{
    // code
}
finally 
{
     System.Threading.Monitor.Exit(refObject);
}

Простое использование

Общее использование lock - критический раздел.

В следующем примере ReserveRoom должен быть вызван из разных потоков. Синхронизация с lock - это самый простой способ предотвратить состояние гонки здесь. Тело метода окружено lock который гарантирует, что два или более потока не могут выполнить его одновременно.

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
        }
    }
}

Если поток достигает lock блока, а другой поток работает внутри него, первый будет ждать другого, чтобы выйти из блока.

Лучшая практика заключается в определении частного объекта для блокировки или частной переменной статического объекта для защиты данных, общих для всех экземпляров.

Исключение исключения в инструкции блокировки

Следующий код освободит блокировку. Проблем не будет. За кулисами оператор блокировки работает как try finally

lock(locker)
{
    throw new Exception();
}

Больше можно увидеть в спецификации C # 5.0 :

Оператор lock формы

lock (x) ...

где x - выражение ссылочного типа , точно эквивалентно

bool __lockWasTaken = false;
try {
    System.Threading.Monitor.Enter(x, ref __lockWasTaken);
    ...
}
finally {
    if (__lockWasTaken) System.Threading.Monitor.Exit(x);
}

за исключением того, что x оценивается только один раз.

Возвращение в операторе блокировки

Следующий код освободит блокировку.

lock(locker)
{
    return 5;
}

Для подробного объяснения рекомендуется использовать этот SO-ответ .

Использование экземпляров объекта для блокировки

При использовании встроенного оператора lock C # необходим экземпляр некоторого типа, но его состояние не имеет значения. Экземпляр object идеально подходит для этого:

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 . экземпляры Type не должны использоваться для этого (в коде выше typeof(ThreadSafe) ), потому что экземпляры Type совместно используются в AppDomains, и, следовательно, степень блокировки может включать в себя код, который он не должен (например, если ThreadSafe загружается в два AppDomains в том же процессе, а затем блокировка на экземпляре Type будет взаимно блокироваться).

Anti-Patterns и gotchas

Блокировка на выделенную стекю / локальную переменную

Одной из ошибок при использовании lock является использование локальных объектов в качестве блокировки в функции. Поскольку эти локальные экземпляры объектов будут отличаться при каждом вызове функции, lock не будет выполняться так, как ожидалось.

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);
    }
}

Предполагая, что блокировка ограничивает доступ к самому синхронизирующему объекту

Если один поток вызывает: lock(obj) и другой поток вызывает obj.ToString() второй поток не будет заблокирован.

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
 }

Ожидание подклассов, чтобы знать, когда блокировать

Иногда базовые классы создаются таким образом, что их подклассы должны использовать блокировку при доступе к определенным защищенным полям:

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!
    }
}

Гораздо безопаснее инкапсулировать блокировку с помощью метода шаблонов :

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
    }
}

Блокировка на коробке ValueType переменной не синхронизирует

В следующем примере приватная переменная неявно помещается в ящик, поскольку она предоставляется в качестве аргумента object функции, ожидая, что ресурс монитора будет заблокирован. Бокс происходит непосредственно перед вызовом функции IncInSync, поэтому экземпляр в штучной упаковке соответствует разному кучному объекту при каждом вызове функции.

public int Count { get; private set; }

private readonly int counterLock = 1;

public void Inc()
{
    IncInSync(counterLock);
}

private void IncInSync(object monitorResource)
{
    lock (monitorResource)
    {
        Count++;
    }
}

Бокс происходит в функции 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         

Это не означает, что встроенный ValueType не может использоваться для блокировки монитора вообще:

private readonly object counterLock = 1;

Теперь бокс происходит в конструкторе, который отлично подходит для блокировки:

IL_0001:  ldc.i4.1    
IL_0002:  box         System.Int32
IL_0007:  stfld       UserQuery+BulemicCounter.counterLock

Использование блокировок без необходимости, когда существует более безопасная альтернатива

Очень распространенная картина заключается в том, чтобы использовать закрытый List или Dictionary в потокобезопасном классе и блокировать каждый раз, когда к нему обращаются:

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 */
}

Если есть несколько методов доступа к словарям values , код может стать очень длинным и, что более важно, блокировать все время, затеняет его намерение . Блокировка также очень легко забыть, и отсутствие правильной блокировки может очень затруднить поиск ошибок.

Используя ConcurrentDictionary , мы можем полностью исключить блокировку:

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 */
}

Использование параллельных коллекций также повышает производительность, потому что все они в некоторой степени используют методы блокировки .



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow