Система очередей

Предисловие

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

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

Что такое очередь?

В Denizen слово «скрипт» может означать несколько разных вещей — это .dsc-файл, контейнер внутри него (например, контейнер world-скрипта) или подраздел контейнера скрипта, в котором перечислены выполняемые команды. На этой странице мы будем использовать слово «скрипт» в последнем из этих значений. Скрипт может представлять собой всего одну команду narrate или длинный список команд, выполняемых одна за другой, потенциально даже с wait для задержки между командами, run для запуска других скриптов и т. д.

Когда вы запускаете скрипт, это запускает очередь. Очередь можно представлять себе как конкретный запущенный экземпляр скрипта. Что именно это значит, поначалу может быть неочевидно, но влияние на работу Denizen значительно.

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

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

Небольшое замечание о терминологии

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

Устойчивый ex-скрипт

Для начала воспользуемся командами /ex и /exs, чтобы получить представление о том, что такое «очередь».

В игре выполните подряд две команды: Сначала: /ex define example <player.name> Затем: /ex narrate <[example]>

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

../../_images/queue_ex_fail.png

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

А теперь попробуем /exs: /ex означает «execute» (выполнить), а /exs — «execute-sustained», то есть поддерживает одну и ту же очередь.

Сначала выполните: /exs define example <player.name> Затем: /exs narrate <[example]>

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

../../_images/queue_exs_success.png

Пример скрипта

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

queue_demo:
    type: task
    script:
    - define choice <util.random_decimal>
    - narrate "<queue.id> is an instance of <script.name> and chose <[choice]>"
    - wait 5s
    - narrate "<queue.id> remembers that it chose <[choice]>"

Загрузите этот скрипт и быстро введите в игре команду /ex run queue_demo дважды подряд.

Вы должны увидеть примерно такой вывод:

../../_images/queue_demo_script.png

На скриншоте выше видно, что запустились две очереди: одна с меткой QUEUE_DEMO_4_MakeupToday, другая — QUEUE_DEMO_7_ReferenceEfficient. Эти ID собираются из имени скрипта, числового ID (счётчика, который увеличивается на единицу с каждой новой очередью) и двух слов из генератора случайных слов. В отладочном логе в консоли у слов в ID очередей также случайным образом назначаются цвета MakeupToday — белый и магента, у ReferenceEfficient — оранжевый и зелёный), чтобы в логах было проще визуально различать очереди. (Примечание: формат ID очереди можно изменить в вашем Denizen/config.yml, если захотите.)

Очередь с меткой MakeupToday выбрала случайное число 0.315…, а ReferenceEfficient0.251….

Обе очереди стартовали из одного и того же task-скрипта queue_demo, но работают независимо друг от друга. Они запустились с небольшим сдвигом (в зависимости от того, насколько быстро вы повторили команду), каждая выбрала своё случайное число и сохранила его в собственной копии определения choice, обе отсчитали по 5 секунд с момента своего запуска, а затем каждая в своё время выдала финальное сообщение — с собственным ID и собственным случайным числом.

Также можно видеть, что у каждого вызова /ex была своя очередь, которая только и сделала, что выполнила команду run и завершилась — отдельно от очереди, созданной для запуска task-скрипта.

Очереди независимы

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

Независимый запуск

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

Независимое выполнение

В нашем примере скрипта выше мы видели, что очереди отсчитывают время независимо. Если это не бросилось в глаза сразу, попробуйте выполнить /ex run queue_demo, затем подождите пару секунд и запустите ещё раз. Вы увидите: стартует первая очередь, пауза, стартует вторая, пауза, завершается первая, ещё одна пауза — той же длительности, что и пауза между вашими запусками, — и завершается вторая. Каждая очередь ведёт свой собственный отсчёт wait.

Независимая остановка

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

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

Независимые данные

У каждой очереди свой собственный набор привязанных данных. Самый наглядный пример — определения: как вы видели в примере скрипта выше, определение choice было привязано к каждой очереди индивидуально.

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

Сюда же относятся данные контекста: например, в world-скрипте одно и то же событие может срабатывать много раз с разными контекстными данными в каждой очереди.

Это также включает аргумент save.

Очереди временны

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

Очереди неизбежно не переживают перезапуск сервера — при выключении сервера все очереди теряются. Это значит, что очередь не может продолжать выполнять команды после wait через перезагрузку сервера. Вместо этого можно использовать команду RunLater, чтобы запланировать запуск скрипта в будущем — даже после перезапуска сервера.

