---
metadata:
  - name: generator
    content: Diplodoc Platform v5.57.4
alternate:
  - https://diplodoc.com/docs/en/dev/extensions/multithreading.md
  - https://diplodoc.com/docs/ru/dev/extensions/multithreading.md
  - href: https://diplodoc.com/docs/ru/dev/extensions/multithreading.md
    type: text/markdown
    title: Markdown version
  - href: https://diplodoc.com/docs/ru/dev/llms.txt
    rel: describedby
updatedAt: '2026-09-04T08:46:13.000Z'
---
> **Documentation Index:** Fetch the complete configuration index at https://diplodoc.com/docs/ru/llms.txt

# Многопоточность и хуки

При запуске сборки с ключом `--jobs` страницы обрабатываются параллельно, в нескольких потоках. Как включить режим и чего от него ждать, описано в статье [Многопоточная сборка](https://diplodoc.com/docs/ru/tools/docs/multithreading.md). Здесь - что это означает для расширения.

Расширение обязано корректно работать в обоих режимах: пользователь включает `--jobs` по своему усмотрению, и расширение не может на это повлиять.

## Модель исполнения {#model}

Рабочий поток - это не отдельная функция, выполняемая на другом ядре, а полная копия программы. В каждом потоке заново разбирается конфигурация, загружаются все расширения и создаются собственные экземпляры всех [сервисов](https://diplodoc.com/docs/ru/dev/extensions/services.md): `markdown`, `leading`, `meta`, `vars`, `toc`, `vcs`, `search`, `redirects`.

Распределение работы:

* **Главный поток** разбирает оглавление, раздаёт страницы рабочим потокам, собирает результаты и выпускает итоговые артефакты: поисковый индекс, singlepage, PDF, манифесты, редиректы, карту файлов.
* **Рабочие потоки** обрабатывают отдельные страницы: markdown или разводящую страницу превращают в конечный файл и записывают его.

Параллельно выполняется ровно одно - обработка отдельной страницы. Всё остальное остаётся в главном потоке.

## Где выполняется какой хук {#hooks}

По отношению к многопоточности хуки делятся на три группы.

### Хуки регистрации {#registration-hooks}

`Base.Command`, `Base.RawConfig`, `Base.Config`, `Base.BeforeAnyRun`, `Build.BeforeRun`, `Vars.PresetsLoaded`, `Markdown.Collects`, `Markdown.Plugins`, `Leading.Plugins`, `Vcs.VcsConnector`, `Search.Provider`.

Выполняются в каждом потоке: в главном и в каждом рабочем. Это точки, где расширение объявляет о себе - добавляет ключ запуска, правит конфигурацию, регистрирует markdown-плагин, подменяет провайдера поиска, подписывается на другие хуки.

{% note warning %}

Обработчик такого хука будет вызван `N + 1` раз, где `N` - число рабочих потоков. Побочные эффекты здесь - запись файлов, сетевые запросы, инкремент счётчиков - выполнятся столько же раз.

{% endnote %}

### Хуки главного потока {#main-thread-hooks}

`Toc.Item`, `Toc.Includer`, `Toc.Included`, `Toc.Loaded`, `Toc.Dump`, `Toc.Filtered`, `Build.Entry`, `Build.AfterRun`, `Base.AfterAnyRun`, `Search.Page`, `Redirects.Page`, `Redirects.Release`.

Выполняются только в главном потоке, независимо от значения `--jobs`. Здесь и только здесь можно безопасно накапливать состояние по всему проекту.

Ключевой хук - `Build.Entry`: он вызывается в главном потоке после того, как страница обработана, и получает результат её обработки. Так устроены штатные возможности сборщика, которым нужны данные обо всех страницах сразу: поиск, singlepage, PDF, манифест обхода, статистика сборки.

```typescript
import {join} from 'node:path';
import {getBuildHooks} from '@diplodoc/cli';

export class Extension {
    apply(program: Build) {
        const index = new Map();

        // Вызывается в главном потоке, состояние переживёт сборку
        getBuildHooks(program)
            .Entry.for('html')
            .tap('MyExtension', (run, entry, info) => {
                index.set(entry, info.title);
            });

        getBuildHooks(program)
            .AfterRun.for('html')
            .tapPromise('MyExtension', async (run) => {
                await run.write(join(run.output, 'index.json'), JSON.stringify([...index]));
            });
    }
}
```

### Изолированные хуки {#isolated-hooks}

`Markdown.Loaded`, `Markdown.Resolved`, `Markdown.Dump`, `Leading.Loaded`, `Leading.Resolved`, `Leading.Dump`, `Meta.Dump`, `Entry.Dump`, `Entry.State`, `Entry.Page`.

При `--jobs > 1` выполняются в рабочем потоке, при однопоточной сборке - в главном. Обработчик видит только состояние своего потока: изменения, которые он внёс в сервисы, в главный поток не вернутся, и наоборот - данные, накопленные другими страницами, ему недоступны.

Что в них работает без оговорок:

* преобразование содержимого текущей страницы;
* чтение исходных файлов;
* запись файлов в выходную директорию;
* логирование через `run.logger`.

Что работать не будет: накопление состояния в памяти расширения с расчётом прочитать его позже в главном потоке. Такое состояние нужно возвращать через `Build.Entry` либо собирать заново в главном потоке.

{% note info %}

Именно поэтому расширение, которое «работает без `--jobs` и молча теряет данные с `--jobs`», почти всегда копит состояние в изолированном хуке.

{% endnote %}

## Что передаётся между потоками {#serialization}

Между потоками передаются только сериализуемые данные:

* из главного потока в рабочие - разобранное оглавление и данные VCS (один раз перед началом обработки), а также путь и метаданные каждой страницы;
* из рабочего потока в главный - результат обработки страницы: заголовок, метаданные, данные страницы и графы зависимостей;
* логи рабочих потоков сводятся в общий вывод по каналам `info`, `warn` и `error`;
* ошибки передаются с сохранением сообщения и стека.

Функции, классы и замыкания границу потока не пересекают. Если расширение кладёт свои данные в метаданные страницы или в результат её обработки, эти данные должны быть простыми объектами.

Хук `Base.Error` срабатывает в том потоке, где возникла ошибка. Ошибки обработки конкретных страниц пробрасываются в главный поток и логируются там.

## Идемпотентность записи {#idempotent-writes}

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

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

## Ограничения {#limitations}

* **Watch-режим однопоточный.** После первой сборки рабочие потоки останавливаются, инкрементальные пересборки идут в один поток.
* **Данные VCS считаются только в главном потоке** и рассылаются в рабочие в готовом виде. Обращаться к системе контроля версий из изолированных хуков нельзя.
* **Откат к однопоточной сборке.** Если рабочие потоки не успевают инициализироваться за 30 секунд, сборка молча продолжается в один поток.

## Что почитать дальше {#see-also}

* [Многопоточная сборка](https://diplodoc.com/docs/ru/tools/docs/multithreading.md)
* [Принципы разработки расширений](https://diplodoc.com/docs/ru/dev/extensions/core-concepts.md)
* [Сервисы Diplodoc](https://diplodoc.com/docs/ru/dev/extensions/services.md)
