C# delegates: synchronuous vs asynchronuous

kataich

Вопрос должен быть простым, но моего понимания Windows не хватает, чтобы на него ответить даже с помощью Интернета, поэтому прошу помощи здесь.
1. Предположим, что есть некий C# проект (Windows Forms).
2. Рассматривается следующий абстрактный сценарий: предположим, что у класса Class есть некий член Member, который может динамически изменятся во времени асинхронно по отношению к внешнему миру. Например, этот член может хранить цену последней заявки на аукционе или идентификационный номер последнего залогинившегося пользователя и т д. Наша задача как-то среагировать на изменение этого члена, а именно исполнять некоторую последовательность действий при каждом изменении Member. Для этого в программе используются делегаты. То есть Class предоставляет делегат, в который мы помещаем ту функцию, которую следует исполнить.
Способ, которым я пользуюсь всегда выглядит так:

...
Class c = new Class;
/*
* Используем делагат, чтобы исполнить собственную
* функцию MyFunction, когда член Member изменится
*/
c += new MemberChangedEventHandler(MyFunction);
...


private void MyFunction(Member NewMember)
{
...
}

Так я поступал для всеx классов, которые имели динамически изменяемые члены.
(Сами классы реализовывал не я - мне лишь известно API, которым нужно пользоваться).
3. Недавно мне попался подобный класс ClassNew, который содержит поле IsReady, отвечающее за то, что внутреннее состояние класса полностью проинициализировано.
На windows форме есть контрол-кнопка, при нажатии на которую исполняется следующий код

/*
* Обработчик события "Нажатие на кнопку"
*/

AutoResetEvent waitHandle = new AutoResetEvent(false);

ClassNew cNew = new ClassNew;

/*
* Используем функцию ClassNewIsReady, которая должна
* исполниться, когда внутреннее состояние объекта будет
* полностью проинициализировано
*/
cNew += new ClassIsReadyEventHandler(ClassNewIsReady);
/*
* Функция старт является асинхронной, то есть выход из неё
* происходит моментально, а реально класс запустится только
* тогда, когда проделаются все предварительные действия
* и член IsReady станет true - тогда позовется функция
* ClasNewIsReady
*/
cNew.Start;

/*
* Здесь по сути можно было не использовать объект синхронизации
* а делать что-то типа "while(!cNew.IsReady) Thread.Sleep(1000);"
* Эффект тот же.
*/
waitHandle.WaitOne
...

private void ClassNewIsReady
{
waitHandle.Set;
}

