Спецификация модели конкурентности
Статус: Официальная спецификация. Основана на RFC-024 (Модель конкурентности), RFC-009 (Модель владения), RFC-008 (Архитектура среды выполнения).
Данный документ определяет спецификацию модели конкурентности языка программирования YaoXiang, включая семантику блоков {}, примитив параллелизма spawn, взаимодействие с владением, обработку ошибок и типы ресурсов.
Ключевая идея дизайна — один примитив, одно правило:
spawn { ... } ← единственный примитив параллелизма
прямое присваивание создаёт задачу ← единственное правило
синхронное блокирующее ожидание результата ← единственное поведениеГлава 1: Обзор
1.1 Сущность блоков {}
В YaoXiang {} — это вычислительная единица, управляемая зависимостями.
| Атрибут | Описание |
|---|---|
| Управление зависимостями | Блок при выполнении проверяет готовность всех внутренних переменных; если все готовы — выполняется немедленно, иначе блокируется в ожидании |
| Время выполнения | Определяется зависимостями, не зависит от «немедленного» или «отложенного» выполнения |
| Возвращаемое значение | Используйте return для явного возврата; без return по умолчанию возвращается Void |
| Единый синтаксис | Независимо от того, появляется ли в теле функции, инициализации переменной или после spawn, семантика одинакова |
| Изоляция области видимости | Переменные строго ограничены внутренней частью {}, не проникают во внешнюю область видимости |
// Пример управления зависимостями
x = compute_x() // x готов
y = compute_y() // y готов
result = {
// Зависит от x и y; выполняется сразу, как только оба готовы
return x + y
}1.2 Правила возврата
| Запись | Возвращаемое значение | Описание |
|---|---|---|
= expr (без фигурных скобок) | Возвращает expr напрямую | Выражение есть значение |
= { ... } (с фигурными скобками) | Необходимо использовать return, иначе возвращается Void | Блок требует явного возврата |
// Без фигурных скобок: прямой возврат
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
}1.3 Семантика блока spawn
spawn { ... } — это единственный примитив параллелизма в YaoXiang.
Основные правила:
- Прямые дочерние присваивания в блоке spawn создают параллельные задачи
- Присваивания во вложенных
{}не считаются отдельными задачами - Весь блок spawn блокирует выполнение синхронно, ожидая завершения всех задач перед возвратом результата
- Нет обратных вызовов,
awaitили аннотаций
// Две задачи выполняются параллельно
(a, b) = spawn {
fetch("url1"), // Задача 1
fetch("url2") // Задача 2
}
// Продолжение после завершения обеих1.4 Модель мышления пользователя
Ваш обычный код выполняется последовательно. Когда вы хотите, чтобы несколько вещей выполнялось одновременно, поместите их в блок
spawn { ... }. Каждое прямое присваивание в блоке немедленно запускается (параллельно), и нужный вам результат будет автоматически ожидаться. Весь блок ждёт завершения всех задач, а затем возвращает вам конечный результат. Никаких обратных вызовов, никакихawait, никаких странных аннотаций.
Глава 2: Синтаксис и семантика
2.1 Обычный код
Обычный код (вне блока spawn) выполняется последовательно.
a = compute_a() // Выполняется первым
b = compute_b(a) // Зависит от a, выполняется после завершения a
c = compute_c(b) // Зависит от b, выполняется после завершения b2.2 Блок spawn
SpawnBlock ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' SpawnBody '}'
SpawnBody ::= Assignment (',' Assignment)*Семантика:
- Прямые дочерние присваивания в блоке spawn выполняются как независимые задачи параллельно
- Результат каждой задачи связывается с соответствующей переменной шаблона
- Весь блок блокируется до завершения всех задач
- Возвращается кортеж всех результатов
// Одна задача
result = spawn {
fetch("url")
}
// Несколько задач
(a, b, c) = spawn {
fetch("url1"),
fetch("url2"),
fetch("url3")
}2.3 spawn в теле функции
Тело функции само по себе является блоком {}, в котором можно использовать spawn.
fetch_and_parse: (urls: List(String)) -> List(Data) = {
results = spawn for url in urls {
parsed = parse(fetch(url))
}
return results
}2.4 spawn в цикле
SpawnFor ::= Identifier '=' 'spawn' 'for' Identifier 'in' Expr '{' Assignment '}'Семантика: Параллельный по данным цикл, каждая итерация как независимая задача.
// Параллельная обработка каждого элемента списка
results = spawn for item in items {
result = process(item)
}Примечание: Тело цикла
spawn forсостоит из независимых задач и не поддерживает общее изменяемое состояние между итерациями. Если требуется агрегация результатов, соберите результаты черезspawn forи обработайте их снаружи.
// Правильно: параллельная обработка с последующей внешней агрегацией
transformed = spawn for item in items {
result = transform(item)
}
total = sum(transformed) // Последовательная агрегация2.5 Вложенный spawn
Блоки spawn могут быть вложенными, при этом внутренний spawn создаёт новую область конкурентности.
(a, b) = spawn {
x = spawn {
fetch("url1"),
fetch("url2")
},
y = compute(x)
}Только прямые дочерние присваивания внутреннего spawn являются задачами; внешний spawn не проникает внутрь.
Глава 3: Взаимодействие с моделью владения
3.1 Семантика перемещения (Move)
Move — это семантика по умолчанию в YaoXiang (zero-copy). После того как переменная попадает в блок spawn, её нельзя использовать снаружи.
data = load_data()
result = spawn {
process(data) // Право владения data перемещается в блок spawn
}
// data здесь недоступен (перемещён)3.2 Токены заимствования
&T и &mut T — это доказательства разрешений нулевого размера на этапе компиляции, которые не могут пересекать границу задач. Это не специальное правило — токены являются доказательствами разрешений на этапе компиляции; для совместного использования между задачами используйте ref.
data = load_data()
// Ошибка компиляции: токен заимствования не может пересекать задачу
result = spawn {
process(&data) // Ошибка! &T не может передаваться между задачами
}Свойства типов токенов:
| Токен | Основная семантика | Вторичные свойства |
|---|---|---|
&T | Замораживает исходные данные — пока существует ReadToken, ни один WriteToken(T) не может быть получен | Нулевой размер, копируемый (Dup) — при гарантии заморозки множественные представления только для чтения безопасны |
&mut T | Исключительное чтение-запись — пока существует WriteToken, никакой другой токен (для чтения или записи) не может сосуществовать | Нулевой размер, линейный (не Dup) — копирование при исключительном доступе бессмысленно |
Причинно-следственная связь: Возможность дублирования ReadToken является следствием гарантии заморозки, а не наоборот. Данные заморожены (мутация невозможна) → множественные представления только для чтения безопасны → можно реализовать Dup. Если принять Dup за определение, а проверку конфликтов за заплатку, причинно-следственная связь нарушается.
3.3 Совместное использование через ref
ref — это единственный способ совместного использования между областями видимости. Компилятор автоматически выбирает Rc (одна задача) или Arc (между задачами), пользователю не нужно об этом заботиться.
data = load_data()
shared = ref data // Компилятор автоматически выберет Rc или Arc
result = spawn {
process_a(shared), // Общая ссылка
process_b(shared) // Общая ссылка
}Стратегия выбора компилятора:
| Условие | Выбор | Причина |
|---|---|---|
| По умолчанию (безопасность не доказана) | Arc | Безопасность прежде всего, чтобы избежать гонки данных |
| Компилятор может доказать, что данные используются только в одной задаче | Rc | Отсутствие накладных расходов на атомарные операции |
ref vs токены заимствования:
&T / &mut T | ref | |
|---|---|---|
| Действие | Посмотреть/изменить на месте | Совместное владение |
| Стоимость | Нулевые накладные расходы (тип нулевого размера) | Rc или Arc (выбирает компилятор) |
| Между задачами | Невозможно | Возможно (компилятор автоматически выбирает Arc) |
3.4 Замыкания и захват
Замыкания не захватывают внешние переменные неявно (Решение RFC-009 от 2026-06-16, SPEC §12.3). Lambda использует только явные параметры и собственные локальные переменные; если нужны внешние данные, передавайте их через явные параметры или фиксируйте через каррирование в точке создания. Код ошибки компиляции для неявного захвата — E1001.
Почему запрещено: внешняя область видимости в точке определения замыкания может уже не существовать после его экраута; ссылки, захваченные неявно, не могут гарантировать своё существование. Значения, зафиксированные через каррирование, берутся в точке создания (область видимости вызывающей стороны существует), что безопасно и не несёт скрытых издержек.
data = load_data()
fn = (x: Int) -> Int = data.value + x // ❌ Ошибка компиляции E1001: неявный захват data// ✅ Правильный способ 1: явный параметр
add: (data: Data, x: Int) -> Int = data.value + x
// ✅ Правильный способ 2: фиксация через каррирование (контекст фиксируется в точке создания, замыкание принимает только параметры)
mk: (data: Data) -> (x: Int) -> Int = (data) => (x) => data.value + xСовместное использование внутри spawn: когда требуется совместное использование между задачами, используйте ref для явного совместного владения (RFC-024):
data = load_data()
shared = ref data
result = spawn {
((x: Int) -> Int = shared.value + x)(1),
((x: Int) -> Int = shared.value + x)(2)
}spawn while запрещает захват внешних переменных типа &mut (Решение RFC-024 от 2026-07-04, ошибка на этапе компиляции, позволяет избежать гонки данных и не вводит Sync).
Глава 4: Обработка ошибок
4.1 Оператор ?
Оператор ? используется для явного распространения ошибок, аналогично семантике Rust.
read_file: (path: FilePath) -> Result(String, IoError) = {
content = open(path)? // При ошибке немедленно распространяет её
return content.read_all()
}4.2 Распространение ошибок в блоке spawn
Правила:
- Ожидание завершения всех задач (даже если некоторые из них уже завершились с ошибкой)
- Распространение первой встреченной ошибки
- Использование
?для явной отметки точки распространения ошибки
(a, b) = spawn {
fetch("url1")?, // Может завершиться ошибкой
fetch("url2")? // Может завершиться ошибкой
}
// Если какая-либо задача завершилась с ошибкой, весь блок spawn распространяет первую ошибку4.3 Типы ошибок
Автоматическая генерация: Компилятор автоматически генерирует объединённый тип ошибок.
// Компилятор выводит тип ошибки как HttpError | IoError
(a, b) = spawn {
fetch("url"), // Может выбросить HttpError
read_file("data.txt") // Может выбросить IoError
}Ручное переопределение: Пользователь может вручную определить единый тип ошибки.
AppError: Type = {
Http: (http_error: HttpError) -> AppError,
Io: (io_error: IoError) -> AppError,
Parse: (parse_error: ParseError) -> AppError
}
process: (url: String, path: FilePath) -> Result(Data, AppError) = {
(a, b) = spawn {
fetch(url).map_err(AppError.Http)?,
read_file(path).map_err(AppError.Io)?
}
return parse(a + b).map_err(AppError.Parse)?
}Глава 5: Типы ресурсов и побочные эффекты
5.1 Встроенные типы ресурсов
| Тип ресурса | Описание | Поведение компилятора |
|---|---|---|
FilePath | Путь в файловой системе | Операции с одним путём автоматически сериализуются |
HttpUrl | Конечная точка HTTP | Операции с одним URL автоматически сериализуются |
DBUrl | Соединение с базой данных | Операции с одним соединением автоматически сериализуются |
Console | Стандартный вывод | Все операции с Console автоматически сериализуются |
// Операции с одним файлом автоматически сериализуются
(a, b) = spawn {
read_file("data.txt"), // Выполняется первой
write_file("data.txt", x) // Ожидает завершения чтения
}5.2 Пользовательские типы ресурсов
Пользовательские типы ресурсов должны быть явно помечены.
Database: Type = {
connection_string: String,
query: (db: Database, sql: String) -> Result(Rows, DbError)
}5.3 Отслеживание побочных эффектов
Компилятор отслеживает использование типов ресурсов для обеспечения безопасности параллелизма.
// Предупреждение компилятора: операции Console могут чередоваться
spawn {
print("Hello"), // Может чередоваться со следующей строкой
print("World")
}
// Правильно: явная сериализация
spawn {
print("Hello\nWorld")
}Глава 6: Поведение компилятора
6.1 DAG-анализ
Компилятор на этапе компиляции анализирует зависимости (DAG) внутри блока spawn, определяя:
- Какие выражения могут выполняться параллельно
- Какие должны выполняться последовательно
- Как распределять задачи
(a, b, c) = spawn {
x = fetch("url1"), // Задача 1
y = fetch("url2"), // Задача 2 (параллельно с задачей 1)
z = process(x, y) // Задача 3 (зависит от x и y, должна ждать)
}6.2 Выбор Rc/Arc (консервативная стратегия)
Компилятор использует консервативную стратегию, по умолчанию выбирая Arc для обеспечения потокобезопасности:
- По умолчанию
Arc: Когда компилятор не может определить, чтоrefиспользуется только в одной задаче, консервативно выбираетсяArc - Понижение до
Rc: Только когда компилятор может доказать через DAG-анализ, что данные никогда не будут разделены между задачами, происходит понижение доRc - Лучше медленнее, чем с ошибкой: Дополнительные расходы от выбора
Arcнамного меньше, чем риск гонки данных
6.3 Предупреждение об отсутствии параллелизма
Если задачи в блоке spawn не имеют реальной возможности параллельного выполнения, компилятор выдаёт предупреждение.
// Предупреждение компилятора: нет возможности параллельного выполнения
result = spawn {
a = fetch("url") // Единственная задача
}
// Рекомендация: используйте обычный код
result = fetch("url")6.4 Обнаружение конфликтов ресурсов
Компилятор обнаруживает потенциальные конфликты типов ресурсов.
// Ошибка компиляции: параллельная запись в один файл
spawn {
write_file("data.txt", "a"),
write_file("data.txt", "b") // Ошибка!
}Глава 7: Уровни среды выполнения
Этап компиляции полностью идентичен, отличие только в способе выполнения в среде выполнения (RFC-008).
| Уровень | Поддержка spawn | DAG-анализ | Сценарии использования |
|---|---|---|---|
| Embedded Runtime | ❌ | Нет | WASM, игровые скрипты, движки правил |
| Standard Runtime | ✅ | Внутри блока spawn | Веб-сервисы, конвейеры данных |
| Full Runtime | ✅ | Внутри блока spawn + work stealing | Научные вычисления, массовый параллелизм |
Embedded Runtime: Мгновенный исполнитель, без поддержки spawn, высокая производительность и низкое потребление.
Standard Runtime: Поддерживает блок spawn {}, выполняет DAG-анализ и автоматический параллелизм внутри блока spawn. num_workers=1 — однопоточный режим.
Full Runtime: Standard + балансировка нагрузки WorkStealer.
Приложение: Краткий справочник по синтаксису
A.1 Оператор spawn
SpawnBlock ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' SpawnBody '}'
SpawnFor ::= Identifier '=' 'spawn' 'for' Identifier 'in' Expr '{' Assignment '}'
SpawnStmt ::= SpawnBlock | SpawnFor
SpawnBody ::= Assignment (',' Assignment)*A.2 Обработка ошибок
Expr '?' // Распространение ошибки (тип Result)A.3 Выражение ref
RefExpr ::= 'ref' ExprA.4 Маркировка типа ресурса
ResourceDecl ::= Identifier ':' 'Type' '=' RecordType