惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
V
V2EX
博客园 - 【当耐特】
Vercel News
Vercel News
雷峰网
雷峰网
爱范儿
爱范儿
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
F
Full Disclosure
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
NISL@THU
NISL@THU
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Attack and Defense Labs
Attack and Defense Labs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Microsoft Security Blog
Microsoft Security Blog
腾讯CDC
P
Proofpoint News Feed
B
Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
I
InfoQ
Google Online Security Blog
Google Online Security Blog
L
LINUX DO - 最新话题
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
V
Visual Studio Blog
AI
AI
Schneier on Security
Schneier on Security
B
Blog RSS Feed
T
Tor Project blog
H
Help Net Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LINUX DO - 热门话题
阮一峰的网络日志
阮一峰的网络日志
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Cyber Attacks, Cyber Crime and Cyber Security
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
V2EX - 技术
V2EX - 技术
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
A
Arctic Wolf
Webroot Blog
Webroot Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Пауза, двойственная по природе: контракт хука и протокол воркера
Руслан Гильмуллин · 2026-06-07 · via Все публикации подряд на Хабре

Пауза, двойственная по природе: контракт хука и протокол воркера

Средний

10 мин

3.5K

Предположим, вы пишете интерпретатор машины Тьюринга, запущенный в Web Worker’е. UI должен показывать трейс — как машина шагает от состояния к состоянию, что пишется на ленту, как двигается каретка. Чтобы пользователь успевал считывать изменения в UI, между итерациями движка нужна короткая задержка — миллисекунды, регулярно, на каждом шаге. Это приостановка движка между итерациями — регулярная и предсказуемая, не «пауза» в смысле UI-кнопки «Пауза» (та останавливает машину до клика «Продолжить»).

Возникает вопрос: где именно в цикле итерации воркеру нужно реализовать приостановку? Кандидатов два, и выбор между ними фиксирует сразу два контракта: хуков движка и протокола между воркером и основным потоком. Выбрать точку — это спроектировать обе стороны сразу; промахнуться — испортить обе. Эта статья — про этот выбор.

Скриншот demo.machines.mellonis.ru: машина Тьюринга в режиме RUNNING_AUTO, кнопка «Пауза» выделена зум-кругом, слева виден пошаговый трейс выполнения

Скриншот demo.machines.mellonis.ru: машина Тьюринга в режиме RUNNING_AUTO, кнопка «Пауза» выделена зум-кругом, слева виден пошаговый трейс выполнения

Введение

turing-machine-js — интерпретатор машины Тьюринга. У движка есть хук onStep — переданный обработчик вызывается после каждого шага машины. Этим вводным про движок ограничимся. Подробнее про устройство и предыдущие изменения API — в статьях «Три мажора, две ошибки: проектирование API приостановки для интерпретатора машины Тьюринга» (1) и «Два мажора, один README, одно демо: два почти бесплатных дизайн-ревью» (2).

