【问题标题】:Does lock() guarantee acquired in order requested?lock() 是否保证按请求的顺序获得?
【发布时间】:2011-05-12 20:20:14
【问题描述】:

当多个线程请求同一个对象上的锁时,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 中找到了这个,基本上同意:

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

    “大致 FIFO”位是我之前想到的,而“偷偷摸摸的线程”位进一步证明了您不应该对 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 将在线程在锁中等待时对操作的执行进行排序。

      【讨论】:

        猜你喜欢
        • 2023-01-27
        • 1970-01-01
        • 2012-06-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-08-24
        • 2021-01-27
        相关资源
        最近更新 更多