【问题标题】:Does lock() guarantee acquired in order requested?lock() 是否保证按请求的顺序获取?
【发布时间】:2023-01-27 01:33:42
【问题描述】:

当多个线程请求同一个对象的锁时,CLR 是否保证将按照请求的顺序获取锁?

我写了一个测试来查看这是否是真的,它似乎表明是的,但我不确定这是否是确定的。

class LockSequence
{
    private static readonly object _lock = new object();

    private static DateTime _dueTime;

    public static void Test()
    {
        var states = new List<State>();

        _dueTime = DateTime.Now.AddSeconds(5);
        
        for (int i = 0; i < 10; i++)
        {
            var state = new State {Index = i};
            ThreadPool.QueueUserWorkItem(Go, state);
            states.Add(state);
            Thread.Sleep(100);
        }
        
        states.ForEach(s => s.Sync.WaitOne());
        states.ForEach(s => s.Sync.Close());
    }

    private static void Go(object state)
    {
        var s = (State) state;

        Console.WriteLine("Go entered: " + s.Index);

        lock (_lock)
        {
            Console.WriteLine("{0,2} got lock", s.Index);
            if (_dueTime > DateTime.Now)
            {
                var time = _dueTime - DateTime.Now;
                Console.WriteLine("{0,2} sleeping for {1} ticks", s.Index, time.Ticks);
                Thread.Sleep(time);
            }
            Console.WriteLine("{0,2} exiting lock", s.Index);
        }

        s.Sync.Set();
    }

    private class State
    {
        public int Index;
        public readonly ManualResetEvent Sync = new ManualResetEvent(false);
    }
}

印刷:

进入:0

0 已锁定

0 休眠 49979998 滴答声

进入:1

进入:2

进入:3

进入:4

进入:5

进入:6

进入:7

进入:8

进入:9

0 退出锁

1 已锁定

1 睡 5001 滴答声

1个出口锁

2 锁定

2 睡 5001 刻

2 退出锁

3 锁定

3 睡 5001 滴答声

3 退出锁

4 锁定

4 睡 5001 滴答声

4 退出锁

5 锁定

5 睡 5001 滴答声

5 退出锁

6 锁定

6 退出锁

7 锁定

7 退出锁

8 锁定

8 退出锁

9 锁定

9 退出锁

