Есть ли жизнь после Class_Terminate, или вечноживущий объект

Хакер дает советы, раскрывает секреты и делится своими мыслями по поводу программирования.

Модератор: Хакер

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Есть ли жизнь после Class_Terminate, или вечноживущий объект

Сообщение Хакер » 16.08.2015 (Вс) 14:39

Должно быть, многие знают, что в отношении VB-объектов в частности и в отношении COM-объектов в целом работает подсчёт ссылок. За счёт механизма подсчёта ссылок объекты определяют момент, когда им нужно уничтожиться. Когда счётчик ссылок достигает значения «0», это означает, что ни у кого больше нигде не осталось ссылки на объект, и значит объект должен быть уничтожен, ибо он больше никому не нужен. Во всяком случае, это должен знать и осознавать каждый, кто работает с VB-объектами или COM-объектами.

Также существует проблема циклических ссылок, когда есть, например, пара объектов, причём первый имеет ссылку на второй, а второй имеет ссылку на первый. В этом случае, даже если ни на один из объектов пары ни у кого в целом по программе не осталось ссылок, они будут жить до конца времён.

В простейшем случае проблема циклических ссылок возникает и с одним объектом.

Попробуйте такой объект:
Код: Выделить всё
Private Self As Object

Private Sub Class_Initialize()
    MsgBox "Я родился"
    Set Self = Me
End Sub

Private Sub Class_Terminate()
    MsgBox "Я умер"
End Sub


Вы не увидите сообение «Я умер», потому что событие Class_Terminate никогда не произойдёт, ибо экземпляр держит ссылку на самого себя, не давая счётчику достигнуть нулевого значения.

Эта ситуация в общем случае считается вредной.

Может ли этот эффект быть полезным?

Зададимся пока другим вопросом: может ли объект отменить собственное уничтожение?

Например у формы есть события Unload и QueryUnload, имеющие аргумент Cancel. Обработчик события может присвоить аргументу ненулевое значение, и это отменит выгрузку формы.

Может ли аналогичным образом экземпляр класса отменить собственное уничтожение из обработчика Class_Terminate? На первый взгляд кажется, что не может, ведь у события Terminate нет никакого Cancel-аргумента (аналогичного тому, что есть у событий Unload и QueryUnload).

Но на самом деле — может.

Когда собственный счётчик ссылок объекта достигает значения 0, VB вызывает обработчик Class_Terminate, чтобы экземпляр класса мог сделать какие-то последние действия перед своей смертью. Но дело в том, что после того, как происходит возврат из Class_Terminate, VB ещё раз проверяет счётчик ссылок.

И если после этого счётчик ссылок имеет ненулевое значение, уничтожение объекта отменяется.

Это напрямую не документированное, но единственно-допустимое поведение: ведь код внутри Class_Terminate мог ссылку на объект передать или присвоить куда-то (куда угодно), что в результате могло увеличить счётчик ссылок на объект. И если после того, как произошёл возврат из Class_Terminate и у кого-то вновь появилась ссылка на объект, объект будет уничтожен, это означало бы приговор для приложения, ибо оно вскоре рухнет.

Вспомним то, что объект может держать ссылку сам на себя и предотвращать таким образом собственную смерть.

— Так можно ли из Class_Terminate отменить предстоящее скорое уничтожение объекта?
— Да, достаточно присвоить ссылку на самого себя приватной переменной объекта.

Примерно так:
Код: Выделить всё
Option Explicit
Private SelfBootstrap As Class1

...

Private Sub Class_Terminate()
    Set SelfBootstrap = Me
End Sub


Экземпляр такого класса не уничтожится после того, как последняя внешняя ссылка на него будет затёрта с помощью Set ref = Nothing.

И в чём польза?

Казалось бы, общий эффект от такого кода ничем не отличается от самого первого примера, где объект присваивает ссылку на самого себя своей собственной внутренней переменной, и становится вечно-живущим. И никакой пользы от вечно-живущих объектов нет, ибо они только утечку памяти создают. Где это вообще может быть применено с пользой?

Во-первых, разница есть, а во-вторых, есть целое семейство ситуаций, когда этот трюк способен обеспечить правильную логику работы приложения.

Представьте себе, что ваш класс управляет некими ресурсами, которые при смерти экземпляра должны освобождаться и уничтожаться. Обычно ресурсы выделяются и активируются в Class_Initialize, либо при первом обращении к какому-то публичному члену класса (подход с отложенной инициализацией). А уничтожаются и освобождаются внутри Class_Terminate.

Но есть целый класс ситуаций, когда освобождение ресурсов выполняется не единственным вызовом одной функции, а когда оно выполняется в несколько стадий и асинхронно. И если вы в Class_Terminate поместите вызов, который инициирует первую стадию освобождения ресурсов, то сразу же после возврата из Class_Terminate ваш объект уничтожится, и некому будет дождаться завершения первой стадии и начать вторую стадию освобождения ресурсов.

А это приведёт к тому, что ресурсы не будут освобождены как полагается в полном объёме.

Было бы решением сделать у класса некий метод CloseAll, который будет инициировать и дожидаться завершения всех стадий освобождения связанных с экземпляром сторонних ресурсов, и потребовать у пользователя класса, чтобы тот неприменно вызывал CloseAll, прежде чем занулит свою ссылку на объект. Но это совершенно не решение и оно не стыкуется с сутью COM: когда обладатель ссылки на объект зануляет свою ссылку на объект, он не имеет никаких средств узнать, является ли он последним обладателем ссылки на объект (и значит должен перед занулением ссылки вызвать CloseAll), либо кто-то ещё в данный момент держит ссылку на объект и может полноценно пользоваться им (и тогда вызов CloseAll будет преждевременным и всё сломает).

К тому же, нельзя не понимать, что программист — существо забывчивое, и скорее всего забудет вызвать CloseAll, либо не сможет предусмотреть все варианты того, как пойдёт выполнение кода, и останется вариант, в котором CloseAll не вызывается перед смертью объекта.

Поэтому подход с явным и принудительным освобождением ресурсов имеет право на жизнь, но в любом случае класс обязан уметь подчищать за собой перед самоуничтожением всё, что не было освобождено принудительным ручным вызовом.

Здесь и приходит на помощь наш трюк.

Внутри Class_Terminate объект проверяет, был ли перед этим вызван метод, выполняющий принудительное освобождение ресурсов, и освобождены ли все ресурсы. Если всё освобождено — объект не препятствует собственной смерти. Но если что-то по-прежнему не освобождено, объект внутри Class_Terminate присваивает ссылку на самого себя своей приватной переменной, и таким образом блокирует своё скорейшее освобождение.

Далее он имеет возможность дождаться момента, когда завершатся все стадии освобождения занятых ресурсов, и в конце-концов сбросить ссылку SelfBootstrap — и умереть окончательно.

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

  • Допустим, ваш класс сам порорждает окна (с помощью CreateWindowEx). Допустим, с каждым окном связан некий блок памяти, хранящий какие-то данные, относящиеся к этому окну. Теперь представьте, что наступает момент, когда затирается последняя ссылка на экземпляр такого класса. В этот момент созданное ранее окно должно уничтожиться, а память, выделенная под «некий блок памяти, хранящие ассоциированные с окном данные» — освободиться.

    Но уничтожение окна — это не та операция, которая выполняется мгновенно и за раз.

    Если вы вызвали DestroyWindow(), то это ещё не значит, что созданное окно тут же перестало существовать. После того, как вы вызвали DestroyWindow, окно продолжает существовать, но в очередь сообщений потока приходят специфические сообщения (WM_DESTROY, а затем WM_NCDESTROY). До тех пор, пока эти сообщения не будут обработаны потоком, окно продолжает существовать, HWND остаётся валидным. Теоретически возможно, что на момент вызова DestroyWindow() в очереди сообщений уже и так есть какие-то рядовые ещё не обработанные сообщения, предназначенные окну. Либо, что такие сообщения поступят в очередь после вызова DestroyWindow(), но до обработки WM_DESTROY. Все эти рядовые сообщения должны быть обработаны, прежде чем дело дойдёт до WM_DESTROY и WM_NCDESTROY. И до тех пор, пока дело не дойдёт до WM_NCDESTROY, наш экземпляр не имеет права умирать, равно как и не имеет права освобождать блок данных, ассоциированных с окном (этим блоком не обязательно должен быть блок, выделенный с помощью HeapAlloc/VirtualAlloc — речь может идти о простом массиве внутри экземпляра или просто о наборе приватных членов класса).

    И это, в общем, как рза наш случай: вызвать в Class_Teminate функцию DestroyWindow и сразу же умереть — это глупейшее, что можно сделать. Правильным будет вызвать её и подтянув себя за шнурки, то есть установив SelfBootstrap = Me, отсрочить свою гибель до момента, когда будет обработано WM_NCDESTROY.


    Пример с окном оказался неактуальным: я сам себя ввёл в заблуждение неудачным экспериментом. На самом деле, WM_DESTROY и WM_NCDESTROY доставляются с помощью SendMessage, а не PostMessage, и поэтому будут обработаны до выхода из DestroyWindow.

  • Другой пример, не связанный с окнами и оконными сообщениями — это работа с мультимедиа. Как осуществляется вывод или захват звука в Windows (либо вывод или приём MIDI-сообщений)? Обычно так: вы подготавливаете некий буфер или набор буферов и отдаёте их системе. Если вы выводите данные, то вы сперва заполняете буфер данными, прежде чем отдать его системе. Если принимаете — то отдаёте буфер системе, и ждёте, пока система не даст вам отмашку, что она заполнила буфер данными.

    С момента, когда вы отдали буфер системе, вы не имеете права его трогать, в частности — освобождать выделенную под буфер память. Вне зависимости от того, принимаете вы данные или отправляете, буфер попадает обратно в ваше распоряжение только тогда, когда система явно вам его возвращает (об этом она оповестит вас одним из нескольких поддерживаемых способов — по вашему выбору).

    Что, если у вас класс, который занимается отправкой или приёмом мультимедийных данных (звука или MIDI-сообщений), и внезапно зануляется последняя ссылка на экземпляр такого класса? Класс не может уничтожиться, не освободив буферы, ибо это приведёт к утечке памяти. В то же время, класс не может освободить буферы сейчас же, если они были переданы в распоряжение системы и не были возвращены ею.

    При выводе данных система возвращает нам буферы по мере того, как она воспроизвела записанные в буферы данные. При захвате поступающих снаружи данных — по мере того, как предоставленные ей нами буферы были заполнены поступающими данными.

    Так или иначе, если наш объект собрался умереть, он не может умереть прямо сейчас, не дождавшись сперва, пока система не вернёт все буферы в наше распоряжение. При происхождении Class_Terminate, мы должны просигнализировать системе, что мы хотим свернуть нашу деятельность по захвату/воспроизведению мультимедийных данных. Система постарается прекратить эту деятельность и вернуть нам буферы. Как быстро она это сделает — зависит от реализации мультимедийных драйверов. Лишь когда мы получим от системы отмашку, что буферы возвращены нам (например сообщения MM_WOM_DONE, MM_WIM_DATA, MM_MOM_DONE, MM_MIM_LONGDATA), наш экземпляр будет иметь право освободить буферы (если они не были частью данных самого экземпляра) и окончательно умереть.

Разумеется, случаи, когда ресурсы освобождаются не мгновенно и не синхронно (то есть не блокируя поток, инициирующий освобождение ресурса до момента его освобождения), не ограничиваются окнами и API библиотеки WinMM.

