px普贤教程PuXian
普贤教程 › 进阶教程

并发与 Actor

并发只在编译轨可用。解释器是单线程子集,遇到 spawn / chan 直接报错——这不是缺陷,是取舍:解释器只负责"跑通逻辑",并发交给编译产物。

spawn 与 chan

px
def work(ch, n):
    var s = 0
    var i = 0
    while i < n:
        s += i
        i += 1
    ch.send (s)

def main():
    var ch = chan [int](4)
    spawn work(ch, 100)
    spawn work(ch, 200)
    print(str(ch.recv()))
    print(str(ch.recv()))
sh
px build conc.px && ./build/conc
text
4950
19900
原语作用
spawn f(args)启动一个并发执行单元
chan [T](n)带类型与缓冲容量的通道
ch.send (v)发送(缓冲满则阻塞)
ch.recv ()接收(无数据则阻塞)
注意空格ch.send (v) 与 ch.recv () 的括号前有一个空格。这是语言当前的写法约定,别省。

用 chan 收集结果

px
def fetch_all(urls):
    var ch = chan [dict](len(urls))
    for u in urls:
        spawn fetch_one(u, ch)
    var out = []
    var i = 0
    while i < len(urls):
        out.append(ch.recv())
        i += 1
    return out

def fetch_one(url, ch):
    var r = http_request(url, "GET", null)
    ch.send ({"url": url, "status": r["status"]})

要点:开启多少个并发单元,就 recv 多少次。少收会泄漏,多收会死等。

worker 池

任务数远大于并发上限时,用固定数量的 worker 从同一个通道取任务:

px
def worker(id, jobs, results):
    while true:
        var job = jobs.recv ()
        if job == null:
            results.send ({"id": id, "done": true})
            return
        var v = job * 2
        results.send ({"id": id, "value": v})

def main():
    var jobs = chan [int](16)
    var results = chan [dict](16)
    spawn worker(1, jobs, results)
    spawn worker(2, jobs, results)

    for i in range(1, 7):
        jobs.send (i)
    jobs.send (null)      # 每个 worker 一个结束哨兵
    jobs.send (null)

    var got = 0
    while got < 8:
        var r = results.recv ()
        got += 1
    print("done")

actor:把状态关进一个邮箱

共享状态最容易出事。actor 的思路是:状态只由持有它的那个执行单元修改,外部通过消息请求它做事,天然免锁。

sh
pxpkg add actor@0.2.0 && pxpkg install
px
import actor

def main():
    var a = ac_new({
        "inc": fn(state, body):
            state["n"] = state["n"] + body
            return state["n"],
        "get": fn(state, body): state["n"]
    }, {"n": 0})

    ac_call(a, "inc", 5)
    ac_call(a, "inc", 3)
    print(str(ac_call(a, "get", null).ok()))
    ac_stop(a)
text
8
处理函数的两种写法只有一条表达式时用 fn(state, body): 表达式;要改 state 就必须写成多行块,末尾 return,并且这个 fn 在字典里后面要跟一个逗号。fn(state, body): state["n"] = ... 这种把赋值当表达式写会直接语法错误。
函数行为
ac_new(handlers, init)创建 actor,handlers 是「消息名 → fn(state, body)」
ac_call(a, name, body)发消息并等应答,返回 Ok(result);未注册的消息返回 Err
ac_send(a, name, body)只发不等
ac_stop(a)优雅停止,已排队消息会先处理完

并发编程的四条纪律

  1. 不要跨执行单元改同一个列表/字典。要共享就通过通道传消息。
  2. 每个 spawn 都要有人等。通道没人收,发送方会永久阻塞。
  3. 退出要有哨兵。用 null 或特定消息告诉 worker 结束,别靠"自然结束"猜。
  4. 并发单元里的错误要回传。子单元崩了主单元不知道,现象是"卡住不动"——把 err 信息也 send 回来。

什么时候不要并发

场景建议
CPU 密集且能拆块并发有效(编译轨)
IO 等待(HTTP、数据库)并发有效
需要强一致的计数用数据库原子操作,不要内存计数
逻辑简单、QPS 低串行更省心,别引入并发
解释器不能跑并发px run 遇到 spawn / chan 会报错。开发期要么只测串行逻辑,要么始终用 px build + 跑产物来验证。