Help us shape the future of Go! Take the 2026 Go Developer Survey

syncx

package
v0.23.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package syncx 并发编排原语(stdlib-only):在飞闸门(按键分档位 / 全局单一 配额,取用分排队与即拒两档)+ 按序扇出收集。 收编自消费方手写形态——LLM 按模型名限在飞(429 风暴治理)、「这个进程总共 最多 N 个在飞」的手搓信号量,与「按 index 写结果免锁」的并行扇出样板。

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FanIn

func FanIn[T any](n int, fn func(i int) T) []T

FanIn 并行执行 fn(0..n-1),各 goroutine 只写自己的 results[i] 槽位(免锁), 全部完成后按 index 序返回。fn 经 async.GoSafe 收编(AGENTS.md「禁裸 go func」, v0.2.1 前为裸 go、fn panic 会打死宿主进程):单个 fn panic 被恢复并走全局 panic handler 落日志,该槽位保持零值、其余槽位与收口不受影响——消费方按 「空槽降级」处理(零值歧义由调用方区分)。n<=0 返回空切片。

Types

type Gate added in v0.19.0

type Gate struct {
	// contains filtered or unexported fields
}

Gate 是**全局在飞上限闸门**:整个实例最多 cap 个在飞,排队者等 ctx 取消 即返回 ctx.Err()。与 KeyedGate 的区别:Gate 是单一全局配额(不分 key), KeyedGate 按 key 分档位(按模型名/租户名分别限流)。

两种取用语义并存、命名区分:

  • Acquire(ctx):满额时**排队**等名额;
  • TryAcquire():满额时**立即拒**(不等待、不占名额)——重活的并发上限 用满就该立刻拒,让调用方回 429 / 503,而不是把请求堆在队列里。

为什么需要这一档:现有原语都不覆盖"这个进程总共最多 N 个在飞"这类闸门。

  • KeyedGate 按 key 隔离,用固定 key 硬凑可行,但那是把"多档位闸门"原语 当"单档位闸门"用,语义与命名都不对;
  • gopool.Pool 是弹性 worker 池,**MaxIdleWorkers 是常驻 worker 上限, 不是并发上限**——任务队列满时回退为独立 goroutine 直跑,任务永不因 并发数被拒(实测 MaxIdleWorkers=2 时实际并发仍是 10);它与"硬配额" 的语义正好相反;
  • ratelimit.KeyedLimiter 是速率型(令牌桶),不是并发闸门。

适用场景:保护下游 DB / 控制子进程数 / 限制外部 API 并发。

并发安全;release 幂等(可重复调用,只归还一次)。

func NewGate added in v0.19.0

func NewGate(cap int) *Gate

NewGate 构造;cap<=0 = 不限(Acquire 直通零开销)。

func (*Gate) Acquire added in v0.19.0

func (g *Gate) Acquire(ctx context.Context) (func(), error)

Acquire 取一个在飞名额;ctx 取消(排队等待中)返回 ctx.Err()。 返回的 release 归还名额,幂等可重复调用。

ctx 为 nil 时按 context.Background() 处理(永不取消)。

func (*Gate) TryAcquire added in v0.20.0

func (g *Gate) TryAcquire() (release func(), ok bool)

TryAcquire 尝试取一个在飞名额,**不等待**:名额已满时**立即**返回 ok=false。

与 Acquire 的区别是**排队 vs 即拒**,两者并存、命名区分:

  • Acquire(ctx):满额时**排队**等名额,等 ctx 取消才返回 ctx.Err();
  • TryAcquire():满额时**立刻拒**,一个名都不占。

什么时候要即拒:Acquire 的排队语义会把「并发被占满」换成「请求堆在队列里」—— 那是另一种 DoS 形态。重活(图片全量解码、ffmpeg 子进程、模型推理)的并发 上限用满就该立刻拒,让调用方回 429 / 503,而不是让队列越堆越长、把内存 或子进程数推到别处爆掉。

ok=false 只有一个含义:**名额已满**(非阻塞取名额没有别的失败原因), 调用方可据此返回 429 / 503,而不是泛化成 500。

返回的 release 归还名额,幂等可重复调用(与 Acquire 一致)。 cap<=0(不限)时恒返回 ok=true——不限额的闸门没有「满」这回事。

用法:

release, ok := gate.TryAcquire()
if !ok {
    return nil, ErrTooManyInFlight // 429 / 503
}
defer release()

type KeyedGate

type KeyedGate struct {
	// contains filtered or unexported fields
}

KeyedGate 按键在飞上限闸门(并发安全;channel 令牌先到先得;键集由调用方 保证有界——如按模型名/租户名,键量级恒小不回收)。

按 key 分档位("每个模型最多 N 个在飞")。要的是**单一全局配额** ("整个进程总共最多 N 个在飞")用 Gate——两者语义不同,不要用固定 key 硬凑。

func NewKeyedGate

func NewKeyedGate(cap int) *KeyedGate

NewKeyedGate 构造;cap<=0 = 不限(Acquire 直通零开销)。

func (*KeyedGate) Acquire

func (g *KeyedGate) Acquire(ctx context.Context, key string) (func(), error)