Я заметил, что тред засыпает на ожидании "waitHandle.WaitOne". Проглядев все другие места, в которых делагаты использовались подобным образом, я заметил, что в остальных местах функции, которые мы помещали в делегаты, исполняются в отдельных тредах (то есть реально функции MyFunction (см первый кусок кода) исполняются в треде, отличном от того, который поместил эту функцию в делегат, сделав "c += new MemberChangedEventHandler(MyFunction);"). Соответственно, в последнем случае, функция ClassNewIsReady исполняется в том же треде, что и положил ее в делегат (так реализован класс ClassNew поэтому происходит бесконечное ожидание на строчке "waitHandle.WaitOne". Тут, кажется, все понятно. Я прочел в доках, что делегаты можно имплементировать обоими способами.
Но меня озадачивает следующее поведение: если мы выходим из обработчика "Нажатие на контрол-кнопку" (например, не ожидая waitHandle.WaitOne то функция ClassNewIsReady зовется,
причем в том же треде. Я думал, что для windows форм есть тред, который крутится в холостом цикле и реагирует на определенные события, связанные с поведение контролов на форме. Чем это отличается от того, чтобы ждать объекта синхронизации в обработчике события "нажатие на кнопку"? Или обработчики событий не могут быть вложенными, в отличие от прерываний? Как устроен ClassNew я не знаю, поэтому буду рад услышать возможные сценарии, вписывающиеся в наблюдаемые события.
Получилось очень длинно, но хотелось быть предельно понятным.

zorin29

Я думал, что для windows форм есть тред, который крутится в холостом цикле и реагирует на определенные события, связанные с поведение контролов на форме. Чем это отличается от того, чтобы ждать объекта синхронизации в обработчике события "нажатие на кнопку"? Или обработчики событий не могут быть вложенными, в отличие от прерываний?
Ты правильно думал, есть такой тред. Называется он main thread, и реализует то, что называется "оконным циклом" (псевдокод):

var message;
do
{
message = GetNextMessageFromQueue;
HandleMessage(message);
}
while (!message.IsProgramExitMessage)

Соответственно, твой обработчик нажатия на кнопку исполняется внутри HandleMessage и пока он не завершится, основной поток не будет обрабатывать остальные сообщения. В том числе и сообщение IsReadyChanged, которого ты ждешь.
waitHandle.Wait следует использовать только тогда, когда у тебя несколько потоков исполнения. Если хочется делать через WaitHandle, то заведи свой собственный поток ожидания, и пусть он висит на waitHandle.WaitOne пока вся остальная программа работает.

zorin29

По этой же причине, если основной поток висит в ожидании, то программа не реагирует на мышку, таймер и т.д.
Не рекомендую использовать wait или sleep в основном потоке.

kataich

Огромное спасибо. Твое объяснение предельно ясное.
Попробую его применить на практике.
По этой же причине, если основной поток висит в ожидании, то программа не реагирует на мышку, таймер
Да-да, такой эффект я тоже наблюдаю.
Если хочется делать через WaitHandle, то заведи свой собственный поток ожидания, и пусть он висит на waitHandle.WaitOne пока вся остальная программа работает.

Буду думать в направлении добавления числа тредов.

kataich

Соответственно, твой обработчик нажатия на кнопку исполняется внутри HandleMessage и пока он не завершится, основной поток не будет обрабатывать остальные сообщения. В том числе и сообщение IsReadyChanged, которого ты ждешь.
Правильно ли я тебя понимаю, что реализация исполнения делегатов через функцию Invoke (выполнение делегата синхронно) помещает сообщения в очередь того треда, в котором она позвалась, а значит, тред явно должен звать "GetNextMessageFromQueue", а если не позовет, то делегат никогда не исполнится?
Этот вопрос я задал к тому, что хотел бы реализовать всё так, что каждый подобный класс (где есть динамически изменяющиеся члены) я хотел бы обрабатывать в отдельном потоке. Таким образом, примерно работа тредов имела бы следующий вид

Class c = new Class;

c += new EventOneHandler(MyFunctionOne);
c+= new EventTwoHandler(MyFunctionTwo);
c+= new EventThreeHandler(MyFunctionThree);
...

c.Start;

while(true)
Thread.Sleep(1000);

То есть задача треда добавить обработчики в делегаты, а дальше ничего не делать,
только обрабатывать события, если таковые будут иметь место быть.
Но твое замечание заставило меня усомниться в этой схеме, так как тред явно
не достает события функцией GetNextMessageFromQueue, а просто спит в бесконечном
цикле, надеясь, что его разбудят, когда надо. Это неверный способ? Как необходимо поступать в такой ситуации?

zorin29

Правильно ли я тебя понимаю, что реализация исполнения делегатов через функцию Invoke (выполнение делегата синхронно) помещает сообщения в очередь того треда, в котором она позвалась, а значит, тред явно должен звать "GetNextMessageFromQueue", а если не позовет, то делегат никогда не исполнится?
Почти.
У тредов нет очереди. Очередь есть у handle-а главного окна приложения. И основной поток достает сообщения именно из этой очереди. И именно в эту очередь происходит запись по Invoke.
А треды, которые ты создаешь сам, не имеют очереди сообщений. Они исполняют свою функцию (которую ты передаешь при создании треда) и завершаются.
Приведенный тобой код будет работать, но в каком именно треде исполнятся MyFunctionXXX - зависит от реализации Class. Но уж точно не в том, в котором создан класс.
Ну и бесконечное ожидание, естественно, лучше заменить на конечное :)
Оставить комментарий
Имя или ник:
Комментарий: