Skip to content

RFC-024: Семантика параллельной среды выполнения на основе spawn

В этом документе определяется семантика поведения spawn во время выполнения. Ортогональность синтаксиса, рефакторинг AST/IR и расширение системы типов описаны в RFC-032.

Оба RFC совместно определяют spawn — 024 отвечает на вопрос "что делать", 032 отвечает на вопрос "как выразить".

Ссылки:

Краткое содержание

В этом документе определяется семантика поведения во время выполнения spawn в языке программирования YaoXiang: spawn <expr> является единственным примитивом параллелизма, может модифицировать любое выражение, вызывающая сторона блокируется синхронно. Форма выражения определяет гранулярность разбиения задач, среда выполнения планирует по модели GMP — задачи без зависимостей помещаются в рабочую очередь, worker'ы соревнуются за их выполнение.

Основной дизайн — один примитив, набор правил:

spawn <expr>               ← единственный примитив параллелизма
Разбиение задач определяется формой выражения    ← единственное правило
Синхронная блокировка ожиданием результата        ← единственное поведение

Устранённая сложность:

  • ❌ Нет аннотаций @block/@eager/@auto
  • ❌ Нет trait Send/Sync
  • ❌ Нет Mutex/RwLock/Atomic
  • ❌ Нет future/неблокирующих дескрипторов
  • ❌ Нет анализа DAG для всей программы
  • ❌ Нет окрашивания функций (async/await)

Ментальная модель пользователя: Ваш обычный код выполняется последовательно. Когда вы хотите, чтобы несколько вещей происходили одновременно, вы помещаете их в spawn <expr>. Никаких обратных вызовов, никаких await, никаких странных аннотаций.

Источники дизайна

ДокументСвязь
RFC-001Заменён этим документом
RFC-008Архитектура среды выполнения, ортогонально этому документу
RFC-009Модель владения, без изменений
RFC-010Унифицированный синтаксис типов
RFC-032Рефакторинг AST/IR, совместно с этим документом определяет spawn

Мотивация

Зачем нужен этот дизайн?

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

ЯзыкМодель параллелизмаПроблема
Rustasync/await + tokioЗаражение асинхронностью, окрашивание функций, крутая кривая обучения
GogoroutineОтсутствие безопасности типов, трудно обнаружить гонки данных
PythonasyncioОграничения GIL, окрашивание функций
JavaScriptPromise/asyncАд обратных вызовов, окрашивание функций

Проблемы старого дизайна (RFC-001)

Трёхуровневая архитектура параллелизма (L1/L2/L3), предложенная в RFC-001, имела следующие проблемы:

ПроблемаОписание
Сложная ментальная модельТри уровня абстракции L1/L2/L3 увеличивают нагрузку на обучение
Избыточные аннотацииАннотации @block/@eager/@auto загрязняют код
Высокая сложность анализаНакладные расходы времени компиляции от анализа DAG всей программы
Сложные ограничения типовtrait Send/Sync увеличивают когнитивную нагрузку
НеконтролируемостьАвтоматический параллелизм трудно предсказать и отладить

Цели дизайна

  1. Простота: только один примитив параллелизма (spawn), может модифицировать любое выражение
  2. Явность: пользователь точно знает, где параллелизм, а где последовательность
  3. Безопасность: правила владения естественно расширяются, без дополнительных ограничений типов
  4. Контролируемость: нет неявного параллелизма, нет неожиданного параллельного поведения
  5. Синхронность: вызывающая сторона блокируется синхронно, без обратных вызовов и await

Предложение

1. Сущность блока {}: вычислительная единица, управляемая зависимостями

В YaoXiang {} — это вычислительная единица, управляемая зависимостями.

АтрибутОписание
Управление зависимостямиПри выполнении блок проверяет готовность всех внутренних переменных; если все готовы — выполняется немедленно, иначе блокируется
Время выполненияОпределяется зависимостями, не связано с "немедленным" или "отложенным"
Возвращаемое значениеИспользуйте return для явного возврата; без return по умолчанию возвращается Void
Унифицированный синтаксисСемантика одинакова независимо от того,出现在函数体、变量初始化还是 spawn
Изоляция области видимостиПеременные строго ограничены внутри {}, не проникают во внешнюю область видимости
yaoxiang
// Пример управления зависимостями
x = compute_x()        // x готов
y = compute_y()        // y готов
result = {
    // Зависит от x и y, выполняется немедленно после готовности обоих
    return x + y
}

2. Семантика выражения spawn

spawn <expr> — это единственный примитив параллелизма в YaoXiang. Может модифицировать любое выражение, форма выражения определяет гранулярность разбиения задач.

2.1 Правила создания задач

Форма выраженияРазбиение задачСинхронная семантика
spawn { a, b, c }Прямые подвыражения → N независимых задачОжидание завершения всех задач
spawn for x in items { body }Каждая итерация → 1 задачаОжидание завершения всех итераций
spawn while cond { body }Каждая итерация → 1 задача (условие между итерациями)Ожидание условия false
spawn if c { a } else { b }Условие c вычисляется последовательно, выбранная ветвь целиком → 1 задачаОжидание завершения выбранной ветви
spawn call(x)Сам вызов → 1 задачаОжидание завершения вызова
spawn expr (любое выражение)Само выражение → 1 задачаОжидание завершения выражения

Мотивация дизайна: Почему spawn может модифицировать любое выражение? Подробнее в RFC-032 §Основной дизайн.

Ортогональность управления потоком: Семантическая разница между spawn <expr> (spawn впереди) и <expr> spawn { body } (spawn позади) описана в RFC-032 §Ортогональность управления потоком (основное определение). Семантика времени выполнения всех обратных комбинаций (for ... spawn { } / while ... spawn { } / if ... spawn { }) — распространение ошибок, типы ресурсов, правила вложенности — наследуют правила §2.4 / §2.5 / §2.6 этого документа.

yaoxiang
// spawn блока: прямые подвыражения параллельно
(a, b) = spawn {
    t1 = fetch("url1")   // Прямое подвыражение → параллельная задача 1
    t2 = fetch("url2")   // Прямое подвыражение → параллельная задача 2
    return (t1, t2)      // Явный возврат кортежа
}

// spawn for: каждая итерация параллельно
results = spawn for item in items {
    process(item)        // Каждая итерация → независимая задача
}

// spawn while: каждая итерация параллельно
spawn while has_next() {
    step()               // Каждая итерация → независимая задача
}

// spawn if: выбранная ветвь целиком как задача
result = spawn if cond {
    branch_a()
} else {
    branch_b()
}

2.2 Изоляция области видимости

Выражение spawn создаёт независимую область видимости; внутренние переменные не влияют на внешние:

yaoxiang
x = 10
result = spawn {
    x = 20              // Это локальный x внутри выражения spawn
    compute(x)
}
// x по-прежнему равен 10

result = spawn for item in items {
    item = item + 1     // Локальная item итерации, независимая копия каждой итерации
    process(item)
}
// Внешняя item не затрагивается

Итерационные переменные (x в for) имеют независимую копию в каждой итерации и автоматически уничтожаются после её завершения.

2.3 Правила владения

После того как переменная входит в выражение spawn, внешний код больше не может её использовать (семантика Move):

yaoxiang
data = load_data()
result = spawn {
    process(data)       // Владение data перемещается в выражение spawn
}
// data здесь недоступна (перемещена)

Если нужно поделиться между несколькими задачами, используйте ref:

yaoxiang
data = load_data()
shared = ref data       // Компилятор автоматически выбирает Rc или Arc

result = spawn {
    process_a(shared),  // Общий доступ по ссылке
    process_b(shared)   // Общий доступ по ссылке
}

Общий доступ между итерациями: Используйте ref для захвата во внешнюю область; общий доступ к одной ссылке между итерациями.

2.4 Правила распространения ошибок

spawn { a, b, c } (блок)
  1. Ожидание завершения всех задач (даже если некоторые уже завершились с ошибкой)
  2. Распространение первой встреченной ошибки
  3. Использование ? для явного обозначения точки распространения ошибки
yaoxiang
(a, b) = spawn {
    fetch("url1")?,     // Может завершиться с ошибкой
    fetch("url2")?      // Может завершиться с ошибкой
}
// Если любая задача завершилась с ошибкой, всё выражение spawn распространяет первую ошибку
spawn for x in items { body? }
  • Ожидание завершения всех итераций, затем возврат первой ошибки
  • После неудачной итерации оставшиеся итерации продолжают выполняться (не отменяются)
  • Использование ? для явного обозначения точки распространения ошибки