Первый настоящий потребитель движка — machines-demo, интерактивный отладчик. Пользовательская машина исполняется в Web Worker’е; UI на основном потоке показывает трейс. То же самое демо, которое в статье (2) спровоцировало два мажора в движке (#109, #108). В этой статье — следующий эпизод из жизни того же движка: v6.2 → v6.3 → v6.4.

В демо есть режим RUNNING_AUTO: пользователь выбирает интервал, нажимает «Старт», машина шагает автоматически. Изначально режим был устроен прямолинейно — основной поток запускал машину в режиме пошагового выполнения (через runner.step() — обёртку над движковым runStepByStep() в воркере), между шагами вставлял setTimeout(intervalMs) для трейса. Просто и работает — до тех пор, пока пользователь не поставит брейкпоинт.

Брейкпоинты в движке реализованы внутри machine.run(): цикл итераций находится в самом движке, и при попадании в debug-помеченное состояние он сам себя останавливает. runStepByStep() же — генератор: цикл по нему крутит потребитель, и брейкпоинты он не видит. machines-demo#43 и был этой дырой — пользователь ставит брейкпоинт в режиме RUNNING_AUTO, нажимает «Старт», но брейкпоинт игнорируется. Чтобы брейкпоинты заработали, RUNNING_AUTO должен перейти с runStepByStep() на run().

Но run() крутит цикл итераций внутри движка, не передавая управление наружу. Раньше приостановка жила в паузах между вызовами runner.step() на основном потоке — то есть на стороне потребителя, который сам управлял интервалом между шагами. С переходом на run() этой паузы на стороне потребителя больше нет — приостановку нужно либо переносить внутрь движка (в существующий или новый хук), либо убирать. Это и стало дизайн-вопросом.

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

Где может жить приостановка

Два кандидата:

  1. Внутри onStep — синхронного хука, который движок зовёт после каждого шага. Сейчас его сигнатура (state) => void. Можно расширить её до (state) => void | Promise<void> — и тогда обработчик может await-ить задержку прямо там.

  2. На границе итерации — добавить новый хук, специально для асинхронных ожиданий. Имя пока неважно; важно, что вызывается он один раз в конце каждой итерации движка, и движок ждёт возвращённый Promise.

Оба варианта отвечают на вопрос «где живёт приостановка». Но они одновременно диктуют, как устроен протокол между воркером и основным потоком.

Если приостановка стоит в onStep, она оказывается посредине каждой итерации движка — в той точке, где зовётся onStep. К этой же точке приходится привязывать и всё остальное в протоколе: и сторожевой таймер, и обработку клика «Пауза» от пользователя — всё повисает на этой мид-итерационной задержке.

Если приостановка стоит на границе итерации, у воркера в конце каждой итерации появляется момент — движок не работает, ждёт Promise. В этот момент удобно помириться со сторожевым таймером и быть готовым к обработке клика «Пауза».

Дальше — оба контракта по очереди.

Контракт «Внутри onStep»

v6.2.0 пошла первым путём — расширила сигнатуру onStep с (state) => void до (state) => void | Promise<void>. Обработчик теперь мог await-ить задержку прямо там, где раньше только смотрел состояние. Со стороны воркера это выглядело красиво — один хук, одна точка ожидания, никаких новых сущностей.

Уже через несколько часов я пометил v6.2.0 в npm как deprecated. Не «вышла версия посвежее, обновляйтесь по случаю», а явное предупреждение: эта версия — неправильная.

v6.3.0 откатила расширение сигнатуры. Причина — простая: onStep задумывался как хук наблюдения, а не координации. Он работает синхронно, посредине каждой итерации движка, для того чтобы потребитель мог посмотреть на состояние и где-то его записать. Добавить ему await в сигнатуру — значит склеить две разные задачи в одну: и наблюдать, и удерживать движок. Читатель сигнатуры теперь не различает: этот хук — «посмотри на состояние» или «придержи здесь движок»? Назначение хука перестаёт быть видно из сигнатуры.

Симптом того, что что-то не так, проявился в документации. README должна была пояснить, как пользоваться расширенным onStep. Честный абзац начинался с оговорки: «onStep — это хук наблюдения, но вы можете возвращать из него Promise, и движок дождётся его перед следующей итерацией». Не получалось убрать ни «но», ни вторую половину фразы. Документация работала ровно так, как описано в статье (2): прозой давила на форму — форма треснула.

v6.4.0 добавила отдельный хук — onIter. Сигнатура: (state) => void | Promise<void>. Контракт: вызывается один раз в конце каждой итерации, асинхронно, безусловно. Имя — iteration boundary, граница итерации — описывает что хук делает (даёт точку в конце итерации, в которой можно подождать), а не зачем потребитель собирается его использовать. А использовать его можно по-разному: для приостановки между шагами трейса, для синхронизации с UI, для прерывания прогона (например, по AbortSignal).

Контракт хуков движка теперь различает три задачи:

  • onStep — синхронный, посредине итерации. Наблюдение за состоянием.

  • onPause — асинхронный, условный (срабатывает только на брейкпоинтах). Граница между ходом и остановом.

  • onIter — асинхронный, безусловный (срабатывает на каждой итерации). Координация: удержание движка для асинхронной работы потребителя.

Три хука — три разные задачи. Наблюдение не должно удерживать движок; для этого есть отдельный, асинхронный хук. Удержание не должно прятаться в хуке наблюдения; для этого есть onIter. Пауза по брейкпоинту — это вообще отдельный сценарий, и под него есть onPause, у которого событие срабатывания явно прописано в имени.

Контракт «На границе итерации»

Где живёт приостановка — теперь ясно: внутри обработчика onIter. Но обработчик onIter запускается внутри воркера, а пауза от пользователя приходит снаружи — с основного потока, по клику. Между ними нужен протокол.

Этот протокол содержит четыре механизма: пара сообщений idle/busy, флаг stepRequested, синтетическое событие paused, поле intervalMs в resume. Каждый закрывает одну конкретную проблему, которую расположение приостановки создаёт.

Сообщения idle и busy: приостановка сторожевого таймера

Упомянутый выше сторожевой таймер в onIter начинает мешать.

Приостановка в onIter на каждой итерации может легко превышать таймаут — особенно если пользователь выкрутил intervalMs на «медленно» (демо позволяет ставить интервал, скажем, в две секунды). Получается ложная тревога: воркер не подвис, он просто ждёт.

Решение — воркер сам сообщает основному потоку, в какой момент он «работает», а в какой «ждёт». Перед тем как await-ить приостановку в onIter, воркер посылает сообщение idle. После того как приостановка завершилась, посылает busy. Основной поток приостанавливает сторожевой таймер на промежутке между idle и busy. Таймер продолжает ловить настоящие зависания (например, недостижимое halt-состояние), но не путает их с легитимным замедлением трейса.

Без такой обёртки у сторожевого таймера два плохих варианта. Либо брать порог с запасом — тогда настоящие зависания не ловятся (или ловятся слишком поздно). Либо брать порог впритык — выше вероятность ложного срабатывания (воркер убивается на легитимной задержке). Сообщения idle / busy разрешают оба сценария одновременно.

Шаг — флаг внутри воркера, а не состояние движка

Когда пользователь нажимает «Шаг», основной поток посылает воркеру сообщение resume с флагом step: true — «продвинь машину на одну итерацию и снова поставь паузу». Реализовано это так: воркер выставляет флаг stepRequested = true. На следующем onIter обработчик видит флаг, сбрасывает его в false, синтезирует событие paused и снова ждёт. Машина прошла ровно один шаг.

Принципиально: «шаг» — это флаг внутри воркера, а не часть состояния движка. Движок не знает, что такое «шаг», — он просто на следующей итерации опять оказался в режиме ожидания. Координация лежит снаружи движка. onIter остаётся тонким await-хуком — он не мутирует движок, он только приостанавливает его выполнение. Это и сделало onIter жизнеспособным как контракт: в нём нет «специальной логики шага», есть только Promise, который воркер сам контролирует. Если бы шаг был частью состояния движка, контракт onIter пришлось бы расширять до «и ещё, дёрни вот этот флаг», и хук перестал бы быть тонким.

При «Шаге» замедление излишне

При нажатии на «Шаг» замедление излишне — пользователь сам играет роль таймера: кликает столько раз и так быстро, как ему нужно. Реализация это отражает: при «Шаге» intervalMs игнорируется, обработчик onIter не вставляет ожидание. Это самое чистое выражение разделения «наблюдение ≠ координация» внутри воркера: когда управляет пользователь, движок подчиняется.

Синтетическая пауза по клику

Сценарий: машина в RUNNING_AUTO — движок выполняется с приостановками по времени, между итерациями onIter ждёт intervalMs. Пользователь нажимает в UI кнопку «Пауза». Клик приходит на основной поток. Основной поток посылает воркеру сообщение pause. Воркер в этот момент удерживает движок в приостановке — Promise ещё не разрешился. Что делать?

Решение — синтезировать такую же паузу, как от брейкпоинта. У UI уже есть готовый обработчик: на paused из воркера он переключается в «остановлено». Если клик «Пауза» пойдёт по другому пути, появится вторая ветка с тем же поведением. Поэтому воркер и синтезирует paused — для UI клик «Пауза» неотличим от срабатывания брейкпоинта.

Воркер отменяет приостановку (разрешает её принудительно, заранее), выставляет флаг stepRequested = false, и из обработчика onIter после возврата из ожидания посылает наружу событие paused — синтетическое, а не возникшее в ходе нормальной работы движка. Под капотом два сценария различаются (один отменяет приостановку, другой ждёт её естественного завершения), но контракт снаружи — один сценарий «пауза».

intervalMs — параметр resume, не run()

Приостановка реализуется в onIter по intervalMs. Откуда воркер берёт это значение? Не из параметров запуска run(). А из последнего сообщения resume: каждое из них приходит с актуальным intervalMs из основного потока.

Причина — пользователь может управлять величиной задержки между приостановками во время прогона. Значение в UI читается в момент клика, не в момент старта run(); следующее resume передаст новое значение, и со следующей итерации приостановка подстроится. Маленькая цена для протокола, большой выигрыш для UX.

Двойственность, явно

Приостановка живёт в одной точке итерации. С точки зрения движка эта точка — обработчик onIter: хук, который вызывается один раз в конце каждой итерации, асинхронно. С точки зрения протокола та же точка — это промежуток между сообщениями idle и busy, в котором сторожевой таймер приостановлен, клик «Пауза» может мягко отменить приостановку, а intervalMs берётся из последнего resume.

Одна точка, два API. От выбора зависят все последующие этапы проектирования. Положить её посредине хука наблюдения — это сразу поломать обе стороны: документация на расширенный onStep не пишется без «но», обёртку сообщениями idle/busy не к чему прикрутить (один и тот же хук обозначает и наблюдение, и ожидание), синтетическое событие paused приходится посылать из неестественного места. Без чистой границы итерации страдают и хук, и протокол.

Заключение

Разделять наблюдение и координацию — это то, ради чего контракт хуков движка различает три задачи, а не сливает в одну. Склеить их — соблазн, которому поддался v6.2.0. И единственное, что позволило осознать ошибочность подхода за несколько часов — README движка не получалось написать честно, и smoke-тест демо не получалось пройти. Два почти бесплатных дизайн-ревью (статья (2)).


Код: turing-machine-js (движок, v6.2 → v6.4) и machines-demo (демо).

Оригинал: https://mellonis.ru/ru/articles/pauza-dvojstvennaya-po-prirode/