Skip to content

并发模型规范 ​

状态:正式规范。基于 RFC-024(并发模型)、RFC-009(所有权模型)、RFC-008(运行时架构)。

本文件定义 YaoXiang 编程语言的并发模型规范,包括 {} 块语义、spawn 并发原语、所有权交互、错误处理和资源类型。

核心设计——一个原语,一个规则:

spawn { ... }        ← 唯一并行原语
直接子赋值创建任务    ← 唯一规则
同步阻塞等待结果      ← 唯一行为

第一章:概述 ​

1.1 {} 块的本质 ​

在 YaoXiang 中,{} 是一个依赖驱动的计算单元。

属性说明
依赖驱动块在执行时检查内部所有变量是否就绪,若齐备则立即执行,否则阻塞等待
执行时机由依赖决定,与"立即"或"延迟"无关
返回值使用 return 显式返回值;无 return 时默认返回 Void
语法统一无论出现在函数体、变量初始化还是 spawn 后,语义一致
作用域隔离变量严格限于 {} 内部,不穿透到外层作用域
yaoxiang
// 依赖驱动示例
x = compute_x()        // x 就绪
y = compute_y()        // y 就绪
result = {
    // 依赖 x 和 y,两者就绪后立即执行
    return x + y
}

1.2 返回规则 ​

写法返回值说明
= 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
}

1.3 spawn 块语义 ​

spawn { ... } 是 YaoXiang 中唯一的并行原语。

核心规则:

  • spawn 块的直接子赋值创建并行任务
  • 嵌套 {} 内的赋值不算作独立任务
  • 整个 spawn 块同步阻塞,等待所有任务完成后返回结果
  • 没有回调、await 或注解
yaoxiang
// 两个任务并行执行
(a, b) = spawn {
    fetch("url1"),      // 任务 1
    fetch("url2")       // 任务 2
}
// 等待两者都完成后继续

1.4 用户心智模型 ​

你写的普通代码是顺序执行的。当你希望多件事一起做时,把它们放进 spawn { ... } 块里。块里的每个直接赋值都会立即开始(并行),你需要的结果会自动等待。整个块会等所有事做完,然后把最终结果给你。没有回调,没有 await,没有奇怪的注解。


第二章:语法与语义 ​

2.1 普通代码 ​

普通代码(spawn 块外部)是顺序执行的。

yaoxiang
a = compute_a()     // 先执行
b = compute_b(a)    // 依赖 a,a 完成后执行
c = compute_c(b)    // 依赖 b,b 完成后执行

2.2 spawn 块 ​

SpawnBlock  ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' SpawnBody '}'
SpawnBody   ::= Assignment (',' Assignment)*

语义:

  1. spawn 块内的直接子赋值作为独立任务并行执行
  2. 每个任务的结果绑定到对应的模式变量
  3. 整个块阻塞直到所有任务完成
  4. 返回所有结果的元组
yaoxiang
// 单任务
result = spawn {
    fetch("url")
}

// 多任务
(a, b, c) = spawn {
    fetch("url1"),
    fetch("url2"),
    fetch("url3")
}

2.3 函数体中的 spawn ​

函数体本身是一个 {} 块,可以在其中使用 spawn。

yaoxiang
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 '}'

语义:数据并行循环,每次迭代作为独立任务。

yaoxiang
// 并行处理列表中的每个元素
results = spawn for item in items {
    result = process(item)
}

注意:spawn for 的循环体是独立任务,不支持跨迭代共享可变状态。如需聚合结果,应使用 spawn for 收集结果后在外部处理。

yaoxiang
// 正确:并行处理后在外部聚合
transformed = spawn for item in items {
    result = transform(item)
}
total = sum(transformed)   // 顺序聚合

2.5 嵌套 spawn ​

spawn 块可以嵌套,内层 spawn 创建新的并发域。

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

内层 spawn 的直接子赋值才是任务,外层 spawn 不会穿透。


第三章:与所有权模型的交互 ​

3.1 Move 语义 ​

Move 是 YaoXiang 的默认语义(零拷贝)。变量进入 spawn 块后,外部不能再使用。

yaoxiang
data = load_data()
result = spawn {
    process(data)   // data 的所有权移入 spawn 块
}
// data 在此处不可用(已 move)

3.2 借用令牌 ​

&T 和 &mut T 是零大小的编译期权限证明,不能跨任务边界。这不是特殊规则——令牌是编译期权限证明,跨任务共享请使用 ref。

yaoxiang
data = load_data()

// 编译错误:借用令牌不能跨任务
result = spawn {
    process(&data)   // 错误!&T 不能跨任务传递
}

令牌类型属性:

令牌首要语义次级属性
&T冻结源数据——ReadToken 存活期间,任何 WriteToken(T) 不可获取零大小,可复制(Dup)——冻结保证下多份只读天然安全
&mut T独占读写——WriteToken 存活期间,任何其他令牌(读或写)不可共存零大小,线性(非 Dup)——独占访问下复制无意义

因果顺序:ReadToken 的 Dup 是冻结保证的推论,不是反过来。数据被冻结(无突变可能)→ 多份只读视图安全 → 可以实现 Dup。如果把 Dup 当定义、冲突检查当补丁,因果就倒了。

3.3 ref 共享 ​