Acquire 取一个键下在飞名额;ctx 取消(排队等待中)返回 ctx.Err()。 返回的 release 归还名额,幂等可重复调用。

幂等标记用 atomic.Bool + CompareAndSwap(同包 Gate 的做法):裸 bool 在 两个 goroutine 并发调用同一个 release 时是 data race,且两者都可能读到 "未归还" 而各执行一次 <-ch —— 名额被归还两次,该 key 的在飞数突破 cap 上限(cap=1 时可同时有 2 个在飞)。按 key 分档位的闸门正是最容易被并发 释放的场景(超时清理路径与正常返回路径同时释放同一个 key)。

ctx 为 nil 时按 context.Background() 处理(永不取消),与 Gate 一致。

func (*KeyedGate) TryAcquire added in v0.21.0

func (g *KeyedGate) TryAcquire(key string) (func(), bool)

TryAcquire 尝试取一个键下在飞名额,**不等待**:该 key 档位已满时**立即**返回 ok=false(不排队、不占名额)。

与 Acquire 的区别是**排队 vs 即拒**,两者并存、命名区分(同包 Gate 的同款分档):

  • Acquire(ctx, key):该 key 满额时**排队**等名额,等 ctx 取消才返回 ctx.Err();
  • TryAcquire(key):该 key 满额时**立刻拒**,一个名都不占。

什么时候要即拒:防重入场景的常见形态——同一个 key 正在处理中就**立刻拒绝** (回 409/429),而不是排队等它做完。此前消费方只能手搓 map[K]*sync.Mutex + TryLock + sync.Once 兜底幂等归还:每仓各写一遍,once 包装又容易漏(漏了就是 双重解锁 panic)。KeyedGate.Acquire 是排队语义,套不上——要「忙即拒」就得上 调用方自己再套一层 goroutine + select,绕开了原语的语义。

ok=false 只有一个含义:**该 key 档位已满**(非阻塞取名额没有别的失败原因), 调用方可据此返回 409 / 429,而不是泛化成 500。

返回的 release 归还名额,幂等可重复调用(与 Acquire 一致,atomic CAS 实现)。 cap<=0(不限)时恒返回 ok=true——不限额的闸门没有「满」这回事。

用法:

release, ok := gate.TryAcquire("account:42:sync")
if !ok {
    return errBusy // 409 / 429
}
defer release()

type Slot added in v0.5.0

type Slot[T any] struct {
	Val T
	// Err 正常错误,或 panic 收编的 *async.PanicError。
	Err error
	// Panicked 历史兼容字段:panic 原始 payload。新代码改用
	// errors.As(&async.PanicError) 读 Err。
	//
	// Deprecated: v0.7.0 起由 Slot.Err 承载。
	Panicked any
}

Slot 是 FanInErr 的逐槽结果:Val/Err 为 fn 的正常返回;fn panic 时 Err 承载 *async.PanicError(errors.As 可辨、携带栈,#12)且 Val 为零值。 Panicked 保留原始 payload(兼容字段,Deprecated——改用 errors.As(Slot.Err))。

func FanInErr added in v0.5.0

func FanInErr[T any](n int, fn func(i int) (T, error)) []Slot[T]

FanInErr 逐槽错误变体:并行执行 fn(0..n-1)(fn 可返回 error),全部完成后 按 index 序返回逐槽结果。fn 正常返回 → Val/Err 如实记录;fn panic → 该槽 Err=*async.PanicError(v0.7.0 起统一收编,errors.As 可辨、携带栈;Panicked 同写原始 payload,已 Deprecated)、Val 零值,并经 ReportPanic 上报——不再 静默(FanIn 契约保留不动,供无 error 形态的轻量路径)。n<=0 返回空切片。

func FanInLimit added in v0.12.0

func FanInLimit[T any](n, limit int, fn func(i int) (T, error)) []Slot[T]

FanInLimit 是 FanInErr 的受限并发变体(#23):n 槽结果语义不变,内部固定 w = min(limit, n) 个 worker 经原子计数器拉取 index 执行——goroutine O(w), 供「n 可达数十以上、fn 是有成本 IO(DB/远端查询)」的 fan-out 限并发, 防下游过载(收编消费方手写的 channel 信号量样板)。其余语义与 FanInErr 一致:按 index 序返回逐槽结果、panic 进该槽 Err(*async.PanicError)并 ReportPanic 上报。limit<=0 = 不设上限(等价 FanInErr 行为)。

func FanInLimitCtx added in v0.14.0

func FanInLimitCtx[T any](ctx context.Context, n, limit int, fn func(ctx context.Context, i int) (T, error)) []Slot[T]

FanInLimitCtx 是 FanInLimit 的 ctx 感知变体(#27):fn 携带 ctx 供单项内部 使用;引擎级取消语义——worker 每轮拉取 index 前先查 ctx.Err()(worker 内 缓存,取消后零额外锁开销),命中即不再启动新工作(fn 一次都不调),当前及 剩余槽一次遍历快速排空为该错误(context.Canceled);已在飞 item 跑完、结果 照常落槽。其余语义与 FanInLimit 一致(panic 进槽并上报、limit<=0 = 不设上限、 n<=0 = nil;ctx 为 nil 时按 context.Background() 处理)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL