Home > Software engineering >  Why can threads change instance data if it is blocked in another thread?
Why can threads change instance data if it is blocked in another thread?

Time:01-27

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

        }
    }
}

CodePudding user response:

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.

CodePudding user response:

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.

  •  Tags:  
  • Related