Состояние гонки между 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
,