ref 是跨作用域共享的唯一方式。编译器自动选择 Rc(单任务)或 Arc(跨任务),用户不需要关心。

yaoxiang
data = load_data()
shared = ref data       // 编译器自动选择 Rc 或 Arc

result = spawn {
    process_a(shared),  // 共享引用
    process_b(shared)   // 共享引用
}

编译器选择策略:

条件选择原因
默认(无法证明安全性)Arc安全优先,避免数据竞争
编译器能证明数据仅在单任务内使用Rc无原子操作开销

ref vs 借用令牌:

&T / &mut Tref
做什么看一眼/原地改共享持有
成本零开销(零大小类型)Rc 或 Arc(编译器选)
跨任务不可可(编译器自动选 Arc)

3.4 闭包与捕获 ​

闭包不隐式捕获外层变量(RFC-009 2026-06-16 决议,SPEC §12.3)。Lambda 只使用 显式参数与自身局部变量;需要外层数据时,用显式参数传入,或经柯里化在创建点固化。 隐式捕获的编译错误码为 E1001。

为什么禁止:闭包定义处的外层作用域在闭包逃逸后可能已死,隐式捕获的引用无法保证 存活;柯里化固化的值在创建点(调用点作用域存活)取值,安全且零隐藏成本。

yaoxiang
data = load_data()
fn = (x: Int) -> Int = data.value + x   // ❌ 编译错误 E1001:隐式捕获 data
yaoxiang
// ✅ 正确方式一:显式参数传入
add: (data: Data, x: Int) -> Int = data.value + x

// ✅ 正确方式二:柯里化固化(上下文在创建点固化,闭包只吃参数)
mk: (data: Data) -> (x: Int) -> Int = (data) => (x) => data.value + x

spawn 内的共享:需要跨任务共享时,用 ref 显式共享持有(RFC-024):

yaoxiang
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.1 ? 运算符 ​

? 运算符用于显式错误传播,与 Rust 语义一致。

yaoxiang
read_file: (path: FilePath) -> Result(String, IoError) = {
    content = open(path)?      // 如果错误,立即传播
    return content.read_all()
}

4.2 spawn 块内错误传播 ​

规则:

  1. 等待所有任务完成(即使某些任务已失败)
  2. 传播第一个遇到的错误
  3. 使用 ? 显式标记错误传播点
yaoxiang
(a, b) = spawn {
    fetch("url1")?,     // 可能失败
    fetch("url2")?      // 可能失败
}
// 如果任一任务失败,整个 spawn 块传播第一个错误

4.3 错误类型 ​

自动生成:编译器自动生成联合错误类型。

yaoxiang
// 编译器推断出错误类型为 HttpError | IoError
(a, b) = spawn {
    fetch("url"),           // 可能抛出 HttpError
    read_file("data.txt")  // 可能抛出 IoError
}

手动覆盖:用户可手动定义统一错误类型。

yaoxiang
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.1 内置资源类型 ​

资源类型说明编译器行为
FilePath文件系统路径同路径操作自动串行
HttpUrlHTTP 端点同 URL 操作自动串行
DBUrl数据库连接同连接操作自动串行
Console标准输出所有 Console 操作自动串行
yaoxiang
// 同一文件的操作自动串行化
(a, b) = spawn {
    read_file("data.txt"),      // 先执行
    write_file("data.txt", x)   // 等待读取完成
}

5.2 用户自定义资源类型 ​

用户自定义资源类型需显式标记。

yaoxiang
Database: Type = {
    connection_string: String,
    query: (db: Database, sql: String) -> Result(Rows, DbError)
}

5.3 副作用追踪 ​

编译器追踪资源类型的使用,确保并发安全。

yaoxiang
// 编译器警告:Console 操作可能交错
spawn {
    print("Hello"),     // 可能与下一行交错
    print("World")
}

// 正确:显式串行
spawn {
    print("Hello\nWorld")
}

第六章:编译器行为 ​

6.1 DAG 分析 ​

编译器在编译期分析 spawn 块内的依赖关系(DAG),确定:

  1. 哪些表达式可以并行
  2. 哪些必须串行
  3. 如何分配任务
yaoxiang
(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 块内的任务没有实际并行机会,编译器发出警告。

yaoxiang
// 编译器警告:无并行机会
result = spawn {
    a = fetch("url")    // 唯一任务
}
// 建议:直接使用普通代码
result = fetch("url")

6.4 资源冲突检测 ​

编译器检测资源类型的潜在冲突。

yaoxiang
// 编译错误:同一文件的并发写入
spawn {
    write_file("data.txt", "a"),
    write_file("data.txt", "b")  // 错误!
}

第七章:运行时分层 ​

编译阶段完全相同,区别仅在运行时执行方式(RFC-008)。

层级spawn 支持DAG 分析适用场景
Embedded Runtime❌无WASM、游戏脚本、规则引擎
Standard Runtime✅spawn 块内Web 服务、数据管道
Full Runtime✅spawn 块内 + 工作窃取科学计算、大规模并行

Embedded Runtime:即时执行器,无 spawn 支持,高性能低占用。

Standard Runtime:支持 spawn {} 块,在 spawn 块内进行 DAG 分析和自动并发。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' Expr

A.4 资源类型标记 ​

ResourceDecl ::= Identifier ':' 'Type' '=' RecordType