【问题讨论】:

    标签: c# .net synchronization locking


    【解决方案1】:

    IIRC,这是很有可能按照这个顺序,但不能保证。我相信至少在理论上存在线程会被虚假唤醒的情况,请注意它仍然没有锁,然后转到队列的后面。这可能只适用于Wait/Notify,但我暗暗怀疑它也用于锁定。

    确实不会依赖它 - 如果您需要按顺序发生事情,请建立一个 Queue&lt;T&gt; 或类似的东西。

    编辑:我刚刚在 Joe Duffy 的 Concurrent Programming on Windows 中找到了这个,它基本上同意:

    因为监视器在内部使用内核对象,所以它们表现出与 OS 同步机制也表现出的相同的大致 FIFO 行为(在前一章中描述)。监视器是不公平的,因此如果另一个线程在一个被唤醒的等待线程尝试获取锁之前尝试获取锁,则允许偷偷摸摸的线程获取锁。

    “roughly-FIFO”位是我之前的想法,“sneaky thread”位进一步证明您不应该对 FIFO 排序做出假设。

    【讨论】:

    • +1 还请注意,您的测试应用程序可能在您的桌面上运行良好,然后在您的生产代码运行的 64 处理器机器上运行不同
    • 无论如何,从多个线程向Queue&lt;T&gt;添加项目都不需要lock,因此会表现出完全相同的行为,因为完全相同的原因,项目可能会乱序添加到队列中吗?
    • @Sam - 在这个用例msdn.microsoft.com/en-us/library/dd267265.aspx 中使用线程安全ConcurrentQueue&lt;T&gt;。为了保证顺序处理,使用单个生产者线程和单个消费者线程。
    • @Sam:两个试图同时添加一些东西的线程之间会出现竞争条件,是的 - 但不同之处在于一旦它们被添加,你就可以按保证的顺序获取......而在在锁定的情况下,即使您能以某种方式知道一个线程已经开始在另一个线程之前获得锁定方式,您也无法保证它会实际上首先获得它。
    • @Jon Skeet,我想我要问的真正问题是锁内的代码是否不比向队列中添加一些东西更复杂或更耗时,锁是最好的选择还是有其他更好的选择?
    【解决方案2】:

    普通的 CLR 锁不保证是 FIFO。

    但是,有一个QueuedLock class in this answer这将提供有保证的 FIFO 锁定行为.

    【讨论】:

    • 完美的!我不记得五年前我在哪个项目中需要它,但很高兴将它放在我的存储库中以备将来使用。感谢张贴的链接。
    【解决方案3】:

    lock 声明被记录为使用 Monitor 类来实现其行为,而 Monitor 类的文档没有提及(我能找到)公平性。所以你不应该依赖于请求的顺序获取请求的锁。

    事实上,Jeffery Richter 的一篇文章表明 lock 实际上是不公平的:

    当然 - 这是一篇旧文章,所以事情可能已经改变,但鉴于 Monitor 类的合同中没有关于公平性的承诺,你需要假设最坏的情况。

    【讨论】:

      【解决方案4】:

      与问题略有关系,但 ThreadPool 甚至不保证它将按照添加的顺序执行排队的工作项。如果您需要顺序执行异步任务,一种选择是使用 TPL 任务(也通过 Reactive Extensions 向后移植到 .NET 3.5)。它看起来像这样:

      public static void Test()
      {
          var states = new List<State>();
      
          _dueTime = DateTime.Now.AddSeconds(5);
      
          var initialState = new State() { Index = 0 };
          var initialTask = new Task(Go, initialState);
          Task priorTask = initialTask;
      
          for (int i = 1; i < 10; i++)
          {
              var state = new State { Index = i };
              priorTask = priorTask.ContinueWith(t => Go(state));
      
              states.Add(state);
              Thread.Sleep(100);
          }
          Task finalTask = priorTask;
      
          initialTask.Start();
          finalTask.Wait();
      }
      

      这有几个优点:

      1. 保证执行顺序。

      2. 您不再需要显式锁定(TPL 会处理这些细节)。

      3. 您不再需要事件,也不再需要等待所有事件。你可以简单地说:等待最后一个任务完成。

      4. 如果在任何任务中抛出异常,则后续任务将中止,并且将通过调用 Wait 重新抛出异常。这可能符合也可能不符合您想要的行为,但通常是顺序的、相关任务的最佳行为。

      5. 通过使用 TPL,您为未来的扩展增加了灵活性,例如取消支持、等待并行任务继续等。

      【讨论】:

      • 谢谢。 ThreadPool 只是为了演示,我使用等待来确保它们都以正确的顺序获得了演示的锁。
      【解决方案5】:

      我正在使用这种方法进行 FIFO 锁定

      public class QueuedActions
      {
          private readonly object _internalSyncronizer = new object();
          private readonly ConcurrentQueue<Action> _actionsQueue = new ConcurrentQueue<Action>();
      
      
          public void Execute(Action action)
          {
              // ReSharper disable once InconsistentlySynchronizedField
              _actionsQueue.Enqueue(action);
      
              lock (_internalSyncronizer)
              {
                  Action nextAction;
                  if (_actionsQueue.TryDequeue(out nextAction))
                  {
                      nextAction.Invoke();
                  }
                  else
                  {
                      throw new Exception("Something is wrong. How come there is nothing in the queue?");
                  }
              }
          }
      }
      

      当线程在锁中等待时,ConcurrentQueue 将命令执行操作。

      【讨论】:

        猜你喜欢
        • 2011-05-12
        • 2012-06-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-08-24
        • 2010-12-14
        • 2011-08-30
        相关资源
        最近更新 更多