Skip to content

Исполнение QtScript на роботе

Tatiana Agapova edited this page Jul 13, 2015 · 12 revisions

Основные классы системы исполнения скриптов

  • TrikScriptRunner -- фасад для системы исполнения. Оправляет скрипты на выполнение и команды прерывания скрипта ScriptEngineWorker'у. Живёт в потоке GUI.
  • ScriptEngineWorker -- управляет исполнением скрипта. Знает про режим исполнения, про состояние скрипта, оповещает GUI об ошибках при исполнении. Также инициализирует движки для исполнения скрипта. Живёт в отдельном потоке, но часть операций предназначена для исполнения из потока GUI.
  • Threading -- управляет потоками в скрипте: создаёт и инициализирует потоки, управляет сообщениями между потоками, прибивает исполнение одного или всех потоков. Живёт в потоке ScriptEngineWorker'а.
  • ScriptThread -- поток исполнения, прогоняет выданный ему скрипт на выданном движке, сообщает об ошибках исполнения.

Режимы

Обычное исполнение

В этом режиме команды скрипта выполняются по порядку, пока скрипт не закончится или не будет вызвана команда script.run(). По умолчанию скрипты, приходящие из студии или запускаемые из меню "Файлы", исполняются в этом режиме. Поддерживается многопоточность исполнения.

Запуск скрипта

Исполнение начинается с вызова TrikScriptRunner::run(). В течение него система сначала приходит в состояние готовности к исполнению (ScriptEngineWorker::reset()), затем происходит выполнение скрипта (ScriptEngineWorker::run()). Оба метода должны выполняться синхронно, в треде GUI, чтобы избежать состояния гонок, когда из GUI приходит несколько команд выполнения подряд.

TrikScriptRunner также назначает всем новым скриптам номера, которые потом используются в GUI для определения, какой именно скрипт начался или завершился, и соответствующего управления виджетами.

ScriptEngineWorker::run() сообщает GUI о фактическом начале исполнения и вызывает метод doRun(), который уже в отдельном потоке запускает исполнение.

Поскольку Threading управляет исполнением всех потоков скрипта, включая основной поток, всё, что делает ScriptEngineWorker::run() для запуска исполнения -- это вызывает Threading::startMainThread(), который создаёт главный поток программы и начинает в нём исполнять скрипт. После запуска скрипта ScriptEngineWorker вызывает метод Threading::waitForAll(), чтобы дождаться завершения исполнения скрипта.

Структуры данных Threading

Прежде чем перейти к описанию функциональности Threading, рассмотрим основным структуры данных, необходимые этому классу для управления потоками:

  • mThreads -- хэш, отображающий имя треда на сам тред, используется для получения доступа к отдельным потокам;
  • mFinishedThreads -- список завершившихся тредов;
  • mPreventFromStart -- список тредов, которые ещё не были запущены, но должны быть завершены (из-за команды killThread); попытка запустить поток с именем из этого списка будет проигнорирована;
  • mThreadsMutex -- мьютекс для вышеперечисленных структур (всего один, поскольку в большинстве случаев они должны изменяться одновременно);
  • mMessageQueues -- отображение имён тредов на их очереди сообщений;
  • mMessageMutex -- мьютекс для этого отображения;
  • mMessageQueueConditions и mMessageQueueMutexes -- условные переменные и мьютексы для них, по одному для каждой очереди сообщений;
  • mResetStarted -- флаг, показывающий, что начался reset, во время которого все остальные операции с тредами (создание, обмен сообщениями и т.д.) должны отменяться; должен быть проверен каждой из операций перед выполнением;
  • mResetMutex -- мьютекс для mResetStarted; поскольку все операции с тредами должны быть атомарными (то есть ни одну из них нельзя прервать посредством reset()), этот мьютекс должен блокироваться каждой из операций на всё время её выполнения.

Запуск потока

Каждый поток владеет собственным движком, который Threading запрашивает у ScriptEngineWorker’а. Этот движок проинициализирован нужными объектами и определениями. Перед исполнением функции, указанной параметром метода startThread(), на движке прогоняется весь файл текущего скрипта, чтобы зафиксировать в движке определения всех функций (поскольку их невозможно скопировать средствами Qt из другого движка).

Threading откажется запустить тред в одном из следующих случаев:

  • в данный момент происходит reset();
  • перед вызовом startThread() произошёл вызов killThread() для потока с тем же именем; в данном случае тред как бы убивается до начала выполнения;
  • тред с таким же именем уже существует; в этом случае будет сгенерирована ошибка, и исполнение скрипта будет прервано (см. Завершение тредов).

Если всё прошло хорошо, Threading создаст объект треда ScriptThread, проинициализировав нужным движком и кодом скрипта с вызовом нужной функции в конце, и запустит поток.

Стоит упомянуть небольшой и очень некрасивый хак в конце. Прежде чем разрешить reset() разблокированием соответствующего мьютекса, нужно дождаться, чтобы исполнение скрипта действительно началось. Иначе возникают неприятности, когда мы пытаемся прибить движок до начала выполнения. Qt не позволяет по-человечески просечь эту возможность, и всё просто падает в такой ситуации. Поэтому перед разблокированием mResetMutex вставлена задержка, которая почти наверняка позволяет избежать падения. Это делает startThread() очень тяжёлым методом. Нужно найти какой-то способ реализовать эту проверку более адекватно и эффективно.

Запуск основного треда потока происходит абсолютно так же, как и любого другого.

Завершение тредов

Поток может завершиться, потому что его исполнение подошло к концу, потому что в процессе исполнения произошла ошибка либо потому что он был прерван.

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

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

Кроме того, Threading проверяет также сообщение об ошибке завершившегося потока. Если ошибка была, то исполнение всего скрипта завершается (см. Завершение скрипта).

Прерывание отдельных тредов

Любой поток может прервать любой поток, в том числе себя. Это делается с помощью метода Threading::killThread(), который получает имя треда, подлежащего завершению.

Если тред с данным именем был завершён (его имя в списке mFinishedThreads), метод ничего не делает.

Если тред ещё не начался, его имя добавляется в mPreventFromStart, и следующая попытка создать поток с этим именем будет проигнорирована.

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

Джойны

Для синхронизации предлагается два механизма: джойны и обмен сообщениями. Здесь описаны джойны, а сообщения - в следующем разделе.

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

Обмен сообщениями

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

При этом очереди сообщений не учитывают состояния тредов (ещё не начат/завершён/выполняется). Очереди привязаны не к тредам, а к их именам. Таким образом, следующая последовательность действий:

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

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

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

Завершение скрипта

Скрипт завершается в следующих ситуациях:

  1. Исполнение кода скрипта завершилось нормально.
  2. В процессе исполнения произошла ошибка.
  3. На роботе нажата кнопка Power.
  4. Из ТРИКСтудии пришла команда “остановить скрипт”.
  5. Из ТРИКСтудии пришла команда запуска нового скрипта.

Выполняя скрипт, ScriptEngineWorker отправляет его на исполнение в Threading и начинает ждать его завершения (вызов метода Threading::waitForAll()). Нормальное завершение означает, что все потоки отработали без ошибок. В этом случае Threading не предпринимает никаких дополнительных действий и выходит из waitForAll(), когда список исполняющихся тредов опустеет. После этого ScriptEngineWorker очищает состояние робота и возвращается в состояние готовности к исполнению, попутно оповещая GUI об успешном завершении скрипта.

При возникновении ошибки в одном из потоков он сразу же завершается. Threading узнаёт об ошибке, прерывает остальные потоки и очищает очереди сообщений. Тут тоже есть странный хак: перед убийством очередного потока сбрасывается состояние таймеров (mScriptControl.reset()). Без этого есть шанс, что поток не получит сигнал об остановке из-за того, что будет висеть на таймере. Перед каждым убийством это делаем, чтобы была меньше вероятность, что следующий поток успеет снова запустить таймер. После прерывания всех потоков Threading вываливается из waitForAll(), и дальше всё происходит, как в предыдущем случае, только GUI получает не пустую строчку, а сообщение об ошибке, которое потом висит на экране, пока пользователь не закроет его кнопкой Power.