Напоследок скажу, что нужно иметь, что Class_Terminate вызывается только единожды — когда в первый раз счётчик ссылок достигает нулевого значения. Если объект предпочтёт продлить своё существование поддержкой себя за шнурки, когда он отпустит шнурок — второй раз Class_Terminate уже не сработает.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение The trick » 16.08.2015 (Вс) 15:52

Интересная особенность. А что если при инициализации присвоить приватной переменной ссылку на себя? (возможности пока нет проверить)
Я думаю тогда можно будет контролировать жизнь объекта, и Terminate вызовется только когда мы этого захотим (присвоим приватной переменной Nothing).
UA6527P

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Хакер » 16.08.2015 (Вс) 16:52

The trick писал(а):А что если при инициализации присвоить приватной переменной ссылку на себя? (возможности пока нет проверить)

Это то, с чего начинается статья — самый первый фрагмент кода в статье. И глупо так делать, потому что в этом случае мы вообще никогда не узнаем, что у внешнего мира пропали все ссылки на нас.

Смысл ставить bootstrap-ссылку только в Class_Terminate (но никак не раньше) именно в том, что в этом случае мы не теряем возможность узнать, что пора сворачиваться (но получаем дополнительное время, на то, чтобы это сделать, дождавшись освобождения ресурсов как положено).
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Хакер » 17.08.2015 (Пн) 12:07

Поправочка: пример с окном оказался неудачным. Я этот пример в тексте статьи зачеркнул.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Mikle
Изобретатель велосипедов
Изобретатель велосипедов
Аватара пользователя
 
Сообщения: 4147
Зарегистрирован: 25.03.2003 (Вт) 14:02
Откуда: Туапсе

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Mikle » 17.08.2015 (Пн) 12:48

Хакер писал(а):пример с окном оказался неудачным

Но пример с воспроизведением мультимедиа очень правильный.
Как ты оцениваешь такой вариант:
Код: Выделить всё
Private Sub Class_Terminate()
  myResource.Destroy
  While Not myResource.Destroyed
    DoEvents
    Sleep 10
  Wend
End Sub

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Хакер » 17.08.2015 (Пн) 13:47

Очень плохо оцениваю.

Это может привести к ситуации, которая называется nested message loop.

Иными словами, кто-то нажал кнопку «Закрыть мультимедиа-порт», это привело к отправке кнопке сообщения, штатный цикл сообщений подхватил сообщение, вызвал DispatchMessage, выполнение прилетело на штатный WP VB-шной кнопке, дальше дело дошло до обработчика события Click, который занулил ссылку, что вызвало Class_Terminate, который инициировал многостадийный мультимедийный cleanup и стал 100 раз в секунду «покручивать» свой собственный цикл сообщений.

Но тут, допустим, что пользователь либо кликнул по кнопке «Открыть порт» во время ожидания (пока крутился цикл внутри Class_Terminate), либо вообще покликал кнопки раньше (пока наша программа по какой-то причине подвисла на секунду). Тогда следующее сообщение будет подхвачено уже функцией PeekMessage, вызванной из DoEvents, и вся последующая обработка событий (которая в свою очередь тоже может породить какой-нибудь свой цикл) будет корнями расти из Class_Terminate мусорного объекта.

Который теперь уже, по факту, не уничтожится, пока не произойдёт возврат в обратно в Class_Terminate.

К тому же, как я уже неоднократно писал, DoEvents — достаточно тяжелая функцию, делающая много тяжелых вещей.

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

Единственное чисто VB-шное исключение: свой собственный цикл прокачки сообщения внутри Sub Main, если мы пишем приложения, в котором нет ни одного живущего экземпляра формы, но при этом мы не хотим, чтобы приложение тут же завершилось. Но это один единственный цикл в Sub Main — подход с созданием циклов в Class_Terminate от этой ситуации далёк.

Во всех остальных случаях надо использовать штатный цикл прокачки сообщений — он хорош, его и надо использовать.

С WinMM объект должен зомбировать себя и дождаться прихода оконного сообщения, оповещающего нас о том, что драйвер возвращает буфер в наше распоряжени, а дождавшись — вызвать Unprepare, освободить память, закрыть WinMM-устройство и сбросить bootstrap-ссылку и с чистой совестью умереть.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Debugger
Продвинутый гуру
Продвинутый гуру
Аватара пользователя
 
Сообщения: 1667
Зарегистрирован: 17.06.2006 (Сб) 15:11

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Debugger » 17.08.2015 (Пн) 15:47

Занятная штука.

Предположим, моё приложение содержит ссылку на какой-нибудь объект с буффером. После закрытия приложения объект хочет пожить ещё некоторое время, чтобы освободить ресурсы, и потом вызвать ещё какие-то действия, и создает для этого ссылку внутри себя на себя же.
1. В каком контексте будет выполняться код? Я верно понимаю, что поток будет ждать уничтожения -всех- объектов внутри себя?
2. Не станет ли это источником увлекательных и тяжело отлавливаемых багов? Например, что произойдет, если у него не получилось высвободить ресурсы (exception), и выполнение до заветной строчки с убиранием ссылки на себя?
3. На фоне этого, если кто-то вызвал создание этого объекта, отдавая себе отчет в том, что его завершение ресурсоемко / требует асинхронной операции - не будет логичным вызов освобождения ресурсов переложить на сторону, который этот объект создала (такой C-way)?

Все это похоже на прикольный трюк, который в отдельно взятом применении превращается в антипаттерн.

(я не знаком с подробностями COM - поэтому и задаю вопросы)

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Хакер » 17.08.2015 (Пн) 18:40

Debugger писал(а):1. В каком контексте будет выполняться код? Я верно понимаю, что поток будет ждать уничтожения -всех- объектов внутри себя?


Какой код? Полагаю, что код, который занулит bootstrap-ссылку?

