px普贤教程PuXian
普贤教程 › 生态包

并发与常见模式

限流、重试、缓存、定时、并发原语。这一组包解决的是「服务跑久了才会出问题」的那类事——它们的共同点是状态显式传递,没有隐藏全局变量。

限流:令牌桶

px
import rate

var bucket = rt_new(10, 2.0)          # 容量 10,每秒补 2 个
print(rt_allow(bucket, 1))
# [{cap: 10, last: 1, rate: 2.0, tokens: 9}, true]
rt_allow 返回的是「新桶 + 布尔」的二元组不是 bool。因为它改写桶状态(扣掉令牌),返回值是新状态——必须接住: var r = rt_allow(bucket, 1) → bucket = r[0] → var ok = r[1]。 只写 if rt_allow(bucket, 1): 会永远为真(非空 list 为真)。
px
def allow(bucket, n):
    var r = rt_allow(bucket, n)
    return [r[0], r[1]]

var b = rt_new(100, 10.0)
var ok = false
var pair = allow(b, 1)
b = pair[0]
ok = pair[1]

重试:带退避

px
import retry

var cfg = {"attempts": 5, "base_ms": 100, "strategy": "exp", "max_ms": 5000}
print(retry_backoff_series(5, 100, "exp"))     # 100/200/400/800/1600(含抖动另算)
print(retry_max_delay(5, 100, "exp"))

var r = retry_call(fetch_fn, null, cfg)        # 可重试错误才重,不可重试立刻返回
只对「可重试」的错误重试retry_is_retryable(err) 判断:连接超时、5xx 可重试;400、参数错误、签名失败不可重试。把不可重试的错误也重试,只会把一次失败放大成五次。

缓存:TTL + LRU

px
import cache

var c = ca_new(100)                    # 最多 100 条
ca_set(c, "k1", "v1", 0)               # ttl 0 = 不过期
print(ca_get(c, "k1").ok())            # v1
print(ca_has(c, "k1", 5000))           # 带 now 判断是否过期

ca_set(c, "k2", "v2", 1, 1000)         # 1 秒过期
print(ca_has(c, "k2", 5000))           # false

时间是显式传入的(默认 now_sec()),所以单测里可以把「现在」设成任意值,不用 sleep。过期是惰性清理:取的时候才判。

定时任务

px
import sched

var s = sched_new()
sched_add(s, "cleanup", 60000, cleanup_fn)     # 每 60 秒
print(sched_counts(s))
sched_run(s, 3600000)                          # 推进 1 小时(虚拟时钟)

sched 是「时间片轮询 + 显式时钟」,不是 systemd timer:它不会自己起线程,你在主循环里推它。真正的常驻定时任务用 systemd timer 或 cron,sched 适合模拟与单测。

并发原语

包作用注意
semaphore计数限流sp_acquire 拿不到会等;有 sp_try_acquire 不阻塞版本
workerpool固定 worker 池跑任务wp_run 提交、wp_close 收尾
concurrent_map加锁的 dict只在多协程共享时用;单协程纯属浪费
actor消息循环 + 状态状态只在一个线程里改,天然免锁
counter计数(Counter 心智)ct_most_common 排行
dist随机分布采样压测造流量、模拟抖动

actor:把状态关进一个线程

px
import actor

# 处理函数签名:(state, body) → 新 state
def on_inc(state, body):
    var n = state["n"] + 1
    return {"n": n}

def main():
    var a = ac_new({"inc": on_inc}, {"n": 0})
    print(ac_send(a, "inc", null))     # Ok(true)
    print(ac_status(a))                # running
    ac_stop(a)

实测输出就是 Ok(true) 与 running。actor 在自己的线程里串行处理消息,所以 state 不需要加锁——这是它比 concurrent_map 更省心的原因。

处理函数里改 state 要写成多行块单表达式赋值当表达式用会语法错;写成 var n = … + return {…} 最稳。

counter:一次算完频次

px
import counter
var c = ct_count(["a", "b", "b", "c", "c", "c"])
print(c)                    # {a: 1, b: 2, c: 3}
print(ct_most_common(c))    # [[c, 3], [b, 2], [a, 1]]

函数式组合

px
import functools
import itertools

print(it_combinations([1, 2, 3], 2))   # [[1, 2], [1, 3], [2, 3]]
print(it_permutations([1, 2, 3], 2))   # [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
print(it_chain([1, 2], [3, 4]))        # [1, 2, 3, 4]
print(it_unique([1, 1, 2]))            # [1, 2]

functools 提供 fn_partial / fn_compose / fn_memo / fn_pipe。fn_memo 的记忆化对纯函数有用,对依赖时间/随机的函数是灾难——缓存的是第一次的结果。

这一组包共同的坑

  • 状态显式传:没有隐藏全局。多请求共享一个「桶」时,桶得存在模块级变量或数据库里。
  • 「返回新状态」的函数必须接住:rt_allow、xsx_add、pd_add_page 都是这个模式。
  • 多进程不共享内存:Ma 的 px 脚本是「每请求一个进程」,进程内限流只能防单进程突发——真正的全局限流要放 Redis(INCR + EXPIRE)。