Переходят ли Monitor.Pulse и Monitor.Wait в режим ядра?

Monitor.Wait() и Monitor.Pulse() должны быть более производительными, чем использование производных классов EventWaitHandle, таких как ManualResetEvent для этого источника.

С точки зрения производительности вызов Pulse занимает около сотни наносекунд на настольном компьютере эпохи 2010 года — примерно треть времени, которое требуется для вызова Set по дескриптору ожидания.

Этот прирост производительности наверняка связан с тем, как помогает CLR — внутренние структуры данных, блоки синхронизации и т. д. Попытка понять исходный код, который в основном реализован на C/C++, для меня, как для разработчика C#, трудна, и мне, как разработчику C#, очень сложно понять, как это работает. удалось получить более глубокое погружение из этого ответа, который, к сожалению, немного не дает полного объяснения:

Отсюда мы добираемся до Object::Wait (строка 531), затем переходим к SyncBlock::Wait (строка 3442). На данный момент у нас есть большая часть мяса. реализация, и это немаловажно.

Учитывая все вышесказанное и возвращаясь к тому, что вы спросили, проще реализации, я бы поостерегся упрощать Monitor.Wait(). Eсть много чего происходит под капотом, и было бы очень легко ошибиться и иметь потенциальные ошибки в альтернативной реализации.

В коде SyncBlock::Wait можно найти:

 CLREvent* hEvent;
 ...
 // Before we enqueue it (and, thus, before it can be dequeued), reset the event
 // that will awaken us.
 hEvent->Reset();

Из этого ответа Ганса Пассанта на связанный с этим вопрос о том, переходит ли Monitor.Enter() в режим ядра:

Необходимо получить планировщик потоков операционной системы. задействовано, поэтому используется CLREvent. При необходимости создается динамически. и вызывается его метод WaitOne(). Что будет включать ядро переход.

Итак, мои вопросы

  1. Войдите в режим ядра Monitor.Wait() и Monitor.Pulse() через вызовы методов CLREvent в их реализации. (Возможно ли вообще, чтобы они работали без участия ОС?)

  2. Если да, то что делает их более производительными, чем использование тонких оболочек дескрипторов ОС AutoResetEvent и ManualResetEvent?

@Charlieface, так это все еще выглядит логикой MonitorEnter?

Ivan Petrov 10.06.2024 06:01

Что ты имеешь в виду?

Charlieface 10.06.2024 11:19

@Charlieface, ты мог бы опубликовать свой предыдущий комментарий в качестве ответа. Я бы поддержал ваш ответ, но не буду голосовать за ваш комментарий, потому что комментарии имеют цель улучшить вопрос, а не ответить на него.

Theodor Zoulias 11.06.2024 06:02

@TheodorZoulias Не знаю, это всего лишь TLDR довольно длинной статьи. Ответы, содержащие только ссылки, также не приветствуются. Вы можете написать ответ самостоятельно.

Charlieface 11.06.2024 06:08

@Charlieface, возможно, я запутанно сформулировал вопрос. Статья, на которую вы ссылаетесь, описывает Monitor.Enter. Я знаю, как это работает, и это довольно очевидно. Чего я не знаю, так это того, как Monitor.Wait и Monitor.Pulse работают в тандеме с классом CLREvent(base).

Ivan Petrov 12.06.2024 09:23

@TheodorZoulias вопрос в том, может ли среда выполнения реализовать Monitor.Wait и Monitor.Pulse, не полагаясь на события ядра - т.е. собственное отслеживание того, какие потоки блокируются, пробуждение потоков вручную (имеет возможность GC) и т. д.

Ivan Petrov 12.06.2024 09:25

tinmanjk Я не изучал Wait/Pulse на уровне реализации. Я знаю только, как его использовать.

Theodor Zoulias 12.06.2024 10:17

@TheodorZoulias Кажется, я нашел небольшое объяснение Джо Даффи в сообщении в блоге joeduffyblog.com/2007/06/24/clr-monitors-and-sync-blocks «Переменные условия реализованы немного иначе», чем в Monitor .Входить.

Ivan Petrov 12.06.2024 10:26
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
9
97
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Джо Даффи в своем сообщении в блоге 2007 года дает обзорное объяснение того, как реализуются переменные состояния, то есть Monitor.Wait/Monitor.Pulse:

Условные переменные реализованы несколько иначе. Каждая среда CLR Объект потока имеет один посвященный ему объект события. Первый когда поток вызывает Wait для условной переменной, событие лениво выделено. И тогда поток просто помещает свой собственный локальный поток. событие в список событий, связанных с монитором. Регистрация событие также требует инфляции для блока синхронизации, если оно не было выполнено. уже произошло, потому что очевидно, что список событий не может быть сохранен в заголовок объекта. Когда случается Pulse, пульсирующая нить просто сигнализирует о первом событии в списке... При появлении PulseAll пульсирующая нить обходит весь список и сигнализирует каждому.

Похоже, что это все еще так, если посмотреть на исходный код для SyncBlock::Pulse и SyncBlock::PulseAll.

void SyncBlock::Pulse()
{
    WaitEventLink  *pWaitEventLink;

    if ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)
        pWaitEventLink->m_EventWait->Set();
}

void SyncBlock::PulseAll()
{
    WaitEventLink  *pWaitEventLink;

    while ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)
        pWaitEventLink->m_EventWait->Set();
}

Теперь вопрос в том, является ли этот объект события объектом события ядра.

Из исходного кода из SyncBlock::Wait

 // First time this thread is going to wait for this SyncBlock.
CLREvent* hEvent;
if (pCurThread->m_WaitEventLink.m_Next == NULL) {
    hEvent = &(pCurThread->m_EventWait);
}
else {
    hEvent = GetEventFromEventStore();
}
waitEventLink.m_WaitSB = this;
waitEventLink.m_EventWait = hEvent;
waitEventLink.m_Thread = pCurThread;
waitEventLink.m_Next = NULL;
waitEventLink.m_LinkSB.m_pNext = NULL;
waitEventLink.m_RefCount = 1;
pWaitEventLink = &waitEventLink;
walk->m_Next = pWaitEventLink;

// Before we enqueue it (and, thus, before it can be dequeued), reset the event
// that will awaken us.
hEvent->Reset();

// This thread is now waiting on this sync block
ThreadQueue::EnqueueThread(pWaitEventLink, this);

isEnqueued = TRUE;

CLREvent наследуется от CLREventBase. Глядя на его методы, кажется, что это класс, который должен помочь в хостинговых возможностях CLR -

// Create an Event that is host aware
void CreateAutoEvent(BOOL bInitialState);
void CreateManualEvent(BOOL bInitialState);
// Create an Event that is not host aware
void CreateOSAutoEvent (BOOL bInitialState);
void CreateOSManualEvent (BOOL bInitialState);

Интерфейс IHostManager имеет очень похожий, если не идентичный API и его назначение:

Предоставляет методы, которые позволяют среде CLR создавать примитивы синхронизации, вызывая хост вместо использования функции синхронизации Win32.

Из официального clr-code-guidelines:

У нас есть следующие утвержденные механизмы синхронизации в CLR:

...

События. Хост может предоставлять дескрипторы событий, которые заменяют события Win32.

Итак, похоже, что по умолчанию создается событие CLREvent — это событие Win32.

Это было подтверждено автором этого поста в блоге , немного поработав с WinDbg и пытаясь определить, какой объект события используется Monitor.Enter - AutoResetEvent.

Судя по тому, что я смог покопаться в стеке вызовов Monitor.Wait, это событие изначально задано ManualResetEvent - например здесь

handle = new CLREvent;
_ASSERTE (handle != NULL);
handle->CreateManualEvent(TRUE);
return handle;

и здесь

bool CreateManualEvent(bool initialState)
{
    m_hEvent = CreateEvent(nullptr, true, initialState, nullptr);
    return IsValid();
}

что соответствует его использованию в Syncblock::Wait сверху

// Before we enqueue it (and, thus, before it can be dequeued), reset the event
// that will awaken us.
hEvent->Reset();

В заключение, по умолчанию (без переопределения хоста) вызовы Monitor.Wait и Monitor.Pulse(All) переходят в режим ядра, поскольку они полагаются на события ОС.

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