Объяснение о получении замков - PullRequest
2 голосов
/ 17 января 2012

Я довольно давно программирую на 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);
    }
}

Ответы [ 4 ]

3 голосов
/ 17 января 2012

Как задокументировано, Monitor.Wait выпускает монитор, с которым он вызван.Таким образом, к тому времени, когда вы попытаетесь получить блокировку в OnComplete, не будет другим потоком, удерживающим блокировку.

Когда монитор пульсирует (или время ожидания вызова)он возвращает его перед возвратом.

Из документов:

Снимает блокировку с объекта и блокирует текущий поток до тех пор, пока он не снимет блокировку.

1 голос
/ 17 января 2012

Я стараюсь избегать этого стиля, но, как уже сказал Джон, 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: если вы отправляете сигнал AutoResetEvent, вызывая его метод Set, и нет ожидающих потоков, AutoResetEvent остается в сигнальном состоянии до тех пор, пока поток не вызовет WaitOne, WaitAny или WaitAll. AutoResetEvent освобождает этот поток и возвращает его в состояние без сигнала.

1 голос
/ 17 января 2012

Помните, что:

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!
}

Вы можете задаться вопросом, почему кто-то сделал бы это, и я тоже, это глупый способ сделать код менее понятным и менее надежным, ноон вступает в игру, когда вы хотите использовать Pulse и Wait, что делает версию с явными вызовами Enter и Exit более понятной.Лично я предпочитаю использовать их вместо lock, если по этой причине я собираюсь Pulse или Wait;Я обнаружил, что lock перестает делать код чище и начинает делать его непрозрачным.

1 голос
/ 17 января 2012

Я написал статью об этом: Ожидание и пульс демистифицированы

Там происходит больше, чем кажется на первый взгляд!

...