оператор 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, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
Lock c как работает
Нередко в потоках используются некоторые разделяемые ресурсы, общие для всей программы. Это могут быть общие переменные, файлы, другие ресурсы. Например:
int x = 0; // запускаем пять потоков for (int i = 1; i < 6; i++) < Thread myThread = new(Print); myThread.Name = $"Поток "; // устанавливаем имя для каждого потока myThread.Start(); > void Print() < x = 1; for (int i = 1; i < 6; i++) < Console.WriteLine($": "); x++; Thread.Sleep(100); > >
Здесь у нас запускаются пять потоков, которые вызывают метод Print и которые работают с общей переменной x. И мы предполагаем, что метод выведет все значения x от 1 до 5. И так для каждого потока. Однако в реальности в процессе работы будет происходить переключение между потоками, и значение переменной x становится непредсказуемым. Например, в моем случае я получил следующий консольный вывод (он может в каждом конкретном случае различаться):
Поток 1: 1 Поток 5: 1 Поток 4: 1 Поток 2: 1 Поток 3: 1 Поток 1: 6 Поток 5: 7 Поток 3: 7 Поток 2: 7 Поток 4: 9 Поток 1: 11 Поток 4: 11 Поток 2: 11 Поток 3: 14 Поток 5: 11 Поток 1: 16 Поток 2: 16 Поток 3: 16 Поток 5: 18 Поток 4: 16 Поток 1: 21 Поток 5: 21 Поток 3: 21 Поток 2: 21 Поток 4: 21
Решение проблемы состоит в том, чтобы синхронизировать потоки и ограничить доступ к разделяемым ресурсам на время их использования каким-нибудь потоком. Для этого используется ключевое слово lock . Оператор 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); > > >
Для блокировки с ключевым словом lock используется объект-заглушка, в данном случае это переменная locker . Обычно это переменная типа object. И когда выполнение доходит до оператора lock, объект locker блокируется, и на время его блокировки монопольный доступ к блоку кода имеет только один поток. После окончания работы блока кода, объект locker освобождается и становится доступным для других потоков.
В этом случае консольный вывод будет более упорядоченным:
Поток 1: 1 Поток 1: 2 Поток 1: 3 Поток 1: 4 Поток 1: 5 Поток 5: 1 Поток 5: 2 Поток 5: 3 Поток 5: 4 Поток 5: 5 Поток 3: 1 Поток 3: 2 Поток 3: 3 Поток 3: 4 Поток 3: 5 Поток 2: 1 Поток 2: 2 Поток 2: 3 Поток 2: 4 Поток 2: 5 Поток 4: 1 Поток 4: 2 Поток 4: 3 Поток 4: 4 Поток 4: 5
Lock c как работает
Еще один инструмент управления синхронизацией потоков представляет класс Mutex или мьютекс, который также располагается в пространстве имен System.Threading .
Так, возьмем пример с оператором 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; Mutex mutexObj = new(); // запускаем пять потоков for (int i = 1; i < 6; i++) < Thread myThread = new(Print); myThread.Name = $"Поток "; myThread.Start(); > void Print() < mutexObj.WaitOne(); // приостанавливаем поток до получения мьютекса x = 1; for (int i = 1; i < 6; i++) < Console.WriteLine($": "); x++; Thread.Sleep(100); > mutexObj.ReleaseMutex(); // освобождаем мьютекс >
Сначала создаем объект мьютекса:
Mutex mutexObj = new Mutex()
Основную работу по синхронизации выполняют методы WaitOne() и ReleaseMutex() . Метод mutexObj.WaitOne() приостанавливает выполнение потока до тех пор, пока не будет получен мьютекс mutexObj.
Изначально мьютекс свободен, поэтому его получает один из потоков.
После выполнения всех действий, когда мьютекс больше не нужен, поток освобождает его с помощью метода mutexObj.ReleaseMutex() . А мьютекс получает один из ожидающих потоков.
Таким образом, когда выполнение дойдет до вызова mutexObj.WaitOne() , поток будет ожидать, пока не освободится мьютекс. И после его получения продолжит выполнять свою работу.
C++ Mutex Lock


