Руководство программиста
Глава 2. Создание моделей блоков
§2.13. Вызов функций блоков
§2.13.5. Отложенный вызов функций блоков
Рассматривается отложенный вызов функций блоков, позволяющий избежать переполнения стека при глубокой рекурсии вызовов. В примере, рассмотренном в §2.13.4, прямые вызовы заменяются на отложенные.
Рассмотренный в §2.13.4 пример имеет один существенный недостаток, из-за которого созданная модель не будет работать в графах с очень длинными маршрутами. Причина этого в способе, которым мы осуществляем разметку графа.
Когда функция GraphPath_FindPath помечает начальный блок маршрута значением 0, функция модели этого блока вызывается в режиме RDS_BFM_FUNCTIONCALL для реакции на «ProgrammersGuide.GraphPath.Mark». Модель блока, реагируя на вызов, вызывает функцию GraphNode_OnMarkBlock, которая, начав перебор соседних блоков, помечает первый из них, то есть вызывает его модель для реакции на ту же самую функцию «ProgrammersGuide.GraphPath.Mark». Эта модель тоже начинает помечать соседние блоки, и опять вызывает модель одного из них. В результате, если в графе существует какой-либо маршрут, состоящий из N блоков, мы получим N вызовов функции модели блока, произведенных один из другого. Поскольку каждый вызов функции занимает в стеке определенное место, тем большее, чем больше у нее параметров и локальных переменных, при размерах графов в несколько тысяч блоков мы можем столкнуться с переполнением стека – его объем не безграничен. Таким образом, попытка найти кратчайший путь между блоками в очень большом графе приведет к аварийному завершению RDS, что будет для пользователя полной неожиданностью, ведь граф, который он нарисовал, умещается в оперативную память, и, следовательно, для его обработки должно хватить ресурсов.
Чтобы избавиться от проблемы переполнения стека из-за большого количества рекурсивных вызовов, можно использовать механизм отложенного вызова функций блоков. При отложенном вызове функция блока не вызывается немедленно, вместо этого ее параметры копируются в специально отведенную область памяти, после чего управление немедленно возвращается вызвавшей функции. До тех пор, пока функция модели не завершится, все отложенные вызовы, сделанные из этой функции, будут ставиться в очередь. Сразу после завершения функции модели отложенные вызовы начнут выполняться один за другим, и, после выполнения каждого из них, память, отведенная под параметры функции, будет освобождаться.
Может показаться, что отложенные вызовы не дают никакого выигрыша: раньше параметры функций при вызове помещались в стек, теперь память под них отводится отдельно, но они все равно продолжают занимать объем тем больший, чем больше функций вызвано. Однако, здесь есть принципиальная разница: при отложенных вызовах память под параметры отводится в динамической области, так называемой «куче» (heap), объем которой, как правило, существенно больше объема стека. Фактически, он ограничен только объемом оперативной памяти, доступной программе. В этой же памяти размещаются данные всех блоков, связей и вспомогательных объектов, созданных RDS, поэтому, если в эту память уместилась созданная пользователем схема, то, вероятнее всего, несколько десятков дополнительных байтов для каждого из ее блоков (а параметры функций редко занимают больше) не приведут к переполнению памяти. Кроме того, в стек помещаются не только параметры, используемые при вызове функции блока, но и все локальные переменные каждой из выполняемых функций. Избавившись от рекурсивных вызовов модели блока мы, тем самым, перестаем заполнять стек этими локальными переменными. Функции моделей при отложенных вызовах вызываются не друг из друга, а по очереди, поэтому размер стека не увеличивается, сколько бы моделей подряд мы не вызвали: следующая модель вызовется только после завершения предыдущей и освобождения занятой ей памяти в стеке.
Для отложенного вызова функции блока используется сервисная функция rdsQueueCallBlockFunction:
void RDSCALL rdsQueueCallBlockFunction( RDS_BHANDLE Block, // Блок, у которого вызывается функция int FuncId, // Идентификатор вызываемой функции LPVOID ParamBuf, // Указатель на область параметров DWORD ParamBufSize, // Размер области параметров DWORD Flags); // Флаги
В первых трех параметрах этой функции, как и у уже знакомой нам rdsCallBlockFunction, передаются идентификатор вызываемого блока, идентификатор вызываемой в нем функции и указатель на начало области параметров этой функции (указатель произвольного типа, то есть void*). Дальше начинаются различия: при прямом вызове функций RDS немедленно передает указатель на область параметров в модель вызванного блока и ждет, пока та не вернет управление, поэтому rdsCallBlockFunction не требуется знать размер этой области. При отложенном же вызове необходимо сделать копию области параметров функции, а для этого необходимо знать ее размер. По этой причине в четвертом параметре rdsQueueCallBlockFunction передается размер области, указатель на которую передан в третьем параметре. В пятом параметре указываются флаги, управляющие постановкой отложенного вызова функции в очередь: флаг RDS_BCALL_FIRST поставит вызов в начало очереди, флаг RDS_BCALL_LAST – в конец. Один из этих флагов может быть скомбинирован побитовым ИЛИ с уже знакомым нам устаревшим флагом RDS_BCALL_CHECKSUPPORT, что позволит перед вызовом функции проверить, поддерживает ли ее данный блок.
При всех достоинствах отложенных вызовов функций блоков у них есть и недостатки, поэтому не во всех случаях они могут заменить прямые вызовы. Прежде всего, параметры, передаваемые при отложенном вызове, не должны содержать внутри себя указателей на какие-либо локальные объекты (строки, структуры, массивы и т.д.), созданные в вызывающей функции. При отложенном вызове вызвавшая функция модели завершится раньше, чем будет произведен вызов, и все ее локальные переменные будут уничтожены, поэтому указатели в скопированной области параметров будут ссылаться на уже освобожденную память. Допустим, например, что мы хотим включить в параметры какой-либо функции блока указатель на строку, и описываем структуру параметров этой функции следующим образом:
typedef struct { DWORD servSize; // Сюда будет записан размер структуры char *String; // Указатель на строку double Val1,Val2; // Какие-то другие параметры функции } TFunction3Params;
Будем считать, что эта функция уже зарегистрирована, и ее идентификатор находится в глобальной переменной Function3Id. Теперь мы хотим произвести отложенный вызов этой функции у какого-то блока:
RDS_BHANDLE block=… // Здесь должен быть идентификатор // вызываемого блока TFunction3Params params; // Структура параметров функции char buf[100]; // Вспомогательный массив params.servSize=sizeof(params); // Присваиваем размер структуры params.Val1=10.0; // Записываем параметры params.Val2=15.0; // Формируем строку во вспомогательном массиве sprintf(buf,"Val1+Val2=%lf",params.Val1+params.Val2); // Записываем указатель на строку в структуру параметров params.String=buf; // Отложенный вызов функции rdsQueueCallBlockFunction(block,Function3Id, ¶ms,sizeof(params), RDS_BCALL_LAST);
При вызове rdsQueueCallBlockFunction RDS сделает копию структуры params (то есть копию области памяти с начальным адресом ¶ms и размером sizeof(params)) и немедленно вернет управление. Поле String скопированной структуры при этом будет ссылаться на массив buf, расположенный в стеке функции, фрагмент которой приведен выше. Как только эта функция завершится, массив buf будет уничтожен вместе со всеми ее локальными переменными. Теперь поле String скопированной структуры ссылается на уничтоженные данные – когда дело дойдет до вызова функции блока, это не приведет ни к чему хорошему.
Может возникнуть соблазн обойти эту проблему следующим образом: отводить строку динамически при помощи сервисной функции rdsAllocate, чтобы она не уничтожилась при завершении создавшей ее функции, а обязанность освобождения памяти, занятой строкой, переложить на вызываемую модель блока. То есть изменить приведенный выше текст следующим образом:
RDS_BHANDLE block=… // Здесь должен быть идентификатор // вызываемого блока TFunction3Params params; // Структура параметров функции char *buf; // Вспомогательный массив buf=(char*)rdsAllocate(100); // Отводим память под массив params.servSize=sizeof(params); // Присваиваем размер структуры params.Val1=10.0; // Записываем параметры params.Val2=15.0; // Формируем строку во вспомогательном массиве sprintf(buf,"Val1+Val2=%lf",params.Val1+params.Val2); // Записываем указатель на строку в структуру параметров params.String=buf; // Отложенный вызов функции rdsQueueCallBlockFunction(block,Function3Id, ¶ms,sizeof(params), RDS_BCALL_LAST);
Однако, решив проблему с преждевременным уничтожением строки, мы создадим новую: если окажется, что вызванная модель блока не поддерживает эту функцию, она не освободит память, отведенную под строку, что приведет к утечке памяти. Поэтому лучше всего взять за правило: при отложенном вызове функций – никаких указателей на локальные или динамически создаваемые объекты в параметрах.
Разумеется, в приведенном примере ничто не мешает включить в структуру параметров функции не указатель на строку, а массив символов фиксированного размера:
typedef struct { DWORD servSize; // Сюда будет записан размер структуры char String[100]; // Строка (массив символов) double Val1,Val2; // Какие-то другие параметры функции } TFunction3Params;
В этом случае структура параметров не содержит указателей на внешние объекты, и никаких проблем при отложенном вызове функции с такими параметрами не будет.
Второй крупный недостаток отложенных вызовов – невозможность получения значения, возвращенного вызванной функцией. rdsQueueCallBlockFunction только готовит данные для отложенного вызова, а сам вызов будет произведен позднее, когда вызвавшая функция уже завершится. Если вызывающая модель хочет получить от вызываемой какой-то ответ, для этого нужно предпринимать специальные действия. Например, вызванная функция модели может считать из поля Caller структуры RDS_FUNCTIONCALLDATA идентификатор вызвавшего ее блока, и вызвать у него в ответ какую-либо специально разработанную для этого функцию, передав в ее параметрах результат выполнения отложенного вызова. Это несколько усложняет создание моделей блоков, поскольку вызов функции и получение ее результата оказываются разнесены во времени.
Как и прямым вызовом, отложенным можно вызывать не только функцию конкретного блока, но и функции всех блоков какой-либо подсистемы. Для этого служит сервисная функция rdsBroadcastFuncCallsDelayed:
void RDSCALL rdsBroadcastFuncCallsDelayed( RDS_BHANDLE System, // Подсистема, блоки которогй вызываются int FuncId, // Идентификатор вызываемой функции LPVOID ParamBuf, // Указатель на область параметров DWORD ParamBufSize, // Размер области параметров DWORD Flags); // Флаги
Ее параметры аналогичны параметрам функции rdsQueueCallBlockFunction, за исключением того, что в первом параметре передается не идентификатор вызываемого блока, а идентификатор подсистемы, блоки которой вызываются. В флагах этой функции можно указывать не только RDS_BCALL_CHECKSUPPORT, RDS_BCALL_FIRST и RDS_BCALL_LAST, но и уже знакомые нам по rdsBroadcastFunctionCallsEx RDS_BCALL_SUBSYSTEMS и RDS_BCALL_ALLOWSTOP.
Вернемся к нашему примеру, в котором мы ищем кратчайший путь между двумя узлами графа. Чтобы перевести функции «ProgrammersGuide.GraphPath.Mark» и «ProgrammersGuide.GraphPath.BackTrace» на отложенный вызов, достаточно изменить всего две из написанных нами вспомогательных функций – GraphPath_FindPath и GraphPath_MarkBlock_Callback. Начнем с первой из них:
// Поиск маршрута в графе в заданной подсистеме void GraphPath_FindPath(RDS_BHANDLE System) { RDS_BHANDLE StartBlock,EndBlock; TProgGuideFuncMarkParams markparams; // Считаем, что маркировка всего графа сброшена // Ищем начальную и конечную точку маршрута if(!GraphPath_GetTerminalBlocks(System,&StartBlock,&EndBlock)) return; // Начало или конец не найдены // Начало маршрута – StartBlock, конец - EndBlock // Маркируем граф от начала маршрута markparams.servSize=sizeof(markparams); markparams.Mark=0.0; // Начало маркируется значением 0 markparams.Previous=NULL;// Это значение не пришло от какого-то // соседнего блока // Вызываем функцию маркировки для начального блока // rdsCallBlockFunction(StartBlock,GraphFuncMark,&markparams); rdsQueueCallBlockFunction(StartBlock,GraphFuncMark, &markparams,sizeof(markparams),RDS_BCALL_FIRST); // Теперь отслеживаем кратчайший путь в обратном направлении // (от конечного блока) // rdsCallBlockFunction(EndBlock,GraphFuncBackTrace,NULL); rdsQueueCallBlockFunction(EndBlock,GraphFuncBackTrace, NULL,0,RDS_BCALL_LAST); } //=========================================
В этой функции мы в двух местах заменили rdsCallBlockFunction на rdsQueueCallBlockFunction, но флаги вызова мы при этом используем разные. Нам нужно сначала разметить весь граф, и только потом вызвать у блока конца маршрута функцию «ProgrammersGuide.GraphPath.BackTrace» для выделения найденного пути. Раньше это получалось само собой: первый вызов rdsCallBlockFunction для функции «ProgrammersGuide.GraphPath.Mark» (идентификатор функции находится в переменной GraphFuncMark) не возвращал управления до тех пор, пока весь граф не оказывался размеченным, и только потом вызывалась функция выделения пути (идентификатор – в GraphFuncBackTrace). Теперь функция rdsQueueCallBlockFunction возвращает управление сразу, поэтому нам нужно принять меры, чтобы функция выделения пути «ProgrammersGuide.GraphPath.BackTrace» вызвалась только после того, как прекратятся все отложенные вызовы функции разметки «ProgrammersGuide.GraphPath.Mark». Для этого достаточно все время ставить функцию разметки в начало очереди, используя флаг RDS_BCALL_FIRST, а функцию выделения – в конец, с флагом RDS_BCALL_LAST. Таким образом, пока в очереди будет находиться хотя бы один отложенный вызов функции разметки, до вызова функции выделения дело не дойдет.
В функции GraphPath_MarkBlock_Callback нам тоже нужно заменить прямой вызов функции разметки на отложенный:
// Функция обратного вызова для GraphNode_OnMarkBlock BOOL RDSCALL GraphPath_MarkBlock_Callback( RDS_PPOINTDESCRIPTION src, RDS_PPOINTDESCRIPTION dest,LPVOID data) { TProgGuideFuncMarkParams *src_params= (TProgGuideFuncMarkParams*)data; TProgGuideFuncMarkParams dest_params; double ArcLen; // … // начало функции – без изменений // … // Помечаем найденный соседний блок суммой маркировки данного // блока (src_params->Mark) и длины дуги к найденному (ArcLen) dest_params.servSize=sizeof(dest_params); dest_params.Mark=src_params->Mark+ArcLen; dest_params.Previous=src->Block; // Блок, от которого // пришла метка // rdsCallBlockFunction(dest->Block,GraphFuncMark,&dest_params); rdsQueueCallBlockFunction(dest->Block,GraphFuncMark, &dest_params,sizeof(dest_params),RDS_BCALL_FIRST); return TRUE; } //=========================================
Здесь мы тоже постоянно ставим вызов функции «ProgrammersGuide.GraphPath.Mark» в начало очереди. Вызов этой функции у какого-либо узла графа добавляет в начало очереди такие же вызовы для его соседей, при их выполнении в начало очереди добавляются вызовы уже для их соседей и т.д., а единственный вызов функции выделения, сделанный в GraphPath_FindPath, будет оставаться в конце этой очереди.
Вызов функции «ProgrammersGuide.GraphPath.BackTrace» в конце функции GraphNode_OnBackTracePath тоже нужно сделать отложенным:
// Проследить и выделить маршрут от данного блока в // обратном направлении void GraphNode_OnBackTracePath(RDS_PBLOCKDATA BlockData) { RDS_BHANDLE PrevBlock; RDS_CHANDLE PrevConn,c; // … // начало функции – без изменений // … if(PrevBlock) { // Визуально выделяем связь MarkConnection(PrevConn); // Вызываем функцию обратного прослеживания маршрута // от найденного блока // rdsCallBlockFunction(PrevBlock,GraphFuncBackTrace,NULL); rdsQueueCallBlockFunction(PrevBlock,GraphFuncBackTrace, NULL,0,RDS_BCALL_LAST); } } //=========================================
Для оставшихся функций блоков, используемых в этом примере, перевод на отложенные вызовы не нужен: они не вызываются рекурсивно и не могут привести к переполнению стека.
Теперь наша модель сможет искать пути даже в очень больших и сильно разветвленных графах. Функции, которые мы перевели на отложенные вызовы, не возвращали никаких значений и их параметры не содержали указателей (функция «ProgrammersGuide.GraphPath.BackTrace» вообще не имеет параметров), поэтому изменения, которые нам пришлось внести, оказались довольно небольшими.
Следует помнить, что основное назначение отложенных вызовов – борьба с переполнением стека из-за глубокой рекурсии. Во всех остальных случаях использование прямых вызовов более целесообразно.