yaoxiang
results = spawn for item in items {
    process(item)?      // Любая неудачная итерация → ожидание всех → распространение первой ошибки
}
spawn while cond { body? }

Наследует собственную семантику ошибок while:

  • step использует ? для распространения ошибки → весь spawn while завершается неудачно, следующий раунд не начинается
  • step не распространяет ошибку (ошибка поглощается) → переход к следующей итерации
yaoxiang
spawn while has_next() {
    item = next()       // Без распространения ошибки, неудача также переходит к следующему раунду
    process(item)
}
spawn if c { a } else { b }
  • Условие c вычисляется последовательно
  • Ошибка при вычислении c → общая ошибка
  • Ошибка внутри выбранной ветви → общая ошибка
yaoxiang
result = spawn if cond()? {  // cond вычисляется последовательно, неудача → общая ошибка
    fetch_a()?
} else {
    fetch_b()?
}

2.5 Правила типов ресурсов

Компилятор отслеживает использование типов ресурсов во время компиляции, обеспечивая безопасность параллелизма:

Тип ресурсаОписаниеПоведение компилятора
FilePathПуть к файловой системеОперации с одним путём автоматически сериализуются
HttpUrlHTTP endpointОперации с одним URL автоматически сериализуются
DBUrlПодключение к БДОперации с одним подключением автоматически сериализуются
ConsoleСтандартный выводВсе операции Console автоматически сериализуются
Внутри блока spawn { ... }
yaoxiang
// Операции с одним файлом автоматически сериализуются
(a, b) = spawn {
    read_file("data.txt"),      // Сначала выполняется
    write_file("data.txt", x)   // Ожидание завершения чтения
}
Кросс-итерации spawn for ... { ... } с одним ресурсом

Когда все итерации обращаются к ресурсу одного типа, компилятор автоматически понижает до последовательного выполнения (spawn деградирует до последовательного for, без ошибок):

yaoxiang
// Все итерации записывают в один файл → автоматическое понижение до последовательного выполнения
results = spawn for item in items {
    write_file("data.txt", item)
}
// Компилятор автоматически сериализует все итерации

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

spawn while ... { ... } захватывает &mut

Ошибка времени компиляции: spawn while не позволяет захватывать внешние переменные типа &mut:

yaoxiang
iter = make_iter()
spawn while iter.has_next() {       // Ошибка времени компиляции
    item = iter.next()              // iter имеет тип &mut, общий доступ с изменением между итерациями = гонка данных
}

Не повторное введение trait Sync: Соответствует обещанию RFC-024 "без Send/Sync". Требуется от пользователя использовать ref или написать без spawn.

spawn if c { ... } else { ... } обе ветви с одним ресурсом

Легально без предупреждений: Условия if взаимоисключающие, выполняется максимум одна ветвь, конфликта параллелизма не существует:

yaoxiang
result = spawn if use_cache {
    load_from_cache(key)            // Ветвь 1: чтение из cache
} else {
    fetch(key)                      // Ветвь 2: чтение из URL
}

2.6 Вложенный spawn

Выражения spawn могут быть вложенными; внутренний spawn создаёт независимую параллельную область:

yaoxiang
(a, b) = spawn {
    x = spawn {
        fetch("url1"),
        fetch("url2")
    },
    y = compute(x)
}

Семантика вложенности:

  • Внутренний spawn — независимая параллельная область (независимая очередь задач, независимое распространение ошибок)
  • Внутренние ошибки распространяются независимо во внешнюю область (внешняя задача получает ошибку при ожидании завершения внутренней)
  • Правила типов ресурсов внутреннего spawn отслеживаются независимо (не проверяются совместно с внешним)
yaoxiang
// Вложенный spawn for в spawn while
results = spawn for x in items {
    inner = spawn while has_more(x) {
        step(x)
    }
    process(inner)
}

3. Разрыв со старым дизайном

