0

I began to study lock and immediately a question arose.

It docs.microsoft says here:

The lock statement acquires the mutual-exclusion lock for a given object, executes a statement block, and then releases the lock. While a lock is held, the thread that holds the lock can again acquire and release the lock. Any other thread is blocked from acquiring the lock and waits until the lock is released.

I made a simple example proving that another thread with a method without the lock keyword can easily change the data of an instance while that instance is occupied by a method using the lock from the first thread. It is worth removing the comment from the blocking and the work is done as expected. I thought that a lock would block access to an instance from other threads, even if they don't use a lock on that instance in their methods.

Questions:

  1. Do I understand correctly that locking an instance on one thread allows data from another thread to be modified on that instance, unless that other thread also uses that instance's lock? If so, what then does such a blocking generally give and why is it done this way?

  2. What does this mean in simpler terms? While a lock is held, the thread that holds the lock can again acquire and release the lock.


So code formatting works well.


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);

        }
    }
}
  • Imagine having a key for opening a door, behind the door there's a cat you want to pet. With locking (`MyMethod1`), one person at a time gets the key, opens the door, pets the cat and then returns the key. Without locking (`MyMethod2`), you don't have a door or a key. You can simply go ahead and pet the cat - even while others go through the door because nothing holds you back. With locking, you don't lock the cat itself - you lock the access to the cat. – devsmn Jan 26 '22 at 10:28
  • Picture a [Kensington slot](https://en.wikipedia.org/wiki/Kensington_Security_Slot) exists on every object around you. Anyone can try and place their own lock into that slot and the only thing that stops them is if someone else put there own lock there first. But notice that the existence of the slot *doesn't affect any normal use of the object*. – Damien_The_Unbeliever Jan 26 '22 at 10:36
  • `why is it done this way?` Because that's how the language was designed. In theory a language could be designed so that if you lock a field in one place, it is automatically locked everywhere else it's used, but I'm not aware of any languages that do that. – Matthew Watson Jan 26 '22 at 10:46

2 Answers2

1

locks are cooperative, it relies on all parties that can change the data to cooperate and take the lock before attempting to change the data. Note that the lock does not care what you are changing inside the lock. It is fairly common to use a surrogate lock object when protecting some data structure. I.e.

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;
        }
    }
}

Because of this locks are very flexible, you can protect any kind of operation, but you need to write your code correctly.

Because of this it is important to be careful when using locks. Locks should preferably be private to avoid any unrelated code from taking the lock. The code inside the lock should be fairly short, and should not call any code outside the class. This is done to avoid deadlocks, if arbitrary code is run it may do things like taking other locks or waiting for events.

While locks are very useful, there are also other synchronization primitives that can be used depending on your use case.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Thanks for the answer. About cooperative locks of an instance it became clearer to me. In other words, the use of a lock is a kind of "agreement" of some entities in different threads that these entities are required to do something with the instance within the conditional queue, but not simultaneously. And their agreement does not mean at all that another entity in which the lock is not applied in relation to this instance will not be able to influence the instance. If I understand correctly. –  Jan 26 '22 at 12:12
  • @NikVladi sounds about right. Note that threads waiting on a lock are not ordered, when the lock is released any of the waiting threads take it. – JonasH Jan 26 '22 at 15:02
0

What does this mean in simpler terms? "While a lock is held, the thread that holds the lock can again acquire and release the lock."

It means that you can do this:

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

You can acquire the lock many times, and then release it an equal number of times. This is called reentrancy. The lock statement is reentrant, because the underlying Monitor class is reentrant by design. Other synchronization primitives, like the SemaphoreSlim, are not reentrant.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104