И прерывание по кнопке Power, и сообщение от ТРИКСтудии происходят в результате пользовательских действий. Эти два события обрабатываются одинаково. Всё начинается с того, что TrikScriptRunner получает команду abort(), в результате которой он сразу же дёргает метод ScriptEngineWorker::reset(). Этот метод заслуживает особого внимания.

Что в нём происходит, зависит от состояния ScriptEngineWorker’а:

  • ready -- ScriptEngineWorker готов к выполнению нового скрипта; при этом состояние робота может не быть очищено (например, на дисплее может болтаться сообщение об ошибке); reset() исправляет это (метод clearExecutionState());
  • starting -- происходит запуск скрипта; перед тем как прибить скрипт, мы должны сначала дождаться, чтобы исполнение действительно началось;
  • resetting -- ScriptEngineWorker уже находится в процессе reset(), ничего не делаем; reset() может быть вызван из двух тредов: GUIшного и собственного потока ScriptEngineWorker’а, поэтому иметь это состояние важно, чтобы избежать всяких race conditions;
  • running -- скрипт выполняется; прибиваем по очереди: таймеры, затем останавливаем все треды (Threading::reset()), затем очищаем состояние робота; после этого ScriptEngineWorker готов запустить новый скрипт.

Если подсистема скриптов получает новый скрипт, в то время как старый ещё исполняется, она вызывает reset() перед тем, как запустить новый. Это происходит точно так же, как если бы пользователь прервал скрипт кнопкой Power. Вызов reset() происходит синхронно: выполнение нового скрипта не начнётся, пока reset полностью не завершится, а ScriptEngineWorker не придёт в состояние готовности.

Исполнение "скрипта как команды"

Это самый обычный скрипт, всё сказанное для него справедливо. Но он не получает порядковый номер, поэтому GUI не показывает его название на дисплее. В этом всё различие.

Event driven

Событийно-ориентированный режим получаем в двух случаях: либо выполняем программу в интерпретаторе ТРИКСтудии, либо в скрипте явно вызываем script.run(). Он не очень рассчитан на жизнь с тредами, на самом деле. При интерпретации треды вообще недоступны, а поддержкой тредов совместно с script.run() серьёзно никто не занимался.

Интерпретация

При интерпретации из ТРИКСтудии движок получает команды одну за одной, при этом воспринимает весь набор команд как один скрипт.

Когда ScriptEngineWorker впервые получает команду, он сначала проверяет, не выполняется ли обычный скрипт. Если да, то тот прибивается. GUI оповещается о начале нового скрипта, скрипт переводится в событийный режим.

При получении каждой следующей команды ScriptEngineWorker уже знает, что идёт интерпретация, поэтому ни reset(), ни создание нового скрипта не происходят, а просто выполняется следующая команда.

В отличие от обычных скриптов, выполнением команд управляет не Threading, а сам ScriptEngineWorker. Команды исполняются в его потоке, на отдельном движке. При этом движок не поддерживает треды вообще (у него просто нет ссылки на Threading).

Завершение серии команд обрабатывается в ScriptEngineWorker::reset(). Это происходит аналогично завершению обычного скрипта: прерывается выполнение, проверяется наличие ошибок, удаляется движок. Новый скрипт начнётся по умолчанию в обычном режиме, не событийном.

script.run()

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

В событийной программе, как правило, сначала происходит объявление нескольких обработчиков событий, а затем вызывается метод script.run(). Поскольку это последнее действие в программе, главный тред считает, что он закончил выполнение и благополучно себя прибивает. Чтобы этого не происходило, в событийном режиме тред не выходит из метода run() после завершения скрипта, а запускает event loop, который крутится, пока кто-нибудь не убьёт скрипт.

Помимо этого, никакой поддержки тредов в событийном режиме нет и никто не тестировал, что происходит, если добавить больше потоков. Тредобезопасностью ScriptExecutionControl, который заведует таймерами, тоже никто не озадачивался. Sad but true.

Clone this wiki locally