开发者

How to lock several objects?

开发者 https://www.devze.com 2023-03-05 21:08 出处:网络
I want to lock on two objects at the same time. Why can\'t I write like such code? lock (obj1, obj2) Should I always write like that?

I want to lock on two objects at the same time. Why can't I write like such code?

lock (obj1, obj2)

Should I always write like that?

lock (obj1)
{
    lock (obj2)
开发者_StackOverflow中文版    {
    }
}

Probably this could be made simpler? Likely it would be better to introduce special private object, and use it for a lock...


Well, this question is way too old but, here is a compact one I figured out, both codes will end up to the same compiled statements (this and the one in the question description):

    lock (obj1) lock (obj2)
    {
        // your code
    }


That is the correct way to lock on multiple objects, yes.

My guess is that the reason for only allowing a single argument to the lock statement is to make the order in which locks are taken as clear as possible.

Note that you must see to it that the two locks are taken in the same order everywhere in your code, or you have a potential for deadlocks.

You could also, as you suggest, introduce a single dedicated lock object, but that would make your locking more coarse. It all depends on your needs. If you sometimes only need one of the locks, you should keep them separate (but make sure to preserve lock ordering, as mentioned above).


If you write code like this, you need to make sure, that you always lock those two objects in this order. Otherwise, you might run into deadlocks.


I have encountered the same kind of problem, and wrote this snippet that might help you out, even though it is far from perfect :

private void MultiLock(object[] locks, WaitCallback pFunc, int index = 0)
{
    if (index < locks.Count())
    {
        lock (locks[index])
        {
            MultiLock(locks, pFunc, index + 1);
        }
    }
    else
    {
        ThreadPool.QueueUserWorkItem(pFunc);
    }
}

And then, just call this method like this :

public object LockedObject1 = new Object();
public object LockedObject2 = new Object();

public void MyFunction(object arg)
{
    WaitCallback pFunc = delegate
    {
        // Operations on locked objects
    }

    MultiLock(new object[] {LockedObject1, LockedObject2}, pFunc);
}


The reason you have to do it as you wrote it, is because you can't lock two objects in the same time; You lock them one after the other (and it is very important to keep the order of the lock, otherwise you might run in to deadlocks), and it's better to be as explicit as you can with these things.


Do something like

    internal static void DuoEnter(object po1, object po2, int pnTimeOutMs = 1000)
    {
        if ((po1 == null) && (po2 == null))
            return;
        int nMaxLoops = 100 * pnTimeOutMs;
        bool lOneProcessor = Environment.ProcessorCount < 2;
        for (int nLoops = 0; nLoops < nMaxLoops; nLoops++)
        {
            if ((po1 == null) || (po2 == null) || (po1 == po2))
            {
                if (Monitor.TryEnter(po1 ?? po2))
                    return;
            }
            else
            {
                if (Monitor.TryEnter(po1))
                    if (Monitor.TryEnter(po2))
                        return;
                    else
                        Monitor.Exit(po1);
            }
            if (lOneProcessor || (nLoops % 100) == 99)
                Thread.Sleep(1); // Never use Thread.Sleep(0)
            else
                Thread.SpinWait(20);
        }
        throw new TimeoutException(
            "Waited more than 1000 mS trying to obtain locks on po1 and po2");
    }

    internal static void DuoExit(object po1, object po2)
    {
        if ((po1 == null) && (po2 == null))
            return;
        if (po1 == null || po2 == null || po1 == po2)
            Monitor.Exit(po2 ?? po1);
        else
        {
            Monitor.Exit(po2);
            Monitor.Exit(po1);
        }
    } 


Short answer:

So in terms of engineering the difference is between when you want to make object to count as a single lock and only for that, and you do not want to allow other programmers execute any code between the lock happens, you should use in-line locking, it will prevent to put the code between locks. On the other side if you want to achieve opposite effect and allow to modify collections or execute something between the locks, then you should use nested locks. But in technical terms the will compile to the same IL code. So, the difference exist, but it isn't technical. It's placed on the precompile time, when you are writing the code.

Detailed answer:

As far as I know inside the lock it works like reference equality. If references are equal than it means it locked on the same object. That's why value types doesn't allowed to be locked as well as dynamic object (because they may change what they are referencing to and you will lost your lock).

On the other hand the underlying mechanism behind the compilation for 2021th is the same, so it's only a sugar of how you would like to use it, but each sugar has it's own cost and this isn't an exclusion.

I would like to share a little snippet of my code to improve the understand of underlying locking mechanics

private class Tes
        {
            private object lock1 = new object();
            private object lock2 = new object();
            public void Test()
            {
                lock(lock2) lock (this.lock1)
                {
                    Console.WriteLine("lol");
                }
                lock(lock2)
                {
                    lock(lock1)
                    {
                        Console.WriteLine("lol2");
                    }
                }
            }
        }
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            var k = new Tes();
            k.Test();

        }
}

Under the hood it's compiled to IL (as known as CIL - common interpreted language) which is compiled to machine instructions, so we assume if IL snippets are match than execution is the same.

This is how it is compiled. The first block of locks

    lock(lock2) lock (this.lock1)
      {
        Console.WriteLine("lol");
      }

Compiled into