Очереди уникальны

Каждая очередь уникальна по своей природе. Соответственно, у каждой очереди — уникальный ID, то есть никакая другая очередь в тот же момент не может иметь такой же ID. Речь идёт о генерируемых словах, но не о случайно назначаемых цветах (цвета нужны исключительно для наглядности отладочного вывода).

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

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

Очереди пересекаются лишь иногда

Если очередь не использует команду wait или другой источник задержки, она может завершиться мгновенно. «Мгновенно» не значит, что не проходит никакого реального времени, но значит, что не проходит никакого игрового времени. Иными словами, на сервере больше ничего не выполняется — ни в Denizen, ни в Minecraft, — пока очередь не закончит. Весь сервер ждёт, пока выполнятся команды в этой очереди.

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

Техническое замечание 1: очереди против потоков

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

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

Техническое замечание 2: InstantQueue против TimedQueue

Время от времени в отладочном логе вы можете увидеть сообщение Forcing queue (NAME) into a timed queue.... Это потому, что внутри системы большинство очередей по умолчанию являются «мгновенными» (instant) и становятся «отсчитывающими время» (timed), если им нужно где-то подождать — например, при использовании команды wait. Разделение между типами очередей существует в основном по внутренним/техническим причинам, так что особо переживать об этом не стоит — просто знайте, что такое сообщение в логе нормально, и это просто внутренняя механика системы делает свою работу.

Очереди не всегда порождаются скриптом

Хотя обычно мы говорим об очередях в контексте скрипта, который их породил, бывают и исключения.

Первое исключение — это, конечно, команды /ex и /exs: они порождают очереди из команды, набранной прямо в игровом чате.

Второе исключение — использование команды inject; подробнее об этом рассказывается на странице «Опции Run».

К слову: любая команда, выполняющаяся в Denizen, всегда находится внутри очереди, но теги иногда могут быть вычислены вне какой-либо очереди.

А эта очередь ещё работает?

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

start_a_queue:
    type: task
    script:
    - run wild_queue save:myqueue
    - while <entry[myqueue].created_queue.is_valid>:
        - narrate "Queue has <entry[myqueue].created_queue.determination.size> determinations thus far"
        - wait 1s
    - narrate "The queue finished and determined: <entry[myqueue].created_queue.determination.formatted>"

wild_queue:
    type: task
    script:
    - while true:
        - determine passively <util.random_boolean>
        - if <util.random_chance[20]>:
            - stop
        - wait 0.5s

Попробуйте скрипт командой /ex run start_a_queue.

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

Заметьте: первое сообщение narrate иногда сразу оказывается «the queue finished…», а если это не оно, то всегда будет «Queue has 1 ...».

Так происходит потому, что команда run мгновенно запускает новую очередь — то есть очередь скрипта start_a_queue приостанавливается и ждёт, пока wild_queue либо завершится сама, либо сделает паузу; затем, как только wild_queue поставила себя на паузу, управление сервером тут же возвращается к очереди start_a_queue, которая выполняет команду while и первый narrate, а затем упирается в собственную команду wait, — и только тогда серверу, наконец, разрешается обрабатывать остальные вещи, включая ванильную игровую логику Minecraft.

Если wild_queue проходит первую проверку random_chance и останавливается, последовательность событий такая: start_a_queue запускает wild_queue; wild_queue входит в цикл, делает одно определение (determination), затем останавливается; затем start_a_queue проверяет условие while, не заходит в цикл, выполняет финальный narrate и завершается — и только после этого остальной игре разрешается продолжить работу.

Denizen работает быстро

В завершение: кое-где выше мы говорили о том, что сервер приостанавливается и ждёт, пока очереди завершат работу. Это может создать впечатление, что игра будет подвисать или лагать, но важно понимать, что речь идёт о процессорных масштабах времени — компьютеры работают очень быстро, и типичный современный процессор выполняет буквально миллиарды операций в секунду. Время, которое сервер проводит в ожидании завершения очереди, обычно составляет несколько наносекунд, иногда — несколько миллисекунд. Практически всё, что обрабатывается на сервере, обычно заставляет сервер подождать, пока оно не завершится — именно так обычно и работают компьютеры: они обрабатывают всё по порядку (если не используется многопоточность, что не относится к Minecraft-серверам, которые в основном однопоточны).