1

Тема: Исследование порядка вызовов Callback-функций в QLua

(Обновлено 28.12.2012 )

Как подключать свои DLL-модули к Lua (QLua) и вызывать из этой Dll свои функции - подробно написано в отдельной теме.

В данном случае подключить DLL мне понадобилось для того, чтобы получить доступ к WinAPI, в частности для исследования в каких потоках какие функции обратного вызова вызываются из QLua.

А сегодня вот о чем. Сделаем такой LUA-скрипт (полный исходник можно скачать по ссылке в конце этого поста):

local testc = require("testc")

f = nil
is_run = true

function writeTyreadID(proc_name)
    f:write(os.date() .. " ".. proc_name .. ":" .. tostring(testc:GetCurrentThreadId()) .. "\n")
    f:flush()
end

function OnInit(path)
    f = io.open(path .. ".out", "w+t")
    writeTyreadID("OnInit")
end

function OnStop(signal)
    writeTyreadID("OnStop")
    is_run = false
end

function main()
    writeTyreadID("main start")
    while is_run do
        sleep(50)
    end
    writeTyreadID("main stop")
    f:close()
end

-------------------

function OnAccountBalance(acc_bal)
    writeTyreadID("OnAccountBalance")
end

function OnAccountPosition(acc_pos)
    writeTyreadID("OnAccountPosition")
end
..........

Под чертой я просто вставил все обработчики, какие перечислены в документации.
Что мы тут видим: подключение внешнего модуля, потом типичная теперь уже функция main() и обработчики инициализации OnInit () и остановки скрипта OnStop, функция writeTyreadID() для записи в файл имени обработчика и ID потока, в рамках которого он вызван. За одно пишется время, чтобы было удобно потом смотреть что и когда происходило, сопоставлять. Идея всей этой затеи с логированием  всех обработчиков была в том, чтобы обнаружить те из них, которые, возможно, вызываются в разных потоках, а значит обрабатываются параллельно.
В итоге в результате запуска и остановки скрипта получился такой лог, повторяющиеся строки я здесь вырезал (полный лог пишется в файле test-all-on.lua.out рядом со скриптом):

12/19/12 09:35:52 OnInit:2584
12/19/12 09:35:52 main start:2592
12/19/12 09:36:25 OnConnected:2584
12/19/12 09:36:25 OnParam:2584
12/19/12 09:36:25 OnParam:2584
12/19/12 09:36:25 OnQuote:2584
12/19/12 09:36:25 OnQuote:2584
12/19/12 09:36:25 OnDepoLimit:2584
12/19/12 09:36:25 OnDepoLimit:2584
12/19/12 09:36:26 OnParam:2584
12/19/12 09:36:32 OnQuote:2584
12/19/12 09:36:33 OnParam:2584
12/19/12 09:36:38 OnFuturesLimitChange:2584
12/19/12 09:36:39 OnParam:2584
12/19/12 09:36:40 OnParam:2584
12/19/12 09:36:38 OnFuturesLimitChange:2584
12/19/12 09:36:40 OnParam:2584
12/19/12 09:37:23 OnQuote:2584
12/19/12 09:37:30 OnParam:2584
12/19/12 09:37:30 OnTrade:2584
12/19/12 09:37:30 OnOrder:2584
12/19/12 09:37:30 OnDepoLimit:2584
12/19/12 09:37:31 OnParam:2584
12/19/12 09:38:33 OnAllTrade:2584
12/19/12 09:38:33 OnAllTrade:2584
12/19/12 09:38:33 OnAllTrade:2584
12/19/12 09:38:36 OnAllTrade:2584
12/19/12 09:38:57 OnParam:2584
12/19/12 09:38:58 OnParam:2584
12/19/12 09:39:02 OnStop:2584
12/19/12 09:39:02 main stop:2592

Из него и нарисовалась картинка в соседней теме "Как устроено Lua в QUIK, правда к моменту ее написания все обработчики я еще не посмотрел.
Увы, к большому сожалению, документация и свежие сообщения на форуме от разработчиков в данном случае нас не обманывали: в отдельном потоке (с отличающимся ID потока) выполняется только функция main(), все остальные обработчики (во всяком случае все, вызов которых удалось увидеть) вызываются в рамках одного (основного) потока терминала QUIK. При этом при каждом запуске даже одного и того же скрипта ID потока в main() каждый раз разный, значит при каждом запуске создается новый поток OC Windows. Для других обработчиков ID потока остается неизменным, т.е. используется один и то же основной поток QUIK.