Ответ на вопрос зависит от того, о каком типе проекта идёт речь и какая у него потоквая модель. В принципе, проекты типа Standard EXE и ActiveX EXE в этом плане действуют одинаково. После запуска проекта начинает крутиться цикл прокачки сообщений, процедура с ним — часть рантайма. Этот цикл может завершиться штатно и нештатно (или совсем уж нештатно).

«Совсем уж нештатно» — это исключение, которое некому обработать, например банальное EXCEPTION_ACCESS_VIOLATION. В этом случае понятно,что управление на самый нижний SEH-обработчик и мы видим грустное окошко «Отправить отчёт». Но это не то, о чём интересно говорить.

Нештатно — это если приложение выполнило End или Stop.

Штатно — это интересный вопрос. Сам по себе цикл прокачки сообщений будет крутиться до тех пор, пока по мнению рантайма существуют объекты, ради которых стоит его крутить. Для проекта типа Standard EXE такими объектами являются живые экземпляры форм, а для ActiveX EXE — экземпляры публичных классов (то есть классов, созданных снаружи — из чужого апартамента).

В принципе, вопрос «что делать, если у меня в проекте типа Standard EXE одни только классы и ни одной формы, но я хочу, чтобы он проработал долго, а не завершался сразу же» — существует, его иногда задают, но он вне рамок данной статьи. Решений у него два: либо иметь в проекте форму (скажем, FApplication) и в Sub Main загружать её (Load FApplication) — ещё достаточно просто загрузить, её не нужно показывать/делать видимой. Либо самостоятельно в Sub Main делать свой цикл прокачки сообщений.

Но это, повторюсь, вне рамок этой статьи.

Предположим, что мы в нашем приложении выбрали один из способов заставить приложение умирать тогда, когда нам надо. Всё, теперь будем исходить из мысли, что приложение до тех пор, пока мы не разрешим, крутит цикл прокачки сообщений (внутрирантаймовый или наш собственный). Вообще, думать об этом приходится только потому, что ты сказал «После закрытия приложения объект хочет». А ведь сценарий намного шире: и при закрытии отдельного окна приложения, и даже просто при смене режима приложения.

Не важно, так или иначе, в какой-то момент жизни в приложении устранилась последняя ссылка на объект, а объект — вот незадача — не может умереть прямо сейчас, ибо ему ещё предстоит выполнить некий долгий асинхронный cleanup.

Вообще, суть рассматрвиаемой ситуации — выполнение некоей предтерминальной асинхронной операции. Асинхронной — означает, что мы вызываем функцию, но возврат происходит не тогда, когда операция завершена, а сразу же, а сама операция продолжает выполняться где-то в фоне ещё некоторое время, но потом нам дают знать, что она завершилась.

