网络与并发编程指南
对标版本:XTC v1.0-rc | 状态:异步/并行/通道/状态机/调度器/选已落地
并发模型
玄铁采用 双层架构:
| 层级 | 机制 | 说明 |
|---|---|---|
| OS 线程层 | 工作窃取线程池 | 执行 CPU 密集型任务,线程数 = CPU 核数 |
| 用户态调度层 | 无栈状态机 + 调度器 | 管理 I/O 等待任务,就绪队列 + 定时器堆 |
异步 { ... } 的执行层由编译器按块内容自动选择(路由规则见 8.1.4)。
异步任务
异步 { ... } 将代码块作为并发任务执行,立即返回任务句柄。
设 task = 异步 {
示("任务执行中...")
时.睡(1000)
示("任务完成")
}
示("主线程继续执行...")
等待(task)
示("任务已结束")执行流程:
- 编译器统计异步块内的挂起点(
等待/收/发/选/时.睡)位置 - 仅在语句级 → 编译为无栈状态机(poll 函数 + 状态结构体),
xt_fiber_spawn直接提交用户态调度器 - 循环体内含挂起点 → 整体编译为独立函数,
xt_async_spawn提交线程池 - 主线程执行到
等待或程序末尾时驱动调度器事件循环 - 任务的
返回值可被等待取回(无返则得空)
并行执行
并行 { {块1}, {块2}, ... } 并发执行多个代码块。
设 results = 并行 {
{ 时.睡(500); 返 "A" }
{ 时.睡(300); 返 "B" }
{ 返 "C" }
}
// results = ["A", "B", "C"]行为约定与禁忌(必读)
- 主线程的
收/发默认阻塞(空等货、满等位)。主线程阻塞期间会顺带驱动 fiber 调度器,不会死锁;但「永远等不到货」的收会让程序永远停在那里——这不是卡死,是阻塞语义。 - fiber 任务的返回值只能取一次。第二次
等待同一个已完成的 fiber 得到的是空(槽位已回收复用)。需要多次使用的结果请先存进变量。 - fiber 内捕获的外层局部变量是 spawn 时刻的值副本。spawn 之后外层再修改,fiber 内看到的是旧值;fiber 内对它的修改也不会写回外层(但修改在 fiber 自身内部跨挂起点持久生效,v0.19 起)。需要共享请用通道传递,或改用全局变量。
等待(fiber)会驱动调度器直到全局静止。若程序里还有长时间睡眠的其他 fiber,等待会等它们一并结束才返回——设计如此,不是挂起。- fiber 句柄上限 65535 个。正常逐个
等待的程序会触发槽位回收,实际远够;只创建不等待会持续占用槽位。 - 不要把「循环内挂起」当成零成本并发。它独占 worker 线程;线程池容量(CPU 核数量级)内没问题,成百上千个这种任务会退化。海量并发场景请把挂起点移到循环外(语句级),走 fiber。
- 嵌套在回调函数内的异步块捕获外部变量仍是已知局限。
自动路由规则(重要)
编译器统计异步块内的挂起点(等待、收、发、选、时.睡、连、流.读、流.写)出现的位置:
| 挂起点位置 | 执行层 | 代价 |
|---|---|---|
| 仅在语句级(不在任何循环体里) | fiber 调度器 | 微秒级切换,几乎零占用 |
任意挂起点出现在循环体(当/遍历/循)内 | 线程池 | 运行期间独占一个 worker 线程 |
路由到线程池时编译器会给出诊断提示,例如:
编译提示: 异步块检测到 1 个循环内挂起点(等待/收/发/选/时.睡),已整体编译为线程池任务...为什么循环内挂起要走线程池:fiber 是无栈状态机,挂起恢复依赖编译期生成的状态跳转;循环体内的挂起恢复需要保留循环索引等局部状态,当前实现选择把这类代码放到真正的 OS 线程上执行——阻塞语义天然正确,但会占用一个 worker。
推荐模式:
// ① 语句级挂起 → fiber,最轻量
设 任务 = 异步 { 时.睡(100); 返 42 }
设 返回值 = 等待(任务) // 返回值 = 42
// ② 循环内挂起 → 自动路由线程池,语义正确,放心写
设 消费者 = 异步 {
当 真 {
设 货 = 通道.收() // 阻塞等待,不占 fiber
示("收到: " & 货)
}
}
// ③ 非阻塞试探:用 选 的超时 0,而不是直接 收
设 idx = 选([通道], 0) // 立即返回:有货返下标,没货返 -1通道
道(容量) 创建带缓冲的并发通道。v0.3 起支持阻塞收发(条件变量)。
基本用法
设 ch = 道(4) // 容量为 4 的通道
ch.发("hello") // 非阻塞发送(满则丢弃); 状态机中为阻塞发送
ch.发("world")
设 msg = ch.收() // 非阻塞接收(空则返回空); 状态机中为阻塞接收
示(msg)阻塞收发(状态机内)
在 异步 块内使用 .发() / .收() 时,编译器自动使用阻塞版本:
设 ch = 道(3)
设 consumer = 异步 {
设 sum = 0
设 i = 0
当 i < 3 {
sum = sum + ch.收() // 阻塞等待,不占 OS 线程
i = i + 1
}
示("Sum: " & sum.转字())
}
设 producer = 异步 {
ch.发(1)
ch.发(2)
ch.发(3)
}
等待(producer)
等待(consumer)多通道选择(选)
选(通道数组, 超时毫秒) 等待多个通道中第一个就绪的,返回通道索引。超时返回 -1。
设 fast = 道(1)
设 slow = 道(1)
设 sel = 异步 {
设 idx = 选([fast, slow], 5000)
若 idx == 0 { 示("fast 就绪") }
抑 idx == 1 { 示("slow 就绪") }
否 { 示("超时") }
}主线程泵送语义:主线程在
选等待期间会驱动用户态调度器(与收/发的无限等待一致),因此发送方是 fiber(无循环挂起的异步块)时数据也能正常到达。代价:fiber 内有长睡眠或线程池有在途任务时,选的返回可能晚于你给的超时毫秒数(等待会持续到 fiber 跑完一轮);超时到期仍未就绪才返回 -1。如果你需要严格守时的多路等待,把发送方移出 fiber(用含循环挂起的异步块走线程池),或把超时拆成多次短轮询。
线程安全
通道操作(发/收/选)已加互斥锁+条件变量保护,多线程并发安全。
HTTP 请求
URL 编码与解码
求 不对 URL 做任何编码:地址里带中文、空格等非 ASCII/保留字符时必须自己先编码,否则服务端会拒答——单参 求 对这种拒答只报到"成功但体长为 0",很容易误判。两个内置函数成对使用:
设 地址 = "https://example.com/search?q=" & URL编码("玄铁 语言 & 文档")
示(地址) // ...?q=%E7%8E%84%E9%93%81%20%E8%AF%AD%E8%A8%80%20%26%20%E6%96%87%E6%A1%A3
示(URL解码("%E7%8E%84%E9%93%81")) // 玄铁URL编码(串):按 RFC 3986,A-Z a-z 0-9 - _ . ~直通,其余按 UTF-8 字节转%XX(大写);空格 →%20(不是 form 的+)。URL解码(串):%XX(十六进制大小写均可)→ 原字节;+原样保留(RFC 3986,不做 form 的空格还原)。转义不完整、非十六进制、或解码结果不是合法 UTF-8,一律明确报错退出(提示带偏移)。字节到字符串(字节):字节流按 UTF-8 严格校验后转字符串(非法字节明确报错退出);配合字节(容量)建缓冲与.追加(字节值)逐字节写入使用。
简单 GET(单参形态)
求(url) 发起 HTTP GET 请求,返回 结果 对象,成功时 值 为最终响应体文本。支持 https://(TLS 走系统原生通道,零外部依赖:Windows 用 Schannel,macOS 用 SecureTransport;Linux 当前无 TLS 通道,https 请求会明确失败)。
行为要点:
- 自动跟随重定向:301/302/303/307/308 最多 5 跳,返回最终页内容(
Location支持绝对/协议相对/绝对路径/相对路径四种形态); - 默认 User-Agent 为完整 Chrome 标识(大量站点对空 UA 返回跳转壳或拒答);需要自定义时用双参形态的
头选项; - 超时:连接 10 秒、收发静默 30 秒(超时返回失败而非挂死);DNS 解析走
getaddrinfo,多 A 记录站点逐 IP 故障转移; - chunked 传输自动解码,二进制响应体安全定长处理。
设 resp = 求("http://api.example.com/data")
若 resp.成功 {
示("响应: " & resp.值)
} 否 {
示("请求失败: " & resp.错误)
}完整客户端(双参形态)
求(url, 选项) 发起自定义请求,选项字典全部可选:
| 键 | 说明 | 默认 |
|---|---|---|
方法 | "GET"/"POST"/"PUT"/"DELETE" 等 | "GET" |
头 | 字典,逐个写入请求头,如 {"Content-Type": "application/json"} | 无 |
体 | 请求体文本(非 GET 时常配) | 无 |
超时 | 收发超时毫秒数(连接超时暂不支持) | 不限 |
成功时 值 为字典:{"状态码": 整, "体": 字, "头": 字典};Transfer-Encoding: chunked 会自动解码。
设 r = 求("https://api.example.com/items", {
"方法": "POST",
"头": {"Content-Type": "application/json"},
"体": "{\"名\":\"玄铁\"}",
"超时": 8000
})
若 r.成功 {
设 v = r.值
示("状态码: " & v["状态码"])
示("响应体: " & v["体"])
示("Content-Type: " & v["头"]["Content-Type"])
}注意:单参形态(8.3.1)若遇 chunked 响应不解码(原样返回含块帧的文本);https 请求建议用双参形态拿完整状态码/响应头。
求 与 异步块的关系(易踩坑)
求 不是 fiber 挂起点——它内部是完整的阻塞 HTTP 事务,不会像 连/读/写 那样让出调度器。推论:
// ✅ 推荐:整个块只有 求,没有别的挂起点 → 自动路由线程池,语义正确
设 抓取 = 异步 {
设 r = 求("https://example.com")
若 r.成功 { 示("拿到 " & r.值.长度 & " 字节") }
}
等待(抓取)
// ⚠️ 避免:同一个块里既有 fiber 挂起点(等待/收/发/选/时.睡/连/读/写)
// 又有 求 —— 块会变 fiber,求 会卡住调度线程直到事务完成并发抓取的正确姿势:每个 URL 一个独立 异步 块(各自只含 求,自动去线程池),主线程逐个 等待 汇总:
设 们 = []
遍历 u 于 ["http://a.com/1", "http://a.com/2"] {
设 地址 = u // 循环内声明,逐迭代捕获
们.追加(异步 {
设 r = 求(地址)
若 r.成功 { 示(地址 & " -> " & r.值.长度 & " 字节") }
})
}
遍历 t 于 们 { 等待(t) }TCP 网络
玄铁的网络 I/O 有两种执行上下文,同一个 读/写/连 在两种上下文里的底层行为完全不同(语义一致,占用模型不同):
| 上下文 | 触发方式 | 底层行为 | 代价 |
|---|---|---|---|
| 阻塞上下文 | 主线程 / 普通函数 / 听 的回调 / 线程池异步任务 | 真正的阻塞 recv/send,独占当前线程 | 一个操作占一个线程 |
| fiber 上下文 | 异步 {} 块内(语句级,不在循环体里) | 非阻塞 + 就绪挂起,等待时不占任何线程 | 微秒级切换 |
「我这个写法会落到哪个上下文」由编译器按 8.1.4 的自动路由规则决定。本节所有示例都会标明落在哪一层。
服务端(监听)
听(端口, 回调) 启动 TCP 监听,立即返回(不等连接)。每来一个新连接,运行时为它在独立的 OS 线程里调用一次回调,回调参数就是该连接的 流。
默认只绑回环。 两参形态只监听 127.0.0.1(仅本机可连)——这是刻意的安全默认:服务在开发机上随手一跑就对整个局域网开门,是这类语言最常见的静默缺口。要对外提供服务,用三参形态显式写地址:
听(8000, 函(流) { /* ... */ }, "0.0.0.0") // 对外(所有网卡)
听(8000, 函(流) { /* ... */ }, "192.168.1.10") // 只绑某块网卡地址不可用(端口被占、地址非法、第三个参数不是字符串)时 听 返回失败结果,错误消息里带具体地址与端口,不会静默回退成对外监听;参数个数不是 2 或 3 时编译期直接报错。
听(8000, 函(流) {
流.写("欢迎!\n")
循 {
设 数据 = 流.读()
若 数据 == 空 { 断 } // 对端关闭 → 读返回 空 → 退出循环
流.写("回显: " & 数据)
}
流.关()
})
输("按回车停止...") // 关键:主线程必须有事可做,否则程序直接退出关键点 1:回调里是阻塞上下文,可以放心用阻塞写法。 因为每个连接独占一个真实线程,循 环里阻塞 读 天经地义——它只卡住「这一个连接」的线程,不影响其他连接,也不影响 fiber 调度器。
关键点 2:听 不挡退出。 听 启动后立刻返回,如果主线程一路跑到程序末尾,进程退出、监听随之消失。所以服务端末尾通常用 输(...) 或一个永不结束的循环把主线程留住:
听(8000, 函(流) { /* ... */ })
示("服务已启动")
循 { 时.睡(1000) } // 或者:输("按回车停止...")关键点 3:多个回调并发跑,共享数据要走通道或全局。 回调之间是真正的多线程并发。例如统计在线人数:
设 在线 = 0 // 全局变量,所有回调线程共享
听(8000, 函(流) {
在线 = 在线 + 1 // 简单计数可以;复杂共享状态请用 通道 汇总(见 8.2)
循 {
设 数据 = 流.读()
若 数据 == 空 { 断 }
流.写("当前在线: " & 在线)
}
在线 = 在线 - 1
流.关()
})关键点 4:目前没有「停止监听」原语。 听 一旦启动就到进程结束,这是已知边界;需要可关停的服务请把「是否继续」判断放在回调内部逻辑里。
关键点 5(必知坑):TCP 没有消息边界,读 不等于「读一条消息」。 阻塞路径下每次 读 最多返回 4096 字节,而且对端一次 写 的数据可能被拆成多次 读 到达(也可能多次 写 被合并成一次 读)。不要假设 读 到的就是一整条消息。 如果协议需要完整消息,自己加分隔符或长度头,循环拼:
// 按行协议:循环读直到凑出一行(\n 结尾)
函 读一行(流) {
设 缓冲 = ""
循 {
设 块 = 流.读()
若 块 == 空 { 返 空 } // 对端关闭
缓冲 = 缓冲 & 块
若 缓冲.包含?("\n") {
设 行集 = 缓冲.分割("\n")
返 行集[0] // 取第一行(分割语义见参考手册 6.4 节)
}
}
}客户端(连接)
连(地址) 连接到 TCP 服务端,返回 结果——必须判 成功 再取用,地址格式 "主机:端口":
设 联 = 连("127.0.0.1:8000")
若 联.成功 {
设 流 = 联.值
流.写("hello")
设 回 = 流.读()
若 回 != 空 { 示("服务端说: " & 回) }
流.关()
} 否 {
示("连接失败: " & 联.错误)
}关键点 1:读 返回 空 只有一个含义——对端关了(或已关)。 它不表示「暂时没数据」:阻塞路径下没数据会一直等到有数据或对端关闭;fiber 路径下没数据会挂起等就绪。所以「读到 空」永远按连接结束处理:
设 数据 = 流.读()
若 数据 == 空 {
// 对端已关闭,别再 写/读,直接收尾
流.关()
返 空
}关键点 2:写 返回 真/假,且保证「写完全部」。 写大数据时底层会自动分多次发送(部分发送的内部续传),返回 真 代表全部发出;返回 假 代表连接已坏。不需要自己处理「只发出去一半」的情况。
关键点 3:关 通常可以不写。 socket 由 ARC 内存管理,引用归零自动关闭;长寿命循环(如服务端回调)里建议显式 流.关() 及时释放。
Socket 方法
| 方法 | 说明 |
|---|---|
.读() | 读取数据;有数据返回字符串,对端关闭返回 空。阻塞路径每次最多 4096 字节 |
.读(上限) | 同上,但最多读 上限 字节。上限仅在 fiber 上下文生效(见 8.4.4 细则 1) |
.写(内容) | 写入字符串(自动分次发完),成功 真 / 失败 假 |
.发(内容) | 同 .写() |
.关() | 关闭连接 |
闭包回调注意:听 的回调经 xt_closure_call1 按 env 感知约定调用——捕获外层变量的闭包可正常使用。
异步块内的网络 I/O(fiber 感知,v0.19+)
一分钟原理
异步 {} 块(语句级、不含循环内挂起点)会被编译成 fiber(无栈状态机),跑在 fiber 调度器上。块内的 连、流.读()、流.写() 是挂起点:socket 自动转为非阻塞;操作不能立即完成时,fiber 连人带状态挂进「socket 就绪等待表」,调度线程立刻去驱动别的 fiber;数据到达时 fiber 被精确唤醒,从挂起点继续——等待期间不占用任何线程。
收益:成百上千个并发连接不再需要成百上千个线程,一个调度线程就能撑起全部「大部分时间在等数据」的连接。
标准客户端模板
设 客 = 异步 {
设 r = 连("127.0.0.1:8000") // 挂起点:连接建立前不占线程
若 非 r.成功 { 示("连接失败") ; 返 空 }
设 流 = r.值
流.写("你好") // 挂起点:写不完就挂起等可写,自动续传
设 回 = 流.读() // 挂起点:没数据就挂起等就绪
示("收到: " & 回)
流.关()
}
等待(客) // 主线程在这里驱动调度器直到 fiber 完成与阻塞版逐字对比你会发现写法完全一样——差别只在底层不占线程。这就是 fiber 感知 I/O 的设计目标:同一套代码,放进 异步 {} 就自动获得高并发能力。
多连接扇出模板(fiber 的主战场)
由于玄铁的 fiber 切换是微秒级,几百个并发连接的正确写法是「每个连接一个 fiber」,而不是开几百个线程:
设 们 = []
设 i = 0
当 i < 100 {
设 j = i // 循环内声明,逐迭代捕获(关键!写成直接用 i 会全拿到终值)
们.追加(异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 }
设 流 = r.值
流.写("我是 " & j)
设 回 = 流.读()
若 回 != 空 { 示("#{j} 号收到: " & 回) }
流.关()
})
i = i + 1
}
遍历 t 于 们 { 等待(t) } // 等全部 100 个 fiber 完成注意 设 j = i 这一行:循环变量 i 只有一个存储,直接捕获会让所有 fiber 读到循环结束后的终值;设 j = i 在循环体内声明新变量,捕获按创建时刻快照(逐迭代语义),每个 fiber 拿到自己那轮的值。
写法纪律(反直觉点,必读)
纪律一:挂起语句独立成句,不要把挂起点包进 若 块。 fiber 是无栈状态机,含挂起点的 若 会被拆成多个「状态片」,片与片之间块内语句会重复执行(既有语义):
// ❌ 反例:若 块里有挂起点,块里的 示 可能被执行多次
异步 {
若 r.成功 {
示("连上了") // 状态片重放时这行会再打印一次!
设 回 = r.值.读()
示("收到: " & 回)
}
}
// ✅ 正例:挂起点全部独立成句,若 只包纯判断
异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 } // 若 块里没有挂起点,安全
设 流 = r.值
设 回 = 流.读()
若 回 != 空 { 示("收到: " & 回) }
}纪律二:需要「读循环」的连接处理,请交给线程池而不是 fiber。 挂起点出现在循环体里时,整个异步块自动路由到线程池(8.1.4)——这不是降级,是设计:循环内挂起需要保留循环现场,fiber 做不到,线程天然正确。所以服务端长连接收包循环这样写就对:
// 循环里有 读 → 整块自动去线程池,阻塞语义,完全正确
设 长连处理 = 异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 }
设 流 = r.值
当 真 {
设 数据 = 流.读() // 线程池里阻塞读,独占一个 worker
若 数据 == 空 { 断 }
示("收到: " & 数据)
}
}而**「一次性问答」式连接**(连上 → 写 → 读 → 关)全部语句级,走 fiber,最省:
设 问答 = 异步 {
设 r = 连("127.0.0.1:8000")
若 非 r.成功 { 返 空 }
设 流 = r.值
流.写("问")
设 答 = 流.读() // 全在语句级 → fiber,挂起不占线程
示("答: " & 答)
流.关()
}细则与边界(逐条都是实测行为)
读(上限)只在 fiber 内生效。流.读(16)在 fiber 里最多读 16 字节;在阻塞上下文(主线程/听回调)该参数被忽略,固定每次最多 4096 字节。结论:无论哪个上下文,都不要依赖单次读到的字节数,要完整消息请用 8.4.1 关键点 5 的循环拼法。- fiber 内
.读()/.写()保留给 socket 流(如同.收/.发保留给通道)。对字典/实例的同名方法在 fiber 内调用会显式报错,不会静默出错。文件.读/写是模块函数,完全不受影响。 - fiber 内的变量跨挂起点持久:
设的局部变量、对捕获变量的修改,在挂起恢复后都保持(v0.19 起;更早版本会丢成空/旧值——如果你在老版本上写 fiber 网络代码,请升级)。 - 同一个 socket 不要跨上下文混用。 socket 一旦被 fiber 碰过就会置为非阻塞;之后再拿到阻塞上下文用虽然做了兼容(内部重试,行为不变),但这是为容错准备的降级,不是设计用法。一个流,从生到死待在同一个上下文里。
读没有超时参数。 需要「超时放弃」时,用对端关闭触发的模式:看门狗 fiber 到时后让对端(或本端持有方)关流——挂起的读会被唤醒并返回空,按连接结束统一处理:
设 看门 = 异步 {
时.睡(3000)
若 服务端流盒子[0] != 空 { 服务端流盒子[0].关() } // 对端关闭 → 读方唤醒得 空
}- TLS(HTTPS)流暂不支持 fiber 感知 I/O;
求走自己的阻塞路径,不受影响。 - fiber 句柄上限 65535(见 8.1.3 第 5 条);socket 就绪等待表上限 1024 个并发挂起 I/O,超出显式报错退出(不静默丢)。
HTTP 服务端(铁铺包 HTTP)
HTTP.服务(端口, 处理函) 以纯玄铁实现启动 HTTP/1.1 服务(基于 听)。处理函收请求字典 {"方法","路径","查询","头","体"},返回响应字典 {"状态码","头","体"}(均可缺省,默认 200/空体)。每连接一请求一响应(Connection: close)。
引 "HTTP" 予 HTTP
HTTP.服务(8080, 函(请求) {
若 请求["路径"] == "/api" {
返 {"状态码": 200, "头": {"Content-Type": "application/json"}, "体": "{\"ok\":真}"}
}
返 {"状态码": 404, "体": "没有 " & 请求["路径"]}
})
示("服务已启动 :8080")
输("按回车停止...")关键点与写法:
- 处理函跑在连接线程里(阻塞上下文)——基于
听,每连接一个独立线程。处理函里可以用文件.读等阻塞操作,也可以访问外层变量(闭包捕获正常)。 - 读 POST 体:
请求["体"]已按Content-Length收齐;请求["方法"]区分动作。回显接口示例:
HTTP.服务(8080, 函(请求) {
若 请求["方法"] == "POST" 且 请求["路径"] == "/echo" {
返 {"状态码": 200, "体": "你发了: " & 请求["体"]}
}
返 {"状态码": 404, "体": "没有 " & 请求["路径"]}
})- 路径与查询串分离:
/a?x=1进来,请求["路径"]是/a,请求["查询"]是x=1,解析查询参数用分割("&")再按=拆。 - 不支持 keep-alive:每个连接处理一个请求就关闭(HTTP/1.1
Connection: close语义)。浏览器/fetch 会自然兼容(后续请求自动开新连接)。 - 响应头自动补:
Content-Length按体.字节数自动写入,Connection: close自动带上;你只需给业务头。 - 关停:目前没有「停止监听」原语,服务随进程生命周期;开发期直接结束进程即可。
即时通讯 (IM) 完整示例
项目 examples/im/ 包含完整的服务端+客户端示例。
// 服务端
听(8000, 函(流) {
示("新连接")
流.写("系统: 欢迎进入聊天室!")
循 {
设 数据 = 流.读()
若 数据 == 空 { 断 }
示("收到: " & 数据)
}
})
示("服务端已就绪")
输("按回车停止...")线程安全速查
| 组件 | 线程安全性 | 说明 |
|---|---|---|
通道 (道) | 安全 ✓ | xt_chan_mutex_t + 条件变量全程持锁 |
| 通道(阻塞收发) | 安全 ✓ | 条件变量等待,不占 OS 线程 |
选 (选) | 安全 ✓ | 轮询+条件变量,超时安全 |
异步 (异步) | 安全 ✓ | 状态机 poll 函数由调度器单线程调用 |
并行 (并行) | 安全 ✓ | cloneEnv 隔离 |
| Socket I/O | 安全 ✓ | 每个 socket 独立,无共享状态 |
| ARC | 安全 ✓ | atomic_fetch_add/sub |
| 调度器 | 注意 ⚠ | 线程池回调唤醒 fiber,无锁竞态(当前单线程未触发) |
| 全局变量 | 不安全 ✗ | 用户代码需自行加锁 |
| 弱引用 | 安全 ✓ | g_weak_mutex 保护 |
命令速查
| 关键字 | 说明 |
|---|---|
异步 { ... } | 异步执行代码块(自动检测状态机) |
等待(task) | 等待异步任务完成 |
并行 { {a}, {b} } | 并行执行多个代码块 |
道(n) | 创建容量为 n 的通道 |
.发(v) | 通道/流 发送(状态机中阻塞) |
.收() | 通道接收(状态机中阻塞) |
选(arr, ms) | 多通道选择,返回索引,超时返回 -1 |
求(url) | HTTP GET(返回体文本) |
求(url, 选项) | 完整 HTTP 客户端(方法/头/体/超时,回 状态码/体/头) |
连(addr) | TCP 连接(fiber 内非阻塞挂起) |
听(port, cb, 绑定地址?) | TCP 监听(回调支持捕获闭包);缺省只绑回环 127.0.0.1,对外须显式写 "0.0.0.0" |
HTTP.服务(port, fn) | HTTP 服务端(铁铺包 HTTP) |
.读() / .读(上限) | Socket 读取(上限仅 fiber 内生效;fiber 内为就绪挂起) |
.写(s) | Socket 写入(fiber 内为可写挂起,自动续传) |
.关() | Socket 关闭 |