Старый дизайн (RFC-001)Новый дизайн (RFC-024 + RFC-032)
Автоматический анализ DAG всей программыАнализ только внутри выражений spawn
Аннотации @block/@eager/@autoБез аннотаций, управление зависимостями
trait Send/SyncНе требуются; владение + ref автоматически
future/неблокирующие дескрипторыСинхронная блокировка, без обратных вызовов
Mutex/RwLock/Atomicref автоматически выбирает Rc/Arc
Трёхуровневая ментальная модель L1/L2/L3Обычный код последовательный, выражения spawn параллельные
Окрашивание функций (async/await)Без окрашивания функций
spawn только модифицирует блоки {}spawn модифицирует любое выражение (см. RFC-032)

4. Правила возврата

Правила возврата в YaoXiang унифицированы и однозначны:

ЗаписьВозвращаемое значениеПояснение
= expr (без фигурных скобок)Непосредственный возврат exprВыражение является значением
= { ... } (с фигурными скобками)Требуется return, иначе возврат VoidБлок требует явного возврата
yaoxiang
// Без фигурных скобок: непосредственный возврат
add: (a: Int, b: Int) -> Int = a + b

// С фигурными скобками: требуется return
process: (data: Data) -> Result = {
    validated = validate(data)?
    return ok(transform(validated))
}

// С фигурными скобками, но без return: возврат Void
log: (message: String) -> Void = {
    print(message)  // Без return, возврат Void
}

5. Ментальная модель пользователя

Ваш обычный код выполняется последовательно.

Когда вы хотите, чтобы несколько вещей происходили одновременно, вы помещаете их в spawn <expr>.

Форма выражения определяет, как разбиваются задачи: каждое прямое подвыражение внутри блока параллельно; каждая итерация for параллельно; выбранная ветвь if как одна задача.

Всё выражение spawn синхронно блокируется, ожидая завершения всех задач.

Никаких обратных вызовов, никаких await, никаких странных аннотаций.

yaoxiang
// Обычный код: последовательное выполнение
a = compute_a()         // Сначала выполняется
b = compute_b(a)        // Зависит от a, выполняется после завершения a
c = compute_c(b)        // Зависит от b, выполняется после завершения b

// Когда нужен параллелизм: используем spawn
(x, y, z) = spawn {
    fetch("url1"),      // Параллельно
    fetch("url2"),      // Параллельно
    fetch("url3")       // Параллельно
}
// Продолжаем после завершения всех
process(x, y, z)

// Параллелизм данных: spawn for
results = spawn for item in items {
    process(item)
}

Компромиссы

Преимущества

  1. Простота: только один примитив параллелизма (spawn), может модифицировать любое выражение
  2. Явность: пользователь точно знает, где параллелизм, где последовательность; нет неявного параллелизма
  3. Безопасность: правила владения естественно расширяются; не требуются дополнительные ограничения типов вроде Send/Sync
  4. Контролируемость: нет автоматического параллелизма; избегаются неожиданные проблемы параллелизма
  5. Синхронность: вызывающая сторона блокируется синхронно; код легко понять и отладить
  6. Без окрашивания функций: не существует проблемы окрашивания функций async/await
  7. Эффективная компиляция: Анализ DAG только внутри выражений spawn; время компиляции контролируемо
  8. Ортогональность: spawn естественно сочетается с любой структурой управления потоком (подробнее в RFC-032)

Недостатки

  1. Требуется явный spawn: автоматический параллелизм невозможен; пользователь должен вручную помечать точки параллелизма
  2. Анализ DAG внутри выражений spawn: компилятору необходимо выполнять анализ зависимостей внутри выражений spawn
  3. Несовместимость со старым кодом: код, использующий старый паттерн RFC-001, требует миграции

Альтернативные решения

РешениеПочему не выбрано
Автоматический DAG всей программы (RFC-001)Высокая сложность, долгое время компиляции, неконтролируемое поведение
async/awaitОкрашивание функций, крутая кривая обучения, плохая читаемость кода
goroutineОтсутствие безопасности типов, трудно обнаружить гонки данных
Модель ActorСложность передачи сообщений, трудности отладки
CSP (каналы Go)Отсутствие безопасности типов, трудно обнаружить взаимоблокировки
spawn только для блоков {}Нарушает ортогональность; spawn for становится частным случаем (см. RFC-032)

Стратегия реализации

