如何锁定多个对象

本文关键字:对象 锁定 何锁定 | 更新日期: 2023-09-27 17:56:06

我想同时锁定两个对象。为什么我不能像这样的代码一样编写?

lock (obj1, obj2)

我应该一直这样写吗?

lock (obj1)
{
    lock (obj2)
    {
    }
}

也许这可以变得更简单?可能最好引入特殊的私人对象,并将其用于锁......

如何锁定多个对象

好吧,这个问题太老了,但是,这是我发现的一个紧凑的问题,两个代码最终将得到相同的编译语句(这个和问题描述中的那个):

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

这是锁定多个对象的正确方法,是的。

我的猜测是,只允许对 lock 语句使用单个参数的原因是使锁的顺序尽可能清晰。

请注意,您必须确保两个锁在代码中的任何地方都以相同的顺序使用,否则可能会出现死锁。

您也可以按照您的建议引入单个专用锁定对象,但这会使您的锁定更加粗糙。这完全取决于您的需求。如果您有时只需要其中一个锁,则应将它们分开(但请确保保留锁顺序,如上所述)。

如果你写这样的代码,你需要确保,你总是按这个顺序锁定这两个对象。否则,您可能会遇到死锁。

我遇到了同样的问题,并写了这个可能对你有所帮助的片段,即使它远非完美:

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

然后,只需像这样调用此方法:

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

你必须按照你写的去做的原因是,你不能同时锁定两个对象;你一个接一个地锁定它们(保持锁的顺序非常重要,否则你可能会遇到死锁),最好尽可能明确地处理这些事情。

做类似的事情

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

简短回答:

所以在工程方面区别在于,当你想让对象算作单个锁和只为那个,和你不想让其他程序员执行任何代码之间的锁发生,你应该使用内联锁,它会阻止把代码放在锁之间。另一方面,如果您想达到相反的效果并允许修改集合或在锁之间执行某些操作,那么您应该使用嵌套锁。但从技术上讲,将编译为相同的 IL 代码。所以,差异是存在的,但不是技术性的。它放置在编写代码时的预编译时间。

详细答案:

据我所知,在锁内部,它的工作原理类似于参考相等。如果引用等于,则表示它锁定在同一对象上。这就是为什么值类型不允许像动态对象一样被锁定(因为它们可能会更改它们所引用的内容,您将失去锁)。

另一方面,2021 年汇编背后的潜在机制是相同的,所以它只是你想如何使用它的糖,但每种糖都有自己的成本,这不是排除。

我想分享我的代码片段,以提高对底层锁定机制的理解

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

在后台,它被编译为 IL(称为 CIL - 通用解释语言),该语言被编译为机器指令,因此我们假设如果 IL 代码段匹配,则执行是相同的。

这就是它的编译方式。第一块锁

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

编译成

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

第二个 c# 代码块

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

编译到同一个 IL 中(比较它以确保,我已经比较过了,但它会让你更深入地了解正在发生的事情)

 // [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

唯一的区别是在内部

IL_0007: stloc.0

函数,但这只是堆栈顶部的 getter,这是基于代码的放置位置,因为我已将所有代码移动到一个类中并同步执行 - 它们都在堆栈上。

但技术匹配的意义并不意味着在实践中是一样的,因为你无法将日志放在单行锁之间,所以你无法确保你在哪里是正确的知道

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

还有另一种处理锁之间中间空间的方法

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

您不是锁定对象本身,而是创建一个名为 PadLock 或类似内容的专用对象,并且仅在需要的地方锁定该对象。

此处锁定并不意味着在锁定期间,其他线程上的其他代码无法访问或修改该对象。如果锁定对象,则任何其他线程都可以同时修改该对象。锁代码块允许您做的是使锁块中的代码成为单个条目,即只有一个线程可以执行一次锁代码块,而其他尝试执行相同代码块的线程必须等到所有者线程完成执行代码块。所以基本上在通常情况下,你真的不需要锁定 2 个或更多对象。通过锁定,您的目的是使代码块成为单个条目