Начал изучать замок и сразу возник вопрос.

Здесь docs.microsoft говорится:

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

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

Вопросы :

  1. Правильно ли я понимаю, что блокировка экземпляра в одном потоке позволяет изменять данные из другого потока в этом экземпляре, если только этот другой поток также не использует блокировку этого экземпляра? Если да, то что тогда вообще дает такая блокировка и почему она делается именно так?

  2. Что это значит, говоря простым языком? Пока блокировка удерживается, поток, удерживающий блокировку, может снова получить и снять блокировку.


Так что форматирование кода работает хорошо.


using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class A
    {
        public int a;
    }

    class Program
    {
        static void Main(string[] args)
        {
            A myA = new A();

            void MyMethod1()
            {
                lock (myA)
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Thread.Sleep(500);
                        myA.a += 1;
                        Console.WriteLine($"Work MyMethod1 a = {myA.a}");
                    }
                }                               
            }

            void MyMethod2()
            {
                //lock (myA)
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Thread.Sleep(500);
                        myA.a += 100;
                        Console.WriteLine($"Work MyMethod2 a = {myA.a}");
                    }
                }                
            }

            Task t1 = Task.Run(MyMethod1);
            Thread.Sleep(100);
            Task t2 = Task.Run(MyMethod2);

            Task.WaitAll(t1, t2);

        }
    }
}
0
NikVladi 26 Янв 2022 в 13:15
Представьте, что у вас есть ключ для открытия двери, а за дверью кошка, которую вы хотите погладить. При запирании (MyMethod1) один человек получает ключ, открывает дверь, гладит кошку, а затем возвращает ключ. Без запирания (MyMethod2) у вас нет ни двери, ни ключа. Вы можете просто пойти и погладить кошку, даже когда другие входят в дверь, потому что вас ничто не сдерживает. При блокировке вы не блокируете саму кошку — вы блокируете доступ к ней.
 – 
imsmn
26 Янв 2022 в 13:28
Представьте, что прорезь Kensington существует на каждом предмете вокруг вас. Любой может попытаться поместить свой собственный замок в этот слот, и единственное, что их остановит, это если кто-то другой первым поместит туда свой собственный замок. Но обратите внимание, что наличие слота не влияет на нормальное использование объекта.
 – 
Damien_The_Unbeliever
26 Янв 2022 в 13:36
why is it done this way? Потому что именно так был разработан язык. Теоретически язык можно спроектировать таким образом, что если вы заблокируете поле в одном месте, оно будет автоматически заблокировано везде, где оно используется, но я не знаю ни одного языка, который делает это.
 – 
Matthew Watson
26 Янв 2022 в 13:46

2 ответа

Лучший ответ

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

private object myLockObject = new object();
private int a;
private int b;

public void TransferMonety(int amount){
    lock(myLockObject){
         if(a > amount){
             a-=amount;
             b+=amount;
        }
    }
}

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

Из-за этого важно быть осторожным при использовании замков. Блокировки предпочтительно должны быть закрытыми, чтобы избежать захвата блокировки каким-либо несвязанным кодом. Код внутри блокировки должен быть достаточно коротким и не должен вызывать какой-либо код вне класса. Это делается для того, чтобы избежать тупиковых ситуаций, если запущен произвольный код, он может делать такие вещи, как захват других блокировок или ожидание событий.

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

2
JonasH 26 Янв 2022 в 13:42
Спасибо за ответ. Про кооперативные блокировки экземпляра мне стало понятнее. Другими словами, использование блокировки — это своего рода «договоренность» некоторых сущностей в разных потоках о том, что эти сущности обязаны что-то делать с экземпляром внутри условной очереди, но не одновременно. И их согласие вовсе не означает, что другая сущность, в которой не применяется блокировка по отношению к данному экземпляру, не сможет влиять на экземпляр. Если я правильно понимаю.
 – 
NikVladi
26 Янв 2022 в 15:12

Что это значит, говоря простым языком? "Пока блокировка удерживается, поток, удерживающий блокировку, может снова получить и снять блокировку".

Значит, вы можете это сделать:

lock (locker)
{
    lock (locker)
    {
        lock (locker)
        {
            // Do something while holding the lock
        }
    }
}

Вы можете получить блокировку много раз, а затем снять ее столько же раз. Это называется повторным входом. Оператор lock является реентерабельным, поскольку лежащий в его основе Класс Monitor является реентерабельным по своей природе. Другие примитивы синхронизации, такие как SemaphoreSlim, не реентерабельный.

1
Theodor Zoulias 26 Янв 2022 в 15:04
Спасибо очень благодарен!
 – 
NikVladi
26 Янв 2022 в 15:13