Нам дают знать — это интересный момент. Разные функции, разные API и разные механизмы могут давать знать по разному. Но основные варианты такие:
  • Когда операция завершится, нам пришлют особое оконное сообщение (тому окну, хендл которого мы укажем)
  • Когда операция завершится, нашему потоку пришлют сообщение (это почти как оконное, но у него hwnd = 0, поэтому обрабатывать его должен не какой-то WindowProc, а код в самом цикле прокачки сообщений)
  • Когда завершится операция нам сообщат с помощью объекта синхронизации (например event'а — ядерного, а не того, что в ООП).
  • Когда завершится операция, нам разморозят (с помощью ResumeThread) поток, хендл которого мы укажем.
  • Когда завершится операция, вызовут предоставленный нами callback.

Стоит разобрать каждый вариант.

Вариант, когда о завершении операции нас оповещают оконным сообщением — самый удобный для нас (как для VB-программиста). Вариант с thread message применим только если мы имеем свой message loop, а значит — далеко не всегда. То есть неудобный.

Вариант с объектом синхронизации скорее неудобный, чем удобный: многопоточный кирпич я пока не сделал, значит у нас нет возможности сделать отдельный поток, который будет ожидать на объекте синхронизации. Ожидать на нём из главного потока с помощью WaitForSingleObject — значит заморозить его выполнение (и всё приложение), что вряд ли является тем, чего бы мы хотели. Ожидать из главного потока с помощью MsgWaitForMultipleObjects (функция, которая ждёт как WaitForMultipleObjects, но при этом прокачивает сообщения) — можно, но чревато проблемой nested message loops.

Вариант с разморозкой потока однозначно не подходит: главный поток замораживать не хочется, а других нет (мой кирпич не готов).

Вариант с callback-ом — отдельная тема. Если кто-то «грозится» вызвать нашу функцию, то нужно не радоваться лёгкому решению проблемы, а озадачиваться тем, откуда этот вызов произойдёт. Варианта по сути три:
  • При инициировании асинхронной операции нашим потоком во владении потока возникло новое невидимое окно (либо он существовало ещё до этого — не важно). В момент, когда операция будет завершена, этому окну пошлют сообщение, цикл прокачки сообщений нашего потока подберёт сообщение, вызовет DispatchMessage, она вызовет WindowProc этого чужого невидимого окна, у же WindowProc вызовет наш callback.
    Это хороший (годный) для нас вариант.
  • При инициировании асинхронной операции нашим потоком был создан ещё один сторонний поток, внутри которого асинхронная (для нас) операция выполнялась синхронно. В конце операции предоставленный нами callback будет вызван в контексте этого чужого потока. Либо может быть целый пул таких чужих потоков, и все асинхронные операции будут распределяться по нему.
    Это плохой для нас вариант — когда наш callback будет вызван в рамках чужого потока, контекст будет не инициализирован (в простонародии — «не инициализирован рантайм») и будет исключение (если только callback не написан с применением особых мер предосторожности, о которых я (и не только я) многократно говорил)
  • Есть ещё вариант, что для вызова нашего callback-а нашему потоку назначат APC.
    Это не очень хороший для нас вариант, потому что чтобы получить APC, наш поток должен перейти в alertable-состояние ожидание, чего при обычной работе нашего потока гарантированно не происходит не происходит на гарантированном базисе.

Так вот, предположим, мы выбрали один из вариантов того, как нас оповестят о завершении условно длинной асинхронной операции.
Это может быть либо отправка нам оконного сообщения, либо вызов нашего callback-а (построенный поверх опять же доставки оконного сообщения).
Все остальные подходы превращают выполнение асинхронной операции либо в синхронную (не будет возврата из Class_Terminate, пока операция не завершится, то есть со стороны пользователя объекта выполнение долго «зависнет» на строке Set myCoolObject = Nothing), причём либо в синхронную замораживающую поток, либо в лучшем случае синхронную с риском возникновения неконтролируемой рекурсии (nested message loops).

Вот теперь мы и подошли к задаче.

Наш экземпляр внутри Class_Terminate понял, что не готов к смерти по причине того, что не выполнена некоторая асинхронная операция очистки (или логгирования или чего угодно). Он зомбифицирует себя (ставит bootstrap-ссылку) и стартует асинхронную операцию. После этого происходит возврат из Class_Terminate, объект не уничтожается, а код, который занулил последнюю из оставшихся ссылку (Set myCoolObj = Nothing) продолжает выполняться дальше, полагая, что объект благополучно уничтожился (чтобы быть честным: в COM вообще и в VB в частности никто вообще никогда не должен полагать, что после Set var = Nothing объект уничтожится, потому что ссылок на объект может быть много, и обладатель никогда не знает, у кого ещё остались ссылки на этот объект, так что он имеет оснований делать подобные предположения).

Дальше приложение продолжает работать как обычно, все его части (кроме самого класса) больше не заботятся о судьбе объекта. Но в момент «икс» асинхронная операция завершается, и тогда либо какое-то окно приложения получает сообщение, либо сразу же дёргают какой-то callback приложения. Это уведомление (вне зависимости того, каким из двух способов оно получено) и сообщает классу, что начатая им асинхронная операция закончена, и теперь можно умереть окончательно. Код класса зануляет bootstrap-ссылку и объект благополучно умирает.

Возражение о том, что нельзя в качестве callback-а указать метод подобного класса (ибо AddressOf не применим к методам классов), и что класс не может напрямую получать оконные сообщения напрямую (придётся либо создавать отдельное окно, либо сабклассить уже существующее) — они к самому вопросу прямого отношения не имеет.

Коль у вас вообще возникла необходимость получать какие-то уведомления, доставляемые в виде оконные сообщений, если вы хороший программист, вы должно написать отдельный класс (скажем CMessageReceiver), который будет создавать невидимое окно для приёма подобных сообщений и все входящие сообщения транслировать в COM-события.

Дальше будет либо один общий экземпляр CMessageReceiver на всё приложение, а все объекты, заинтересованные в получении уведомлений, доставляемых посредством оконных сообщений, подписываются на его событие, либо каждый объект, выполняющий асинхронные операции, может порождать свой собственный экземпляр CMessageReceiver и подписываться на его события.

Когда я говорю подписываться на события, я имею в виду, что наш класс, экземпляры которого оттягивают своё уничтожение, имеют внутри нечто подобное:

Код: Выделить всё
Private WithEvents NotificationWindow as CMessageReceiver
Private BootstrapRef as Object

Private Sub Class_Initialize
    Set NotificationWindow = New CMessageReceiver
End Sub

Private Sub Class_Terminate()
      Call StartLongAsyncOperation(..., NotificationWindow.hWnd)
End Sub

Private Sub NotificationWindow_OnMessage(byval msg as long, byval wParam as long, byval lParam as long)
     if msg = WM_COMPLETION_OF_ASYNC_OPERATION
           ' Вот тут мы знаем, что длинная операция кончилась и мы можем умереть окончательно
           Set BootstrapRef = Nothing
     end if
End Sub


Возвращаясь к изначальному вопросу про контекст.

Допустим, у нас есть форма, на которой есть кнопка. Экземпляр формы держит ссылку на объект класса CHeavyStuffController. По нажатию кнопки код формы зануляет свою ссылку на экземпляр CHeavyStuffController.

CHeavyStuffController, допустим, как раз тот самый случай, когда объект не может умереть, а не деинициализировав некоторые ресурсы, а ресурсы деинициализируются асинхронно и не мговенно. Это может быть управление каким-то устройством, а может быть отправка GOODBYE-пакета серверу. Мы не хотим оставить устройство в неправильном состоянии и не хотим, чтобы сервер в логах оставил кучу записей, что клиенты отключаются не попрощавшись как положено. В случае с WinMM — мы не хотим, чтобы всё рухнуло, потому в момент уничтожения объекта освободился массив (бывший частью объекта), который мы до этого скормили WinMM в качестве буфера, а код драйвера, работающий в режиме ядра, ещё не в курсе, и начал писать какие-то данные в буфер, которого давно нет.

Так вот. Обработчик клика на кнопке зануляет ссылку на экземпляр CHeavyStuffController. Если это была последняя ссылка (а предположим, что это именно так) — срабатывает Class_Terminate.

Экземпляр CHeavyStuffController инициирует асинхронную операцию, вешает bootstrap-ссылку, и возвращает выполнение из Class_Terminate. Смерти объекта не происходит. Выполнение возвращается из IUnknown::Release обратно в код обработчика клика по кнопке. С точки зрения формы объекта больше нет (ссылки на него, а есть ли сам объект нас не волнует).

Допустим, что в обработчике события Click кнопки кроме зануления ссылки больше ничего нет. Выполнение из обработчика события Click возвращается в код оконной процедуры VB-шной кнопки, а оттуда — возвращается в DispatchMessage, а из неё — в цикл прокачки оконных сообщений.

Дальше этот самый верхнеуровневый цикл прокачки сообщений крутится как обычно, и все приложение выполняется как обычно. Как будто нет никакой асинхронной операции и нет объекта, отсрочившего своё уничтожение.

Но в некоторый момент в очередь сообщений прилетает сообщение о том, что асинхронная операция завершилась. Тот же самый цикл прокачки сообщений вызывает для этого сообщения DispatchMessage, управление переходит в оконную процедуру окна, которое мы использовали для доставки сообщений (это окно, созданное экземпляром класса CMessageReceiver). Оконная процедура окна дёргает некий метод класса CMessageReceiver, а этот метод делает
Код: Выделить всё
RaiseEvent OnMessage(msg, wParam, lParam)


в результате чего все заинтересованные имеют возможность обработать пришедшее сообщение в рамках event-driven подхода с использованием VB-событий.

Может быть кто-то ещё подписался на это событие, нас это мало волнует, но как минимум на него подписался умирающий экземпляр CHeavyStuffController. Начинает выполняться NotificationWindow_OnMessage, который снимает bootstrap-ссылку и даёт экземпляру CHeavyStuffController умереть окончательно.

Либо, кстати, не снимает bootstrap-ссылку, а инициирует вторую фазу деинициализации или, в обещем случае, просто какую-то вторую асинхронную операцию. И так до тех пор, пока все асинхронные операции не будут выполнены, и тогда он снимет bootstrap-ссылку и умрёт.

Так что на вопрос «в каком контексте будет выполняться код» ответ наверное дан.

Метод Class_Terminate вызывается из IUnknown::Release, а IUnknown::Release вызывается строкой set ref = Nothing (которая сама, скорее всего, выполняется либо в контексте Sub Main, либо в контексте какого-то обработчика какого-то события, которое инициировано из какой-то оконной процедуры, которая вызвана из цикла прокачки сообщений).

Метод, который разрешает объекту умереть, так или иначе тоже вызывается из какой-то оконной процедуры, вызванной из цикла прокачки сообщений.
(Но к слову, последний может быть вызван наверное из APC, если код, который выполняет асинхронные операции, умеет нас уведомлять при помощи них, а мы согласны регулярно переходить в alertable wait state).

Debugger писал(а):Не станет ли это источником увлекательных и тяжело отлавливаемых багов?

Всё, что угодно, может стать источником тяжело отлавливаемых багов. Но не представляю, как это может стать источников увлекательных багов. Единственный побочный эффект: память освободится не сейчас, а несколько позже, когда будет можно. Саму ситуацию можно рассматривать с такого ракурса: когда объект оттягивает свою гибель, сохраняя в самом себе ссылку на себя, можно думать об этом так, как будто кто-то другой оттягивает его гибель, сохраняя ссылку на него где-то у себя. И когда этот кто-то другой перестанет зависеть от объекта, он тоже скинет эту последнюю оставшуюся ссылку. Но в нашем случае этот кто-то другой не умеет release-ить ссылку на наш объект в подходящий момент, зато умеет оповестить нас о наступлении подходящего момента. Поэтому доп. ссылку мы сохраняем живой сами у себя, и ждём оповещения о наступлении подходящего момента.

Debugger писал(а):и выполнение до заветной строчки с убиранием ссылки на себя?

Тут видимо пропущено сказуемое.

Debugger писал(а):На фоне этого, если кто-то вызвал создание этого объекта, отдавая себе отчет в том, что его завершение ресурсоемко / требует асинхронной операции - не будет логичным вызов освобождения ресурсов переложить на сторону, который этот объект создала (такой C-way)?


Нет, ибо две фундаментальных проблемы:
  1. Нет контроля за распространением ссылок на объект. Мы создали объект, у нас была ссылка на него, нам объект больше не нужен, но мы не можем быть уверены, что в данный момент нет кого-то ещё, кому объект по прежнему нужен.
  2. Типичная для ООП ситуация: та сторона, которая создала объект, уже сам давно не существует, а объект по прежнему существует. Например объект-обладающий-ресурсом был создан объектом-фабрикой (factory). Фабрика давно не существует, а у объекта, ею созданного, в своё время была куча пользователей. Например было 15 других объектов, подписавшихся на события данного объекта и обрабатывающих их. И вот постепенно все эти объекты, являющиеся юзером данного объекта, перестают ими быть. Умирают сами или просто затирают свои ссылки на данный объект. Каждый из них не знает, является ли он последним обладателем ссылки на данный момент, а просто зануляет свою ссылку. И в какой-то момент сылок не остаётся, наш объект должен произвести долгое (но к счастью — асинхронное) освобождение удерживаемых ресурсов, а фабрика, создавшая данный объект уже 100 лет не существует.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Mikle
Изобретатель велосипедов
Изобретатель велосипедов
Аватара пользователя
 
Сообщения: 4147
Зарегистрирован: 25.03.2003 (Вт) 14:02
Откуда: Туапсе

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Mikle » 17.08.2015 (Пн) 19:42

Хакер писал(а):DoEvents — достаточно тяжелая функцию, делающая много тяжелых вещей.

Это ясно, тут считай, что псевдокод, вместо DoEvents может быть любой MyDoEvents. Речь о другом.
Хакер писал(а):вся последующая обработка событий (которая в свою очередь тоже может породить какой-нибудь свой цикл) будет корнями расти из Class_Terminate мусорного объекта.

Вот это я не учёл. Просто я так никогда не делаю (не рождаю в обработчиках событий долгоживущие процессы, на сколько это вообще грамотно с точки зрения стиля программирования?), единственное исключение - когда весь код проекта расположен в модуле формы, главный цикл (если он вообще есть) может быть расположен прямо внутри Form_Load.

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Хакер » 17.08.2015 (Пн) 21:08

Mikle писал(а):Просто я так никогда не делаю (не рождаю в обработчиках событий долгоживущие процессы, на сколько это вообще грамотно с точки зрения стиля программирования?)


А что ты считаешь долгоживущим процессом? Тот же MsgBox или InputBox — это собственный долгоиграющий цикл прокачки сообщений. Или показ экземпляра формы модально. Или много что ещё (любые диалоги ComDlg, SHBrowseForFolder).

Откуда ещё показывать диалоги, кроме как из обработчиков событий?
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Mikle
Изобретатель велосипедов
Изобретатель велосипедов
Аватара пользователя
 
Сообщения: 4147
Зарегистрирован: 25.03.2003 (Вт) 14:02
Откуда: Туапсе

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Mikle » 18.08.2015 (Вт) 9:22

Хакер писал(а):показ экземпляра формы модально

Вот это, пожалуй, самый "долгоживущий" пример. Диалоги и MsgBoxы - явления временные по определению, ничего страшного, если они провисят "под аккомпанемент" пустого цикла со Sleepом.
Хакер писал(а):Далее он имеет возможность дождаться момента, когда завершатся все стадии освобождения занятых ресурсов, и в конце-концов сбросить ссылку SelfBootstrap — и умереть окончательно.

А можно подробнее? Мы должны дать объекту извне команду сбросить ссылку SelfBootstrap, или, может быть, объект, уже выполнивший Class_Terminate, но ещё не уничтоженный, всё ещё принимает сообщения о событиях, то есть мы, к примеру, можем подписаться на сообщение от мультимедиа сервиса об уничтожении нашего контента? А если сервис завис, а мы завершаем свою программу, объект желательно уничтожить в любом случае, мы должны послать объекту сообщение самоуничтожиться, но все ссылки на объект мы уже давно уничтожили, как послать сообщение? Не выполнять же End?

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Хакер » 18.08.2015 (Вт) 10:13

Mikle писал(а):Вот это, пожалуй, самый "долгоживущий" пример. Диалоги и MsgBoxы - явления временные по определению, ничего страшного, если они провисят "под аккомпанемент" пустого цикла со Sleepом.

Ещё как страшно.
Предположим, в момент, пока горит MsgBox, сработает некий таймер, который зануляет ссылку на второй такой объект с затянутой терминацией, и ссылка оказывается последний. Последствия надо описывать?

Mikle писал(а):Мы должны дать объекту извне команду сбросить ссылку SelfBootstrap, или, может быть, объект, уже выполнивший Class_Terminate, но ещё не уничтоженный, всё ещё принимает сообщения о событиях, то есть мы, к примеру, можем подписаться на сообщение от мультимедиа сервиса об уничтожении нашего контента?

Эмм... Ну я же привёл выше пример в виде кода. Что может быть подробнее?
Код: Выделить всё
    Private WithEvents NotificationWindow as CMessageReceiver
    Private BootstrapRef as Object

    Private Sub Class_Initialize
        Set NotificationWindow = New CMessageReceiver
    End Sub

    Private Sub Class_Terminate()
          Call StartLongAsyncOperation(..., NotificationWindow.hWnd)
    End Sub

    Private Sub NotificationWindow_OnMessage(byval msg as long, byval wParam as long, byval lParam as long)
         if msg = WM_COMPLETION_OF_ASYNC_OPERATION
               ' Вот тут мы знаем, что длинная операция кончилась и мы можем умереть окончательно
               Set BootstrapRef = Nothing
         end if
    End Sub


Объект после Class_Terminate по прежнему, если мы не дали ему умереть, по прежнему остаётся подписанным на события других объектов, на которые он был подписан до Class_Terminate. Объект по прежнему может принимать уведомления о событиях других объектов, либо же просто принимать внешние вызовы своих методов. Так что объект может быть извещён о том, что длинна асинхронная операция завершилась, и при том — несколькими путями. Каким именно — в сушности не важно.

Mikle писал(а):А если сервис завис, а мы завершаем свою программу, объект желательно уничтожить в любом случае, мы должны послать объекту сообщение самоуничтожиться, но все ссылки на объект мы уже давно уничтожили, как послать сообщение?

Во-первых, завершающаяся штатно или нештатно программа всё равно терминирует все инстансы своих классов. Освободит память, занулит все данные, Release'нит ссылки на внешние объекты.

Так что если ты переживаешь за память и за внешние объекты (из чужих процессов или даже машин) — не стоит.

Если ты переживаешь, что не будет отпущен системный ресурсы — ну так он и так не будет отпущен?

То есть, скажем, у тебя был открыт MIDI IN порт. Ты вызыва midiInReset. Дальше ты не имеешь права освобождать память, отведённую под буферы и заголовки буферов (структура MIDIHDR), пока для каждого буфера не получишь MM_MIM_DONE. Но ты их не получаешь и не получаешь. Ждёшь и ждёшь. А их всё нет и нет.

Не ждать этого и освобождать буферы — значит получить скорый потенциальный крах.

Вызывать midiInUnprepereBuffer не дожидаясь — не выйдет, система вернёт MMSYSERR_STILLPLAYING.
Закрывать устройство с помощью midiInClose — тоже самое — не выйдет, система вернёт MMSYSERR_STILLPLAYING.

Иными словами, проблема есть сама собой, она возникла не от предложенного мною трюка. И решения у неё два: ждать до посинения, либо так и оставить MIDI IN открытым.

Тут я прихожу с описанием своей хитрой техники, но проблему это никак не переиначивает. Либо придётся ждать, либо придётся плюнуть. Два завершающегося приложения вариант «плюнуть» не такой и страшный — в случае с MIDI- или Wave- устройствами после смерти процесса система всё равно их «отпустит».

Mikle писал(а):но все ссылки на объект мы уже давно уничтожил

Мне кажется, что здесь ты упускаешь важное.
Во-первых, у нас нет ссылок на объект. Но у объекта вполне возможно, что есть ссылка на нас. Ссылка, образованной подпиской на наши события. Так что если у нас нет ни одной сильной ссылки на объект, это не значит, что мы потеряли связь с объектом. Механизм подписки на события (WithEvents) использует слабые ссылки для того, чтобы источник событий помнил своих подписчиков.

Во-вторых, всегда вдобавок к началу асинхронной операции можно запускать таймер. Если сообщение о завершении асинхронной операции не придёт, то придёт сообщение о таймауте, и тогда мы плюнем и умрём так.

Но со, скажем, объектами, олицетворяющими MIDI-порты я бы так не делал. Пусть лучше, пока не придёт MM_MIM_DONE, объект не умирает. Всё равно, пока он не умрёт, порт занят нашим процессом, и открыть его второй раз не удастся (зависит от драйвера на самом деле). Придёт MM_MIM_DONE через час — значит мы умрём, и другие объекты получат зелёный свет на рождение. Не придёт — так и проживёт объект до конца работы процесса, олицетворяя открытый порт,который сама система не позволяет нам закрыть.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Mikle
Изобретатель велосипедов
Изобретатель велосипедов
Аватара пользователя
 
Сообщения: 4147
Зарегистрирован: 25.03.2003 (Вт) 14:02
Откуда: Туапсе

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Mikle » 18.08.2015 (Вт) 11:31

Хакер писал(а):Предположим, в момент, пока горит MsgBox, сработает некий таймер, который зануляет ссылку на второй такой объект с затянутой терминацией, и ссылка оказывается последний. Последствия надо описывать?

Терминация второго объекта родится из DoEvents первого, соответственно первый будет ждать не только уничтожения своего контента, но и уничтожения второго объекта... более тяжких последствий пока не вижу.
Хакер писал(а):Объект после Class_Terminate по прежнему, если мы не дали ему умереть, по прежнему остаётся подписанным на события других объектов, на которые он был подписан до Class_Terminate. Объект по прежнему может принимать уведомления о событиях других объектов, либо же просто принимать внешние вызовы своих методов.

Вот это "просто принимать внешние вызовы своих методов" не совсем ясно. Откуда этот вызов поступит, если нет ссылок, как тут поможет ссылка внутри объекта на нас?
Про сообщение о событии - это я и сам понял.

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Есть ли жизнь после Class_Terminate, или вечноживущий об

Сообщение Хакер » 18.08.2015 (Вт) 11:53

Mikle писал(а):Терминация второго объекта родится из DoEvents первого, соответственно первый будет ждать не только уничтожения своего контента, но и уничтожения второго объекта... более тяжких последствий пока не вижу.

Ещё более запутанно: терминация второго родится из цикла внутри функции MessageBox, вызванной из DoEvents первого.
Если MsgBox спрашивал нас насчёт какого-то действия Yes/No, то действие нифига не выполнится, пока второй объект не терминируется (а допустим это занимает 15 секунд). Пользователь подумает, что мы идиоты, потому что мы написали программу, которая спрашивает что-то, но ничего не делает после того, как мы ответили.

В общем, это совершенно отстойное решение, потому что оно приводит к плохо прогнозируемым вложенным циклам (с плохо прогнозируемой глубиной вложенности). Избежать этих вложенных циклов можно блокируя UI, таймеры и все входящие сообщение, способные вызвать какие-то события на период ожидания. Но это-то как раз и превращает асинхронную операцию в фактически синхронную (всё приложение ждёт терминации какого-то жалкого объекта, который никому больше не нужен).

А наша цель — писать классные асинхронные приложения. Ну что было бы, если бы при закрытии одного окна проводника на несколько секунд блокировался бы UI всех остальных окон проводника?

Mikle писал(а):Вот это "просто принимать внешние вызовы своих методов" не совсем ясно.

Как по твоему работает механизм событий, когда один объект делает RaiseEvent, а у другого начинает работать обработчик соответствующего события?
На всякий случай напомню, что работает он так: когда второй объект имеет WithEvents-переменную, то когда ей присваивают ссылку на первый объект, в момент присвоения ссылки второй объект обращается к первому и передаёт ему ссылку на себя со словами «я подписываюсь на твои события». Эта ссылка — слабая, то есть он не увеличивает счётчик ссылок. Первый объект держит список ссылок на подписчиков. RaiseEvent проходится по всему список ссылок на подписчиков, перебирает их и у каждого вызывает метод-обработчик.

Когда во втором объекте меняется значение WithEvents-переменной (была сслыка на первый объект, а становится ссылка на третий или на Nothing), то перед изменением значения переменной второй объект обращается к первому со словами «я отписываюсь от тебя», и первый удаляет ссылку на второй из своего списка подписчиков.

Так что умирающий объект имеет сильную ссылку на инстанс CMessageReceiver (и удерживает его от гибели), а инстанс CMessageReceiver имеет слабую ссылку на CHeavyStuffController (вернее на его sink-интерфейс — то есть интерфейс с событийными обработчиками).

Когда приходит оконное сообщение о конце длинной операции, оно транслируется в объектный вызов к инстансу CMessageReceiver, а тот в свою очередь делает RaiseEvent, что приводит к вызову обработчика события внутри инстанса CHeavyStuffController. Инстанс CHeavyStuffController понимает, что всё кончено и стирает bootstrap-ссылку. Умирая, у него затирается его WithEvents переменная, что как минимум ликивидирует подписку (слабую ссылку из инстанса CMessageReceiver на инстанс CHeavyStuffController), а как максимум — устраняет последнюю ссылку на инстанс CMessageReceiver (если тот был в эксклюзивном владении умирающего объекта, а не один глобальный на всё приложение).
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.


Вернуться в МануAll

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 2

    TopList