关于获取锁的说明

本文关键字:说明 获取 于获取 | 更新日期: 2023-09-27 18:20:45

我已经用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);
    }
}

关于获取锁的说明

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

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

来自文档:

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

我写了一篇关于这方面的文章:等待和脉冲解密

发生的事情远不止眼前所见!

请记住:

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停止了使代码更干净,并开始使其不透明。

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

但IMHO的例子有点缺陷。通常,问题是,如果Monitor.PulseMonitor.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方法来向AutoResetEvents发出信号,并且没有线程在等待,则在线程调用WaitOne、WaitAny或WaitAll之前,AutoResetEvent将保持在发出信号的状态。AutoResetEvent释放该线程并返回到无信号状态。