IL_0001: ldarg.0      // this
      IL_0002: ldfld        object ConsoleApp1.Program/Tes::lock2
      IL_0007: stloc.0      // V_0
      IL_0008: ldc.i4.0
      IL_0009: stloc.1      // V_1
      .try
      {
        IL_000a: ldloc.0      // V_0
        IL_000b: ldloca.s     V_1
        IL_000d: call         void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
        IL_0012: nop

        // [13 17 - 13 34]
        IL_0013: ldarg.0      // this
        IL_0014: ldfld        object ConsoleApp1.Program/Tes::lock1
        IL_0019: stloc.2      // V_2
        IL_001a: ldc.i4.0
        IL_001b: stloc.3      // V_3
        .try
        {
          IL_001c: ldloc.2      // V_2
          IL_001d: ldloca.s     V_3
          IL_001f: call         void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
          IL_0024: nop

          // [14 5 - 14 6]
          IL_0025: nop

          // [15 6 - 15 31]
          IL_0026: ldstr        "lol"
          IL_002b: call         void [System.Console]System.Console::WriteLine(string)
          IL_0030: nop

          // [16 5 - 16 6]
          IL_0031: nop
          IL_0032: leave.s      IL_003f
        } // end of .try
        finally
        {

          IL_0034: ldloc.3      // V_3
          IL_0035: brfalse.s    IL_003e
          IL_0037: ldloc.2      // V_2
          IL_0038: call         void [System.Threading]System.Threading.Monitor::Exit(object)
          IL_003d: nop

          IL_003e: endfinally
        } // end of finally

        IL_003f: leave.s      IL_004c
      } // end of .try
      finally
      {

        IL_0041: ldloc.1      // V_1
        IL_0042: brfalse.s    IL_004b
        IL_0044: ldloc.0      // V_0
        IL_0045: call         void [System.Threading]System.Threading.Monitor::Exit(object)
        IL_004a: nop

        IL_004b: endfinally
      } // end of finally

And the second block of c# code

lock(lock2)
    {
        lock(lock1)
            {
                Console.WriteLine("lol2");
            }
    }

Compiled into same IL (compare it to ensure, I have compared, but it will give you a deeper understanding of what's going on)

 // [17 5 - 17 16]
      IL_004c: ldarg.0      // this
      IL_004d: ldfld        object ConsoleApp1.Program/Tes::lock2
      IL_0052: stloc.s      V_4
      IL_0054: ldc.i4.0
      IL_0055: stloc.s      V_5
      .try
      {
        IL_0057: ldloc.s      V_4
        IL_0059: ldloca.s     V_5
        IL_005b: call         void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
        IL_0060: nop

        // [18 5 - 18 6]
        IL_0061: nop

        // [19 6 - 19 17]
        IL_0062: ldarg.0      // this
        IL_0063: ldfld        object ConsoleApp1.Program/Tes::lock1
        IL_0068: stloc.s      V_6
        IL_006a: ldc.i4.0
        IL_006b: stloc.s      V_7
        .try
        {
          IL_006d: ldloc.s      V_6
          IL_006f: ldloca.s     V_7
          IL_0071: call         void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
          IL_0076: nop

          // [20 6 - 20 7]
          IL_0077: nop

          // [21 7 - 21 33]
          IL_0078: ldstr        "lol2"
          IL_007d: call         void [System.Console]System.Console::WriteLine(string)
          IL_0082: nop

          // [22 6 - 22 7]
          IL_0083: nop
          IL_0084: leave.s      IL_0093
        } // end of .try
        finally
        {

          IL_0086: ldloc.s      V_7
          IL_0088: brfalse.s    IL_0092
          IL_008a: ldloc.s      V_6
          IL_008c: call         void [System.Threading]System.Threading.Monitor::Exit(object)
          IL_0091: nop

          IL_0092: endfinally
        } // end of finally

        // [23 5 - 23 6]
        IL_0093: nop
        IL_0094: leave.s      IL_00a3
      } // end of .try
      finally
      {

        IL_0096: ldloc.s      V_5
        IL_0098: brfalse.s    IL_00a2
        IL_009a: ldloc.s      V_4
        IL_009c: call         void [System.Threading]System.Threading.Monitor::Exit(object)
        IL_00a1: nop

        IL_00a2: endfinally
      } // end of finally

The only difference is inside the

IL_0007: stloc.0

function, but this is only a getter from top of the stack, this is based on where the code is placed, since I have moved all my code into one class and executed it synchronously - it's all on the stack.

But the meaning of technical matching doesn't mean it is the same in the practice, because you are unable to put the logs between single line locks, so you cannot ensure where you are right know

lock(obj1) lock(obj2) { Console.WriteLine(""); } //you are sure after both, but //you are unable to catch the space between them

And another way to handle middle space between the locks

lock(obj1){
  Console.WriteLine("do something after first lock");
  lock(obj2) {
    //you are clearly know when the first lock and the second lock appers
  }
}


Instead of locking the objects themselves, you create a dedicated object called PadLock or similar and only lock that one where it is needed.


Locking here doesn't mean that for duration of the lock no other code on other thread can access or modify the object. If you lock an object any other thread can modify the object at the same time. What lock code block allows you to do is to make the code inside the lock block to be single entry i.e only one thread can execute the lock code block once and other threads who tries to execute the same code block will have to wait till the owner thread is done with executing the code block. So basically you really don't need to lock 2 or more objects in usual cases. By locking your purpose is to make the code block single entry

0

精彩评论

暂无评论...
验证码 换一张
取 消