Состояние гонки между Monitor.Pulse и Monitor.Wait?

Вот минимальный код, который иллюстрирует проблему:

StringBuilder input = new StringBuilder();

void ToUpper()
{
    lock (input)
    {
        while (true)
        {
            Monitor.Wait(input);

            Console.WriteLine(input.ToString().ToUpper());
        }
    }
}

public void Run()
{
    new Thread(ToUpper) { IsBackground = true }.Start();

    // "Avoid" the initial race
    Thread.Sleep(100);

    while (true) 
    {
        lock (input)
        {
            input.Clear();
            input.Append(Console.ReadLine());
            Monitor.Pulse(input);
        }
        // Thread.Sleep(1);
    }
}

Игнорируя хорошо известное начальное состояние гонки, я удивляюсь поведению Pulse и Wait.

Вот что я ожидал:

  • поток ToUpper вызывает Wait => он помещается в очередь ожидания монитора
  • основной поток звонков Pulse => поток "ToUpper" "перемещен" из очереди ожидания в очередь готовности для немедленного получения блокировки
  • основной поток выходит из монитора при выходе из lock объем заявления
  • поток "ToUpper" получает блокировку и обрабатывает ввод, пока основной поток снова регистрируется для владения блокировкой

Но один раз из двух поток "ToUpper" не обрабатывает ввод, и вместо этого основной поток немедленно выполняет свою обработку.

Вот с моими предположениями:

  • Pulse не "перемещает" поток "ToUpper" немедленно, поэтому готовая очередь остается пустой
  • основной поток снимает блокировку, зацикливается, снова требует блокировки
  • так как в готовой очереди больше нет никого, он становится владельцем
  • иногда позже выполняется запрос на продвижение потока "ToUpper", и он, наконец, помещается в очередь готовности.
  • основная нить Pulse второй раз впустую и снимает блокировку
  • он зацикливается, пытается восстановить блокировку, но поток "ToUpper" уже существует
  • на этот раз поток "ToUpper" получает блокировку и обрабатывает ввод
  • после этого он снова спит в ожидании следующего сигнала
  • основной поток получает блокировку

Чтобы проверить это предположение, я заставил основной поток быть менее энергичным и позволить другому потоку работать, добавив Thread.Sleep(1) => в этом случае все "работает как положено".

Так что все сводится к поведению Pulse это может не сразу подтолкнуть потоки из очереди ожидания в очередь готовности.

Проблема действительно в этой возможной гонке или есть еще одна тонкость, которую я упускаю?

1 ответ

Решение

Ваше замечание о добавлении Thread.Sleep(1) это действительно ответ. Pulse Метод ничего не гарантирует для ожидающего объекта - он просто высвобождается в готовую очередь, из которой он выходит как обычный поток. Там нет никакого следа о звонке на Pulse, Таким образом, после звонка Pulse приложение ведет себя как обычное двухпоточное приложение, и в очереди готовности есть два потока: ToUpper нить и Run нить. Так без Thread.Sleep(1) это может произойти (без этого, я думаю, просто менее вероятно), что Run поток получает блокировку первым.

Еще одно важное замечание из второй ссылки, которую я предоставил:

Важной особенностью Monitor.Pulse является то, что он выполняется асинхронно, то есть сам по себе не блокируется и не приостанавливается.

Для этого сценария AutoResetEvent класс кажется более подходящим. Кроме того, во второй ссылке вы можете найти пример сценария производитель-потребитель с Wait а также Pulse,

Другие вопросы по тегам