Lock c как работает
Наряду с оператором lock для синхронизации потоков мы можем использовать мониторы, представленные классом System.Threading.Monitor . Для управления синхронизацией этот класс предоставляет следующие методы:
- void Enter(object obj) : получает в эксклюзивное владение объект, передаваемый в качестве параметра. void Enter(object obj, bool acquiredLock) : дополнительно принимает второй параметра — логическое значение, которое указывает, получено ли владение над объектом из первого параметра
- void Exit(object obj) : освобождает ранее захваченный объект
- bool IsEntered(object obj) : возвращает true, если монитор захватил объект obj
- void Pulse (object obj) : уведомляет поток из очереди ожидания, что текущий поток освободил объект obj
- void PulseAll(object obj) : уведомляет все потоки из очереди ожидания, что текущий поток освободил объект obj. После чего один из потоков из очереди ожидания захватывает объект obj.
- bool TryEnter (object obj) : пытается захватить объект obj. Если владение над объектом успешно получено, то возвращается значение true
- bool Wait (object obj) : освобождает блокировку объекта и переводит поток в очередь ожидания объекта. Следующий поток в очереди готовности объекта блокирует данный объект. А все потоки, которые вызвали метод Wait, остаются в очереди ожидания, пока не получат сигнала от метода Monitor.Pulse или Monitor.PulseAll, посланного владельцем блокировки.
Стоит отметить, что фактически конструкция оператора lock инкапсулирует в себе синтаксис использования мониторов. Например, в прошлой теме для синхронизации потоков применялся оператор lock :
int x = 0; object locker = new(); // объект-заглушка // запускаем пять потоков for (int i = 1; i < 6; i++) < Thread myThread = new(Print); myThread.Name = $"Поток "; myThread.Start(); > void Print() < lock (locker) < x = 1; for (int i = 1; i < 6; i++) < Console.WriteLine($": "); x++; Thread.Sleep(100); > > >
Фактически данный пример будет эквивалентен следующему коду:
int x = 0; object locker = new(); // объект-заглушка // запускаем пять потоков for (int i = 1; i < 6; i++) < Thread myThread = new(Print); myThread.Name = $"Поток "; myThread.Start(); > void Print() < bool acquiredLock = false; try < Monitor.Enter(locker, ref acquiredLock); x = 1; for (int i = 1; i < 6; i++) < Console.WriteLine($": "); x++; Thread.Sleep(100); > > finally < if (acquiredLock) Monitor.Exit(locker); >>
Метод Monitor.Enter принимает два параметра — объект блокировки и значение типа bool, которое указывает на результат блокировки (если он равен true, то блокировка успешно выполнена). Фактически этот метод блокирует объект locker так же, как это делает оператор lock. А в блоке try. finally с помощью метода Monitor.Exit происходит освобождение объекта locker, если блокировка осуществлена успешно, и он становится доступным для других потоков.
оператор lock — обеспечение монопольного доступа к общему ресурсу
Оператор lock получает взаимоисключающую блокировку заданного объекта перед выполнением определенных операторов, а затем снимает блокировку. Во время блокировки поток, удерживающий блокировку, может снова поставить и снять блокировку. Любой другой поток не может получить блокировку и ожидает ее снятия. Оператор lock гарантирует, что в любой момент времени только один поток выполняет свой текст.
Оператор lock имеет форму
lock (x) < // Your code. >
Здесь x — это выражение ссылочного типа. Оно является точным эквивалентом
object __lockObj = x; bool __lockWasTaken = false; try < System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken); // Your code. >finally
Так как в коде используется try-finally оператор , блокировка освобождается, даже если в тексте lock инструкции возникает исключение.
Выражение нельзя использовать await в теле lock оператора.
Рекомендации
При синхронизации доступа потоков к общему ресурсу блокируйте выделенный экземпляр объекта (например, private readonly object balanceLock = new object(); ) или другой экземпляр, который, скорее всего, не будет использоваться как объект блокировки другими частями кода. Не используйте один и тот же экземпляр объекта блокировки для разных общих ресурсов: это может привести к взаимоблокировке или состязанию при блокировке. В частности, не используйте следующие экземпляры в качестве объектов блокировки:
- this , так как он может использоваться вызывающими объектами как блокировка;
- Type экземпляры, так как они могут быть получены оператором typeof или отражением.
- экземпляры строк, включая строковые литералы, так как они могут быть интернированы.
Удерживайте блокировку в течение максимально короткого времени, чтобы сократить число конфликтов при блокировке.
Пример
В следующем примере определяется класс Account , который синхронизирует доступ к закрытому полю balance путем блокировки выделенного экземпляра balanceLock . Использование одного и того же экземпляра для блокировки гарантирует, что balance поле не может быть обновлено одновременно двумя потоками, пытающимися вызвать Debit методы или Credit одновременно.
using System; using System.Threading.Tasks; public class Account < private readonly object balanceLock = new object(); private decimal balance; public Account(decimal initialBalance) =>balance = initialBalance; public decimal Debit(decimal amount) < if (amount < 0) < throw new ArgumentOutOfRangeException(nameof(amount), "The debit amount cannot be negative."); >decimal appliedAmount = 0; lock (balanceLock) < if (balance >= amount) < balance -= amount; appliedAmount = amount; >> return appliedAmount; > public void Credit(decimal amount) < if (amount < 0) < throw new ArgumentOutOfRangeException(nameof(amount), "The credit amount cannot be negative."); >lock (balanceLock) < balance += amount; >> public decimal GetBalance() < lock (balanceLock) < return balance; >> > class AccountTest < static async Task Main() < var account = new Account(1000); var tasks = new Task[100]; for (int i = 0; i < tasks.Length; i++) < tasks[i] = Task.Run(() =>Update(account)); > await Task.WhenAll(tasks); Console.WriteLine($"Account's balance is "); // Output: // Account's balance is 2000 > static void Update(Account account) < decimal[] amounts = [0, 2, -3, 6, -2, -1, 8, -5, 11, -6]; foreach (var amount in amounts) < if (amount >= 0) < account.Credit(amount); >else < account.Debit(Math.Abs(amount)); >> > >
Спецификация языка C#
Дополнительные сведения см. в разделе об инструкции lock в документации Предварительная спецификация C# 6.0.
См. также
- справочник по C#
- System.Threading.Monitor
- System.Threading.SpinLock
- System.Threading.Interlocked
- Обзор примитивов синхронизации
- Общие сведения о System.Threading.Channels
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
Практическое руководство. SpinLock и низкоуровневая синхронизация
В следующем примере демонстрируется использование SpinLock. В этом примере критический раздел выполняет минимальный объем работы, что делает его хорошим кандидатом для SpinLock. При небольшом увеличении работы производительность SpinLock повышается по сравнению со стандартной блокировкой. При этом в определенный момент SpinLock может оказаться дороже стандартной блокировки. Чтобы увидеть, какой тип блокировки обеспечивает наибольшую производительность в вашей программе, воспользуйтесь функцией профилирования параллелизма. Дополнительные сведения см. в разделе Визуализатор параллелизма.
class SpinLockDemo2 < const int N = 100000; static Queue_queue = new Queue(); static object _lock = new Object(); static SpinLock _spinlock = new SpinLock(); class Data < public string Name < get; set; >public double Number < get; set; >> static void Main(string[] args) < // First use a standard lock for comparison purposes. UseLock(); _queue.Clear(); UseSpinLock(); Console.WriteLine("Press a key"); Console.ReadKey(); >private static void UpdateWithSpinLock(Data d, int i) < bool lockTaken = false; try < _spinlock.Enter(ref lockTaken); _queue.Enqueue( d ); >finally < if (lockTaken) _spinlock.Exit(false); >> private static void UseSpinLock() < Stopwatch sw = Stopwatch.StartNew(); Parallel.Invoke( () =>< for (int i = 0; i < N; i++) < UpdateWithSpinLock(new Data() < Name = i.ToString(), Number = i >, i); > >, () => < for (int i = 0; i < N; i++) < UpdateWithSpinLock(new Data() < Name = i.ToString(), Number = i >, i); > > ); sw.Stop(); Console.WriteLine("elapsed ms with spinlock: ", sw.ElapsedMilliseconds); > static void UpdateWithLock(Data d, int i) < lock (_lock) < _queue.Enqueue(d); >> private static void UseLock() < Stopwatch sw = Stopwatch.StartNew(); Parallel.Invoke( () =>< for (int i = 0; i < N; i++) < UpdateWithLock(new Data() < Name = i.ToString(), Number = i >, i); > >, () => < for (int i = 0; i < N; i++) < UpdateWithLock(new Data() < Name = i.ToString(), Number = i >, i); > > ); sw.Stop(); Console.WriteLine("elapsed ms with lock: ", sw.ElapsedMilliseconds); > >
Imports System.Threading Imports System.Threading.Tasks Class SpinLockDemo2 Const N As Integer = 100000 Shared _queue = New Queue(Of Data)() Shared _lock = New Object() Shared _spinlock = New SpinLock() Class Data Public Name As String Public Number As Double End Class Shared Sub Main() ' First use a standard lock for comparison purposes. UseLock() _queue.Clear() UseSpinLock() Console.WriteLine("Press a key") Console.ReadKey() End Sub Private Shared Sub UpdateWithSpinLock(ByVal d As Data, ByVal i As Integer) Dim lockTaken As Boolean = False Try _spinlock.Enter(lockTaken) _queue.Enqueue(d) Finally If lockTaken Then _spinlock.Exit(False) End If End Try End Sub Private Shared Sub UseSpinLock() Dim sw = Stopwatch.StartNew() Parallel.Invoke( Sub() For i As Integer = 0 To N - 1 UpdateWithSpinLock(New Data() With , i) Next End Sub, Sub() For i As Integer = 0 To N - 1 UpdateWithSpinLock(New Data() With , i) Next End Sub ) sw.Stop() Console.WriteLine("elapsed ms with spinlock: ", sw.ElapsedMilliseconds) End Sub Shared Sub UpdateWithLock(ByVal d As Data, ByVal i As Integer) SyncLock (_lock) _queue.Enqueue(d) End SyncLock End Sub Private Shared Sub UseLock() Dim sw = Stopwatch.StartNew() Parallel.Invoke( Sub() For i As Integer = 0 To N - 1 UpdateWithLock(New Data() With , i) Next End Sub, Sub() For i As Integer = 0 To N - 1 UpdateWithLock(New Data() With , i) Next End Sub ) sw.Stop() Console.WriteLine("elapsed ms with lock: ", sw.ElapsedMilliseconds) End Sub End Class
SpinLock может пригодиться, если блокировка общего ресурса будет непродолжительной. В подобных случаях на многоядерных компьютерах полезно запустить заблокированный поток на несколько циклов, пока блокировка не будет снята. За счет цикличности поток не блокируется, а значит ресурсы процессора активно используются. При определенных условиях SpinLock остановит цикл, чтобы избежать истощения ресурсов логических процессоров или инверсии приоритетов в системах с технологией гиперпоточности.
Обратите внимание на использование false в вызове SpinLock.Exit. Это обеспечивает оптимальную производительность. Укажите true в архитектурах IA64, чтобы воспользоваться барьером памяти, который очищает буферы записи, обеспечивая таким образом доступность блокировки для других потоков.
См. также раздел
- Объекты и функциональные возможности работы с потоками
- Оператор lock (C#)
- Оператор SyncLock (Visual Basic)
Синхронизация потоков

При построении многопоточного приложения необходимо гарантировать, что любая часть разделяемых данных защищена от возможности изменения их значений множеством потоков. Учитывая, что все потоки в AppDomain имеют параллельный доступ к разделяемым данным приложения, представьте, что может случиться, если несколько потоков одновременно обратятся к одному и тому же элементу данных. Поскольку планировщик потоков случайным образом будет приостанавливать их работу, что если поток А будет прерван до того, как завершит свою работу? А вот что: поток В после этого прочтет нестабильные данные.
Чтобы проиллюстрировать проблему, связанную с параллелизмом, давайте рассмотрим следующий пример:
public class MyTheard < public void ThreadNumbers() < // Информация о потоке Console.WriteLine("поток использует метод ThreadNumbers",Thread.CurrentThread.Name); // Выводим числа Console.Write("Числа: "); for (int i = 0; i < 10; i++) < Random rand = new Random(); Thread.Sleep(1000*rand.Next(5)); Console.Write(i+", "); >Console.WriteLine(); > > class Program < static void Main() < MyTheard mt = new MyTheard(); // Создаем 10 потоков Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) < threads[i] = new Thread(new ThreadStart(mt.ThreadNumbers)); threads[i].Name = string.Format("Работает поток: #", i); > // Запускаем все потоки foreach (Thread t in threads) t.Start(); Console.ReadLine(); > >
Прежде чем посмотреть на тестовые запуски, давайте еще раз проясним проблему. Первичный поток внутри этого домена приложений начинает свое существование, порождая десять вторичных рабочих потоков. Каждый рабочий поток должен вызвать метод ThreadNumbers() на одном и том же экземпляре MyTheard. Учитывая, что никаких мер для блокировки разделяемых ресурсов этого объекта (консоли) не предпринималось, есть хороший шанс, что текущий поток будет отключен, прежде чем метод ThreadNumbers() сможет напечатать полные результаты. Поскольку в точности не известно, когда это может случиться (и может ли вообще), будут получаться непредвиденные результаты. Например, может появиться следующий вывод:

Ясно, что здесь присутствует определенная проблема. Как только каждый поток требует от MyTheard печати числовых данных, планировщик потоков меняет их местами в фоновом режиме. В результате получается несогласованный вывод. Для решения подобных проблем в C# используется синхронизация.
В основу синхронизации положено понятие блокировки, посредством которой организуется управление доступом к кодовому блоку в объекте. Когда объект заблокирован одним потоком, остальные потоки не могут получить доступ к заблокированному кодовому блоку. Когда же блокировка снимается одним потоком, объект становится доступным для использования в другом потоке.
Средство блокировки встроено в язык C#. Благодаря этому все объекты могут быть синхронизированы. Синхронизация организуется с помощью ключевого слова lock. Она была предусмотрена в C# с самого начала, и поэтому пользоваться ею намного проще, чем кажется на первый взгляд. В действительности синхронизация объектов во многих программах на C# происходит практически незаметно.
Ниже приведена общая форма блокировки:
lock(lockObj) < // синхронизируемые операторы >
где lockObj обозначает ссылку на синхронизируемый объект. Если же требуется синхронизировать только один оператор, то фигурные скобки не нужны. Оператор lock гарантирует, что фрагмент кода, защищенный блокировкой для данного объекта, будет использоваться только в потоке, получающем эту блокировку. А все остальные потоки блокируются до тех пор, пока блокировка не будет снята. Блокировка снимается по завершении защищаемого ею фрагмента кода.
Блокируемым считается такой объект, который представляет синхронизируемый ресурс. В некоторых случаях им оказывается экземпляр самого ресурса или же произвольный экземпляр объекта, используемого для синхронизации. Следует, однако, иметь в виду, что блокируемый объект не должен быть общедоступным, так как в противном случае он может быть заблокирован из другого, неконтролируемого в программе фрагмента кода и в дальнейшем вообще не разблокируется.
В прошлом для блокировки объектов очень часто применялась конструкция lock (this). Но она пригодна только в том случае, если this является ссылкой на закрытый объект. В связи с возможными программными и концептуальными ошибками, к которым может привести конструкция lock (this), применять ее больше не рекомендуется. Вместо нее лучше создать закрытый объект, чтобы затем заблокировать его.
Давайте модифицируем предыдущий пример добавив в него синхронизацию:
public class MyTheard < private object threadLock = new object(); public void ThreadNumbers() < // Используем маркер блокировки lock (threadLock) < // Информация о потоке Console.WriteLine("поток использует метод ThreadNumbers", Thread.CurrentThread.Name); // Выводим числа Console.Write("Числа: "); for (int i = 0; i < 10; i++) < Random rand = new Random(); Thread.Sleep(1000 * rand.Next(5)); Console.Write(i + ", "); >Console.WriteLine(); > > > .
Как только поток войдет в контекст lock, маркер блокировки (в данном случае — текущий объект) станет недоступным другим потокам до тех пор, пока блокировка не будет снята по выходе из контекста lock. Таким образом, если поток А захватит маркер блокировки, другие потоки не смогут войти ни в один из контекстов, использующих тот же маркер, до тех пор, пока поток А не освободит его.
Чтобы блокировать код в статическом методе, нужно объявить приватную статическую переменную-член, которая будет служить в качестве маркера блокировки. Если теперь запустить приложение, можно увидеть, что каждый поток получил возможность выполнить свою работу до конца: