【问题标题】:Explanation about obtaining locks获取锁的说明
【发布时间】:2012-01-17 14:04:41
【问题描述】:

我用 C# 编码已经有一段时间了,但是这个锁定顺序对我来说没有任何意义。我对锁的理解是,一旦用lock(object)获得锁,代码就得退出锁作用域才能解锁对象。

这让我想到了手头的问题。我剪掉了下面的代码,它恰好出现在我的代码中的动画类中。该方法的工作方式是将设置传递给该方法并进行修改,然后再传递给另一个重载方法。另一个重载方法会将所有信息传递给另一个线程,以某种方式处理并实际为对象设置动画。动画完成后,另一个线程调用OnComplete 方法。这实际上都完美工作,但我不明白为什么

另一个线程能够调用OnComplete,获得对象上的锁并向原始线程发出信号,它应该继续。由于对象被锁定在另一个线程中,此时代码是否应该冻结?

因此,在修复我的代码时不需要帮助,而是需要澄清它的工作原理。任何理解方面的帮助表示赞赏!

public void tween(string type, object to, JsDictionaryObject properties) {
    // Settings class that has a delegate field OnComplete.
    Tween.Settings settings = new Tween.Settings();
    object wait_object = new object();

    settings.OnComplete = () => {
        // Why are we able to obtain a lock when the wait_object already has a lock below?
        lock(wait_object) {
            // Let the waiting thread know it is ok to continue now.
            Monitor.Pulse(wait_object);
        }
    };

    // Send settings to other thread and start the animation.
    tween(type, null, to, settings);

    // Obtain a lock to ensure that the wait object is in synchronous code.
    lock(wait_object) {
        // Wait here if the script tells us to.  Time out with total duration time + one second to ensure that we actually DO progress.
        Monitor.Wait(wait_object, settings.Duration + 1000);
    }
}

【问题讨论】:

    标签: c# .net multithreading locking


    【解决方案1】:

    如文档所述,Monitor.Wait释放它被调用的监视器。因此,当您尝试获取 OnComplete 中的锁时,不会有另一个线程持有该锁。

    当监视器发出脉冲(或调用超时)时,它会在返回之前重新获取它。

    来自文档:

    释放对象上的锁并阻塞当前线程,直到它重新获得锁。

    【讨论】:

    • +1 但是,由于某种原因,与使用EventWaitHandle 进行同步相比,我发现这种发信号方式稍微有点复杂。我不确定是否存在性能差异,但在这种情况下,我总是需要停下来更彻底地检查代码。如果不出意外,则涉及更多代码行。
    • 细节不多也不少。我喜欢。感谢您使其易于理解!
    【解决方案2】:

    我为此写了一篇文章:Wait and Pulse demystified

    还有更多的事情发生!

    【讨论】:

    • 那篇文章非常详细地解释了Monitor 类。谢谢。
    【解决方案3】:

    记住:

    lock(someObj)
    {
      int uselessDemoCode = 3;
    }
    

    相当于:

    Monitor.Enter(someObj);
    try
    {
      int uselessDemoCode = 3;
    }
    finally
    {
      Monitor.Exit(someObj);
    }
    

    实际上,这有不同版本的变体。

    已经很清楚了,我们可以用以下方式解决这个问题:

    lock(someObj)
    {
       Monitor.Exit(someObj);
       //Don't have the lock here!
       Monitor.Enter(someObj);
       //Have the lock again!
    }
    

    您可能想知道为什么有人会这样做,好吧,我也会这样做,这是一种使代码变得不那么清晰和不可靠的愚蠢方式,但是当您想使用 PulseWait 时,它确实会发挥作用,带有显式EnterExit 调用的版本更清晰。就我个人而言,如果出于这个原因我要去PulseWait,我更喜欢使用它们而不是lock;我发现lock 停止让代码更清晰,并开始变得不透明。

    【讨论】:

      【解决方案4】:

      我倾向于避免这种风格,但是,正如 Jon 已经说过的,Monitor.Wait 释放了调用它的监视器,因此此时没有锁定。

      但是恕我直言,这个例子有点缺陷。问题是,一般来说,如果Monitor.Pulse 被调用之前 Monitor.Wait,等待线程将永远不会收到信号。考虑到这一点,作者决定“谨慎行事”并使用指定超时的重载。所以,抛开不必要的获取和释放锁,代码感觉不对。

      为了更好地解释这一点,请考虑以下修改:

      public static void tween()
      {
          object wait_object = new object();
      
          Action OnComplete = () =>
          {
              lock (wait_object)
              {
                  Monitor.Pulse(wait_object);
              }
          };
      
          // let's say that a background thread
          // finished really quickly here
          OnComplete();
      
          lock (wait_object)
          {
              // this will wait for a Pulse indefinitely
              Monitor.Wait(wait_object);
          }
      }
      

      如果OnComplete在主线程获取锁之前被调用,并且没有超时,我们就会死锁。在您的情况下,Monitor.Wait 只会挂起一段时间并在超时后继续,但您明白了。

      这就是为什么我通常推荐一种更简单的方法:

      public static void tween()
      {
          using (AutoResetEvent evt = new AutoResetEvent(false))
          {
              Action OnComplete = () => evt.Set();
      
              // let's say that a background thread
              // finished really quickly here
              OnComplete();
      
              // event is properly set even in this case
              evt.WaitOne();
          }
      }
      

      引用MSDN

      Monitor 类不维护指示 Pulse 方法已被调用的状态。因此,如果您在没有线程等待时调用 Pulse,则调用 Wait 的下一个线程会阻塞,就好像 Pulse 从未被调用过一样。如果两个线程使用 Pulse 和 Wait 进行交互,这可能会导致死锁。

      将此与 AutoResetEvent 类的行为进行对比:如果您通过调用其 Set 方法发出 AutoResetEvent 信号,并且没有线程在等待,则 AutoResetEvent 将保持信号状态,直到线程调用 WaitOne、WaitAny 或 WaitAll。 AutoResetEvent 释放该线程并返回到未发出信号的状态。

      【讨论】:

      • 因此,即使在调用锁WaitOne 之前调用 AutoResetEvent 也会发出信号。这在某些情况下可能很方便。唯一的问题是它没有超时属性。在这种情况下,Monitor.Wait 中的超时是必不可少的。
      • It does have a timeout overload。这就是为什么它是首选的同步方式。最有趣的是 Monitor.Pulse 的 MSDN 示例与您的示例一样存在缺陷。 :)
      • 好吧,如果是这样的话,那么为了清楚起见,我可能想切换到它。看起来它可能是一个更好、更容易、更安全的类。谢谢!
      猜你喜欢
      • 2010-09-22
      • 2014-07-10
      • 2013-06-06
      • 2016-02-13
      • 1970-01-01
      • 2016-02-21
      • 2020-04-13
      • 2016-08-11
      • 1970-01-01
      相关资源
      最近更新 更多