Анализ времени компиляции

  1. Распознавание формы выражения: согласно форме выражения после spawn определяется разбиение задач (подробнее в RFC-032 §Анализ DAG)
  2. Построение DAG: анализ зависимостей внутри выражения spawn
  3. Топологическая сортировка: определение порядка выполнения внутри выражения spawn
  4. Распознавание параллелизма: распознавание поддеревьев без зависимостей внутри выражения spawn
  5. Анализ побега: ref → Rc или Arc
  6. Обнаружение конфликтов ресурсов: обнаружение потенциальных конфликтов типов ресурсов

Организация модулей

Код, связанный с spawn, унифицированно размещается в frontend/core/spawn/:

frontend/core/spawn/
├── mod.rs           # Точка входа модуля spawn
├── placement.rs     # Проверка легитимности позиций появления spawn
└── analysis.rs      # Распознавание задач, анализ зависимостей, обнаружение конфликтов ресурсов

Пояснение по миграции (2026-06-11): Существующий frontend/core/typecheck/passes/spawn_placement.rs будет перемещён в frontend/core/spawn/placement.rs. Объявление модуля spawn_placement в каталоге typecheck/passes/ необходимо同步 удалить.

Выполнение во время выполнения

Ссылка на архитектуру Runtime из RFC-008:

  • Embedded Runtime: без поддержки spawn, немедленное выполнение
  • Standard Runtime: поддержка выражений spawn
  • Full Runtime: Standard + балансировка нагрузки WorkStealer

Зависимости

  • RFC-008 (архитектура Runtime) → Завершено
  • RFC-009 (модель владения) → Завершено
  • RFC-010 (унифицированный синтаксис типов) → Завершено
  • RFC-011 (обобщённая система) → Завершено
  • RFC-032 (рефакторинг AST/IR) → Совместно с этим документом определяет spawn

Запись решений по дизайну

РешениеРешениеПричинаДата
Примитив параллелизмаspawn <expr>Простота, явность, контролируемость2026-06-05
Диапазон модификации spawnЛюбое выражениеСинтаксическая ортогональность, устранение специализации spawn for2026-07-04
Разбиение задачОпределяется формой выраженияСильная выразительность, унифицированные правила2026-07-04
Модель выполненияСинхронная блокировкаЛегко понять, легко отлаживать2026-06-05
Диапазон анализа DAGТолько внутри выражений spawnЭффективная компиляция, контролируемое поведение2026-06-05
Механизм совместного использованияref автоматически выбирает Rc/ArcУпрощение решений пользователя2026-06-05
АннотацииНетУменьшение шума кода2026-06-05
Send/SyncУдаленыВладение + ref достаточно2026-06-05
Mutex/RwLockУдаленыref автоматически обрабатывает2026-06-05
future/дескрипторыУдаленыСинхронная блокировка проще2026-06-05
Окрашивание функцийНетИзбежание проблем async/await2026-06-05
Типы ресурсовВстроенные + определяемые пользователемАвтоматическая сериализация2026-06-05
Ошибки spawn {}Ожидание всех, распространение первой ошибкиДетерминированное поведение2026-06-05
Ошибки spawn forОжидание всех, распространение первой ошибкиСогласованность с spawn {}2026-07-04
Ошибки spawn whileНаследует семантику ошибок whileСтандартное поведение while2026-07-04
Ошибки условия spawn ifc вычисляется последовательно, неудача → общая ошибкаСоответствует интуиции2026-07-04
spawn for с одним ресурсомАвтоматическое понижение до последовательногоБезопасное понижение, не грубый отказ2026-07-04
spawn while захватывает &mutОшибка времени компиляцииИзбежание гонок данных, без введения Sync2026-07-04
spawn if с одним ресурсомЛегально без предупрежденийВзаимоисключающие ветви не создают конфликта2026-07-04
Вложенный spawnВнутренний — независимая параллельная областьНезависимые очереди задач, ошибки, ресурсы2026-07-04

Библиографические ссылки

Официальная документация YaoXiang

Внешние ссылки


Жизненный цикл и судьба

СтатусРасположениеПояснение
Принято (пересмотренная версия)docs/design/rfc/accepted/Совместно с RFC-032 определяет spawn (семантика времени выполнения)