Lock c как работает
Перейти к содержимому

Lock c как работает

Инструкция lock. (Справочник по C#)

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

Оператор lock имеет форму

Здесь x — это выражение ссылочного типа. Оно является точным эквивалентом

Так как в коде используется блок try. finally, блокировка освобождается, даже если возникает исключение в теле оператора lock .

Нельзя использовать оператор await в теле оператора lock .

Рекомендации

При синхронизации доступа потоков к общему ресурсу блокируйте выделенный экземпляр объекта (например, private readonly object balanceLock = new object(); ) или другой экземпляр, который, скорее всего, не будет использоваться как объект блокировки другими частями кода. Не используйте один и тот же экземпляр объекта блокировки для разных общих ресурсов: это может привести к взаимоблокировке или состязанию при блокировке. В особенности избегайте использования следующих объектов в качестве объектов блокировки:

  • this , так как он может использоваться вызывающими объектами как блокировка;
  • экземпляров Type, так как их может получать оператор typeof или отражение;
  • строковых экземпляров, включая строковые литералы, так как они могут быть интернированы.

Удерживайте блокировку в течение максимально короткого времени, чтобы сократить число конфликтов при блокировке.

Пример

В следующем примере определяется класс Account , который синхронизирует доступ к закрытому полю balance путем блокировки выделенного экземпляра balanceLock . Использование того же экземпляра для блокировки гарантирует, что поле balance не смогут одновременно обновить два потока, пытающиеся вызвать методы Debit или Credit одновременно.

Спецификация языка C#

Дополнительные сведения см. в разделе об инструкции lock в документации Предварительная спецификация C# 6.0.

Lock c как работает

Нередко в потоках используются некоторые разделяемые ресурсы, общие для всей программы. Это могут быть общие переменные, файлы, другие ресурсы. Например:

Здесь у нас запускаются пять потоков, которые вызывают метод Print и которые работают с общей переменной x. И мы предполагаем, что метод выведет все значения x от 1 до 5. И так для каждого потока. Однако в реальности в процессе работы будет происходить переключение между потоками, и значение переменной x становится непредсказуемым. Например, в моем случае я получил следующий консольный вывод (он может в каждом конкретном случае различаться):

Решение проблемы состоит в том, чтобы синхронизировать потоки и ограничить доступ к разделяемым ресурсам на время их использования каким-нибудь потоком. Для этого используется ключевое слово lock . Оператор lock определяет блок кода, внутри которого весь код блокируется и становится недоступным для других потоков до завершения работы текущего потока. Остальный потоки помещаются в очередь ожидания и ждут, пока текущий поток не освободит данный блок кода. В итоге с помощью lock мы можем переделать предыдущий пример следующим образом:

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

Многопоточность и блокировка в c#

Зачем в операторе lock мы указываем какой-то объект? Не могу понять. Ясно, что этот оператор блокирует определённый блок кода для других потоков, однако зачем указывать какой-то объект для этого? Что это даёт? Что он вообще делает с этим объектом, который я ему передал?

user avatar

user avatar

И два метода в том же классе, где и locker:

Важно, что в конструкции lock используется один и то тот же объект locker. Теперь пускаем методы lWrite и lRead каждый в своем потоке, один в цикле добавляет элементы в лист, другой выводит количество элементом листа на экран.

Происходит при работе следующее: допустим, первый поток t1 первым доходит до lock, тогда он ставит блокировку на этом объекте и выполняет блок кода, вложенный в lock, в данном случае добавляет элемент в лист. Допустим, t1 у нас сейчас находится в состоянии добавления элемента, и в это же время поток t2 доходит до блока lock в своем куске кода, смотрит на объект блокировки и видит, что на объекте locker уже выставлена блокировка, пока она там есть, войти в вложенный в lock код поток не может и становится на ожидание. Как только t1 доделает свое добавление, т.е. код в блоке lock завершится, он снимает блокировку с объекта locker. Поток t2 видит, что блокировка снята, заходит в блок lock, ставит на объект locker свою блокировку и выполняет вложенные команды.
Т.е. конструкция lock проверяет, есть ли на указанном объекте блокировка, если нет, то ставит блокировку, выполняет вложенные инструкции, затем снимает блокировку. Если есть, то останавливает поток, ожидая, когда блокировка будет снята, и затем проделывает описанное выше, ставит свою блокировку и т.д. Код, заключенный в lock, не важен, важно состояние объекта блокировки, в данном случае locker. Допустим, в данном примере в методе lRead можно делать что-то абсолютно несвязанное со списком list, пусть это и лишено в данном случае смысла. Суть в том, что делать что-то заключенное в lock будем, только когда на объекте будет снята блокировка.

Если добавить в начало:

и переписать lRead:

то при запуске циклов t1 и t2 никакой синхронизации происходить не будет, ибо каждый поток вызывает блокировку на своем собственном объекте блокировки. Они никак не связаны получаются и никакой синхронизации не будет.

Еще момент: одним и тем же должен быть именно объект блокировки, а не одинаковый член класса.

Допустим, имеем класс:

Далее создаем объекты и опять пускаем циклы:

То никакой блокировки при работе методов Doing возникать не будет, ибо locker в данном случае у каждого класса свой. А вот если сделать так:

то мы потоки t2 и t3 будут ссылаться на один и тот же объект, и методы Doing и DoingElse будут ждать друг друга.

Если надо синхронизировать разные объекты, то можно, например, ввести в родительский класс статическую переменную и использовать ее в качестве объекта блокировки. Допустим, все это вызывалось в классе Program, то кидаем в него:

Далее в Test правим методы:

И тогда разные экземпляры:

Будут ссылаться на один и тот же объект, и синхронизация будет происходить.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *