C# Language
Заблокировать
Поиск…
Синтаксис
- 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 */
}
Использование параллельных коллекций также повышает производительность, потому что все они в некоторой степени используют методы блокировки .