Обещанные в начале исходники:
Полный скрипт и готовая DLL-ка в архиве.

2

Re: Исследование порядка вызовов Callback-функций в QLua

< reserved >

3

Re: Исследование порядка вызовов Callback-функций в QLua

Результат чего и в каком виде вы хотите получить? не понятно.

Код VCLua - он доступен и не так уж велик.
Родить поток или контекст виртуальной машины магическими пассами - нельзя; можно это сделать лишь вызовом определённых всем известных функций. Достаточно убедиться, что таких функций в коде не встречается, что не сложно - и ответ станет очевиден на 100%.

Более того. Обычно никто в здравом уме не плодит ни потоки, ни новые контексты, используя Lua. Хотя бы потому, что это весьма нетривиально и требует от всех участников весьма высокого уровня знаний и умений. (Как пример: тьма проблем в QUIK, которые они не так давно кое-как смогли выгрести из своей многопоточной (зачем?!) реализации в Lua. Плюс тьма оставшихся принципиальных проблем, когда идет обращение к одним и тем же переменным (обычно созданным Lua-программистам таблицам), к которым осуществляется доступ как из main(), так и из callback'ов. Но эти проблемы уже так и останутся на совести тех, кто в Lua пишет роботов и им явно не позавидуешь.)

Так что бояться, что кто-то где-то в другой библиотеке начнёт такое городить - не стоит.

Что же до нестабильностей VCLua, то я вижу три их природы:

  • Для Lazarus вполне естественно в случае любых проблем (например, указан номер отсутствующего столбца в Grid'е) кинуть исключение; в самом Lazarus это спокойно обрабатывается, отображается окно в ошибкой - и программа в принципе продолжает работать. Т.е. исключения как стандарный и нормальный механизм сообщений об ошибках. В VClua такие исключения приводят уже к неминуемому краху терминала; причем отключить их "легким движением руки" - уже, возможности нет. Единственная у меня идея борьбы с этой проблемой - все интерфейсные функции, которые вызывает Lua-машина из VLua обернуть обработчиком исключений; но таких точек входа - много, как это сделать более-менее элегантно - не придумал; кстати, оригинальная VCLua в случае любой проблемы старается еще и стопнуть свой процесс (в смысле Windows-процесс), это место я постарался победить в QVCLua.

  • Проблема с завершением я думаю, хотя экспериментов еще не проводил, связана вот с чем: вызвали Form:Release(), форма фактически разрушена и связанный с ней объект Lazarus удалён; но, на сколько я понимаю код, Lua-переменная Form - она никуда не делась, а значит вполне вероятны какие-то обращения к её методам, но при этом фактически Lazarus-объекта уже нет, однако у нему происходит обращение - ну и привет, наступаем на невалидную память, всё разваливается. Пока это только предположения, я попробую на каком-то показательном эксперименте проработать так ли это.

  • Ну и конечно же проблема многопоточного  доступа. Но тут всё "просто": есть жесткие рекомендации по корректной работе:

    • создавать форму и прочие компоненты в основном теле скрипта или callback-ах, т.к. они работают в рамках осноновго потока и проблем нет;

    • не обращаться ни к каких элементам VCLua из main() (c этим пунктом теоретически можно попробовать как-то "побороться" или, хотя бы, предоставить вариант решения как из main() все же обратиться (изменить свойства) к какому-то элементу; но это дело будущего)

4

Re: Исследование порядка вызовов Callback-функций в QLua

sam063rus пишет:

я ничего не путаю. если ты 100% уверенный - то, где результат? почему vcl не стабильна? (без обид)
Лично я - не 100% уверен и даже не 70. Я пытаюсь рассматривать и принимать во внимание все варианты. То что, любой код вызывается и работает в контексте вызвавшего его потока ещё не говорит о том, что вызываемый код сам не плодит потоки и дочерние/не дочерние контексты виртуальных машин. для меня тут - масса вопросов. насчёт уже якобы составленной карты потоков - я пока не вижу, что она отражает реальную картину. также, я считаю крайне важно оценить/измерить/учесть влияние главного контекста виртальной машины на поведение порождённых. как они влияют друг на друга в квике, как и в какой момент, в какой последовательности разрушаются и полностью ли, как это сказывается на других контекстах, почему квик не отрабатывает или не зарегистрировал должным образом функцию обработки исключений, почему всего один скрипт может уничтожить работу всех остальных скриптов, а значит и виртуальных машин, а также ввести квик в ступор. и чтобы на эти вопросы ответить я предлагаю детально изучить механизм работы скриптов в квике, а начать разумеется, с вышеописанного. таким образом, по результатам исследований, у нас будет аргументированное требование к разработчикам.

Давайте я попробую тезисно объяснить некоторые вещи.
1. каждый скрипт выполняется максимум в двух потоках - колбеки и main()
2. Никакая вызываемая функция не порождает дополнительные потоки ОС
3. Полноценно обработать все исключения не представляется возможным, тут как повезет.
И еще по поводу vcl, почему стали происходить падения. Анализ дампов показывает, что после закрытия Lua ( lua_close() ) происходит попытка вызова какого-то кода из уже отгруженной библиотеки, но детально я в этом не разбирался.

5 (2015-02-02 10:50:22 отредактировано swerg)

Re: Исследование порядка вызовов Callback-функций в QLua

mbul пишет:

2. Никакая вызываемая функция не порождает дополнительные потоки ОС

Ну справедливости ради стоит наверное можно сказать, что в общем случае какая-либо библиотека вполне может родить потоки; но мало кто в здравом уме захочет идти этим тернистым путём wink

mbul пишет:

Анализ дампов показывает, что после закрытия Lua ( lua_close() ) происходит попытка вызова какого-то кода из уже отгруженной библиотеки, но детально я в этом не разбирался.

А вот это вполне перекликается с моим пунктом номер 2, только с другого боку.
Михаил, может подскажете как можно отловить это lua_close() или разрушение Lua-объекта? в смысле отловить со стороны библиотеки (фактически со стороны Lua-скрипта).

6 (2015-02-02 11:24:46 отредактировано mbul)

Re: Исследование порядка вызовов Callback-функций в QLua

С ходу могу предложить только отлавливать в vcl событие DLL_THREAD_DETACH

7

Re: Исследование порядка вызовов Callback-функций в QLua

Я несколько другое спрашивал, хотя это тоже надо будет посмотреть.
Все "объекты" в VCLua, да и в Lua вообще, есть метатаблицы.
Можно ли отловить событие, когда метатаблица уничтожается Lua?

8

Re: Исследование порядка вызовов Callback-функций в QLua

Насколько я помню, для данных типа userdata можно определить метаметод __gc(), который будет вызываться при удалении. Я поискал в исходниках VCL и не нашел такой подстроки.

9

Re: Исследование порядка вызовов Callback-функций в QLua

sam063rus пишет:

ну-ну... создай форму в коллбеках...

В чем проблема?

VCL = require "qvcl"

is_run = true

function main()
  while is_run do
    sleep(50)
  end
end

function OnStop()
  is_run = false
end

function OnOrder(order)
  local form =  VCL.Form( {
            Height = 100, Width = 200, Caption = "",
            Position = "poDefaultPosOnly",
            OnClose = function (sender) sender:Release() end
  } )
  local label = VCL.Label( form, { top=0, left=0 } )
  label.Caption = tostring(order.order_num) .. " " .. order.sec_code .. " " .. tostring(order.qty)
  form:Show()
end

10

Re: Исследование порядка вызовов Callback-функций в QLua

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

11

Re: Исследование порядка вызовов Callback-функций в QLua

swerg пишет:

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

Начиная с версии 6.16 lua_close() вызывается после завершения main() в том же потоке. так же lua_close() может быть вызвана в основном потоке терминала после ручной остановки скрипта, если main() не завершилась корректно (поток был завершен принудительно)

12

Re: Исследование порядка вызовов Callback-функций в QLua

mbul пишет:

Начиная с версии 6.16 lua_close() вызывается после завершения main() в том же потоке.

В том же - в смысле в потоке main() ?!
А lua_open() где делается?
Хорошо ли это, что lua_close() в дочернем потоке? это наверняка ведь получается, что запускающийся в этот момент сборщик мусора тоже запустится в неосновном потоке?

13

Re: Исследование порядка вызовов Callback-функций в QLua

swerg пишет:
mbul пишет:

Начиная с версии 6.16 lua_close() вызывается после завершения main() в том же потоке.

В том же - в смысле в потоке main() ?!
А lua_open() где делается?
Хорошо ли это, что lua_close() в дочернем потоке? это наверняка ведь получается, что запускающийся в этот момент сборщик мусора тоже запустится в неосновном потоке?

lua_open вызывается в момент нажатия кнопки старт. то есть в потоке терминала. Но в случае с lua_open разницы нет где ее вызвать, сама эта функция ничего особенного не делает. Загрузки библиотек начинаются с вызова require или loadlib в скрипте.
Можно предусмотреть в VCLua какой-нибудь финализатор? Возможно либо в виде явной потокобезопасной функции для вызова из скрипта, либо как реакцию на событие THREAD_DETACH или PROCESS_DETACH.
То решение, которое я предложил и дал паре человек попробовать- это скорее "грязный хак". Времени разбираться не было, да и в Дельфи не особенно силен, поэтому выглядит так:

library vcl;

{$mode Delphi}{$H+}
{$i vcldef.inc}

{$R *.res}

uses
  Windows,
  Classes, SysUtils,
  Interfaces, InterfaceBase,
  Forms, Controls, Graphics, Dialogs,
  {$i vcl.inc}

function luaopen_qvcl(L: Plua_State): Integer; cdecl;
begin
  LoadLibrary('qvcl.dll');
...

Побочный эффект - библиотека не выгружается при остановке скрипта.

14

Re: Исследование порядка вызовов Callback-функций в QLua

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

А всякие THREAD_DETACH или PROCESS_DETACH - они и так обрабатывается, разумеется. И именно в этом и "проблема".
К сожалению понять в каком потоке все это вызывается экспериментально не так просто, потому и хотелось бы пояснений от разработчиков.

15

Re: Исследование порядка вызовов Callback-функций в QLua

swerg пишет:

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

А всякие THREAD_DETACH или PROCESS_DETACH - они и так обрабатывается, разумеется. И именно в этом и "проблема".
К сожалению понять в каком потоке все это вызывается экспериментально не так просто, потому и хотелось бы пояснений от разработчиков.

lua_close можеть быть вызвана:
1. при корректном завершении функции main() в том же потоке. в котором выполнялась эта функция.
например для такого кода:

function main()
 return
end

или вот такого:

stopped = false
function OnStop(signal)
 stopped = true
end
function main()
 while not stopped do sleep(100) end 
end

2. для такого кода:

function main()
 while true do  sleep (100) end
end

lua_close будет вызвана из основного потока терминала, после ожидания дефолтного тайм-аута и принудительного завершения потока для main()

16

Re: Исследование порядка вызовов Callback-функций в QLua

Понял, спасибо за ответ.

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

от admin: вопрос перформулировал, наезды убрал, впредь буду банить за такое.

17 (2015-02-04 10:14:13 отредактировано swerg)

Re: Исследование порядка вызовов Callback-функций в QLua

Провел эксперимент.
Если main() корректно не завершать, то qvclua работает отлично, запускается по несколько скриптов с окошками, надёжно работают, завершаются по кнопке "Остановить" в диалоге скриптов. Потому что деинициализация библиотеки корректно происходит в основном потоке.

Если же корректно по какому-либо событию завершать main() - то в этот момент терминал довольно часто падает, стек падения показывает куда-то в обработку сообщений.

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

Если выбить лестницу - то электрононтёр свалится со столба, но он сам в этом виноват ведь, правда? это ведь он упал.

"О сколько нам открытий чудных..."

Рекомендация пользователям QVCLua:
При использовании библиотеки в 6.16 версии терминала - просто уберите из кода Lua скрипта завершение main(), т.е. код в ней должен быть пока такой, например:

function main()
  while is_run or true do  -- когда починят - уберем or true
    sleep(50)
  end
end
function OnStop()
  is_run = false
  mainForm:Release() -- уничтожим основную форму скрипта
end

Завершаем скрипт по кнопке "Остановить". В этом случае всё работает корректно, падений не замечено.


Вопрос:
Михаил, можно ли надеяться, что это безобразие будет исправлено в ближайших версиях терминала, и вызов lua_close() будет при любом сценарии завершения скрипта всегда выполняться в основном потоке терминала, т.е. там же, где и lua_open()?
Вы ведь согласны, надеюсь, что сделанный в 6.16 вариант - не нормален?

18

Re: Исследование порядка вызовов Callback-функций в QLua

Значит так.
swerg и sam063rus идут перечитывать правила.
Кратко:

  • недопустимы оскорбления, ни в каком виде никого из пользователей;

  • желательно не общаться на "ты"; то, что где-то "так общаются, и ничего" - не аргумент, тащить сюда гадость - не стоит.

Надеюсь, удастся обойтись без бана кого-либо.
swerg, быть может вы не заметили, что sam063rus выкладывает качественные переводы?

Флуд удалён.

19

Re: Исследование порядка вызовов Callback-функций в QLua

swerg пишет:

Вопрос:
Михаил, можно ли надеяться, что это безобразие будет исправлено в ближайших версиях терминала, и вызов lua_close() будет при любом сценарии завершения скрипта всегда выполняться в основном потоке терминала, т.е. там же, где и lua_open()?
Вы ведь согласны, надеюсь, что сделанный в 6.16 вариант - не нормален?

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

20

Re: Исследование порядка вызовов Callback-функций в QLua

Как вы видите, выше есть пример того, что изменение логики очень поможет. (пример про "незаканчивающийся main" и стабильную работу скрипта при этом).
Этот же пример, на мой взгляд, подсказывает нам, что изменить логику вроде не должно составить для вас труда, т.к. желаемый вариант уже фактически реализован в одном из сценариев.

21 (2016-09-29 11:24:24 отредактировано sam063rus)

Re: Исследование порядка вызовов Callback-функций в QLua

-

22 (2015-06-13 12:06:08 отредактировано CyberTrader)

Re: Исследование порядка вызовов Callback-функций в QLua

swerg пишет:

while is_run or true do  -- когда починят - уберем or true

В версии 6.17.1.17 вроде починили.

23 (2015-10-25 14:53:24 отредактировано CyberTrader)

Re: Исследование порядка вызовов Callback-функций в QLua

CyberTrader пишет:
swerg пишет:

while is_run or true do  -- когда починят - уберем or true

В версии 6.17.1.17 вроде починили.

Поторопился я с выводом: при закрытии формы скрипты продолжают крашить терминал без использования "костылей".

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

24

Re: Исследование порядка вызовов Callback-функций в QLua

mbul пишет:

То решение, которое я предложил и дал паре человек попробовать- это скорее "грязный хак". Времени разбираться не было, да и в Дельфи не особенно силен, поэтому выглядит так:

library vcl;

{$mode Delphi}{$H+}
{$i vcldef.inc}

{$R *.res}

uses
  Windows,
  Classes, SysUtils,
  Interfaces, InterfaceBase,
  Forms, Controls, Graphics, Dialogs,
  {$i vcl.inc}

function luaopen_qvcl(L: Plua_State): Integer; cdecl;
begin
  LoadLibrary('qvcl.dll');
...

Побочный эффект - библиотека не выгружается при остановке скрипта.

Это прекрасно работает, и уже не один месяц, ещё раз спасибо!
Подскажите пожалуйста, это всё, что Вы дописали в библиотеку? В какой файл исходников? Дело в том, что swerg периодически добавляет в свои исходники всё новые полезные изменения, а у меня только Ваша скомпилированная версия, которая немного подотстала от жизни )

25

Re: Исследование порядка вызовов Callback-функций в QLua

Позвольте я отвечу.
Да, это всё, что добавлено для стабильности.
А что, в самом деле так много стабильнее?