Программирование и разработка
На чтение 5 мин Просмотров 355 Опубликовано 05.11.2022
C++ известен как один из самых быстрых языков программирования с хорошей производительностью, высокой точностью и адекватной системой управления памятью. Этот язык программирования также поддерживает одновременное выполнение нескольких потоков с разделением между ними нескольких ресурсов. В многопоточности поток должен выполнять только операцию чтения, которая не создает проблем, поскольку на поток не влияет то, что в это время делают другие потоки. Но если эти потоки должны были разделить ресурсы между собой, один поток может изменить данные в то время, что создает проблему. Чтобы решить эту проблему, у нас есть «мьютекс» C++, который предотвращает доступ нескольких ресурсов к нашему коду/объекту, обеспечивая синхронизацию, в которой говорится, что доступ к объекту/коду может быть предоставлен только одному потоку одновременно,
Процедура
Мы узнаем, как мы можем остановить доступ нескольких потоков к объекту одновременно, используя блокировку мьютекса. Мы поговорим о синтаксисе блокировки мьютекса, о том, что такое многопоточность и как мы можем решать проблемы, вызванные многопоточностью, используя блокировку мьютекса. Затем мы возьмем пример многопоточности и реализуем для них блокировку мьютекса.
Синтаксис
Если мы хотим узнать, как мы можем реализовать блокировку мьютекса, чтобы предотвратить одновременный доступ нескольких потоков к нашему объекту или коду, мы можем использовать следующий синтаксис:
$ std :: mutex mut_x
$ mut_x. lock ( ) ;
Void func_name ( )
$ // code we want to hide from the multiple threads would be written here
$ mut_x. unlocks ( ) ;
>
Теперь мы будем использовать этот синтаксис в фиктивном примере и в псевдокоде (который мы не можем просто запустить в редакторе кода), чтобы вы знали, как именно мы можем использовать этот синтаксис, как указано ниже:
Пример
В этом примере попробуем сначала создать многопоточную операцию, а затем окружить эту операцию блокировкой и разблокировкой мьютекса, чтобы обеспечить синхронизацию операции с созданным кодом или объектом. Mutex имеет дело с условиями гонки, которые представляют собой значения, которые совершенно непредсказуемы и зависят от переключения потоков, которые учитывают время. Чтобы реализовать пример для мьютекса, нам сначала нужно импортировать важные и необходимые библиотеки из репозиториев. Требуемые библиотеки:
Библиотека «iostream» предоставляет нам функцию для отображения данных как Cout, чтения данных как Cin и завершения оператора как endl. Мы используем библиотеку «потоков», чтобы использовать программы или функции из потоков. Библиотека «mutex» позволяет реализовать в коде как блокировку, так и разблокировку мьютекса. Мы используем «# include», потому что это позволяет включать все программы, связанные с библиотекой, в код.
Теперь, после выполнения предыдущего шага, мы определяем класс мьютекса или глобальную переменную для мьютекса, используя метод std. Затем мы создаем функцию для блокировки и разблокировки мьютекса, которую мы можем вызвать позже в коде. В этом примере мы называем эту функцию блоком. В теле функции блока мы сначала вызываем «mutex.lock()» и начинаем писать логику кода.
mutex.lock() запрещает другим потокам доступ к нашему созданному объекту или коду, так что только один поток может читать наш объект одновременно. В логике мы запускаем цикл for, который работает с индексом от 0 до 9. Мы отображаем значения в цикле. Как только эта логика создается в замке мьютекса после завершения его работы или после выхода из логики, мы вызываем метод «mutex.unlock()». Этот вызов метода позволяет нам разблокировать созданный объект от блокировки мьютекса, поскольку доступ объекта к одному единственному потоку был предоставлен ранее и после того, как операция над этим объектом выполняется одним потоком за один раз. Теперь мы хотим, чтобы другие потоки также имели доступ к этому объекту или коду. В противном случае, наш код перемещается в ситуацию «тупика», из-за которой созданный объект с мьютексом навсегда остается в заблокированной ситуации, и никакой другой поток не сможет получить доступ к этому объекту. Следовательно, незавершенная операция продолжает выполняться. После этого выходим из функции блока и переходим к главной.
В основном мы просто отображаем наш созданный мьютекс, создавая три потока, используя «std::thread thread_name (вызывая здесь уже созданную функцию блока, в которой мы создали мьютекс)» с именами thread1, thread2 и thread3 и т. д. Таким образом создаются три потока. Затем мы объединяем эти три потока для одновременного выполнения, вызывая файл «thread_name. присоединиться ()». И затем, мы возвращаем значение, равное нулю. Ранее упомянутое объяснение примера реализовано в виде кода, который можно показать на следующем рисунке:

В выводе кода мы видим выполнение и отображение всех трех потоков один за другим. Мы можем видеть, даже если наше приложение подпадает под категорию многопоточности. Тем не менее, ни один из потоков не перезаписал или не изменил данные и не поделился измененным ресурсом из-за реализации мьютекса «функционального блока».
Заключение
В этом руководстве дается подробное объяснение концепции функции мьютекса, используемой в C++. Мы обсудили, что такое многопоточные приложения, с какими проблемами приходится сталкиваться в многопоточных приложениях и почему нам необходимо реализовать мьютекс для многопоточных приложений. Затем мы обсудили синтаксис мьютекса на фиктивном примере с использованием псевдокода. Затем мы реализовали полный пример многопоточных приложений с мьютексом в Visual Studio C++.