跳转到内容

并发下的段错误、竞态与 ImageRef 生命周期

复制本页

如果服务只在并发负载下出现 SIGSEGV、异常尺寸或通道数,先检查 ImageRef 是否跨 goroutine 共享,以及作为算子输入的源图是否被提前关闭。单请求测试通常覆盖不到这两类生命周期问题。

  • 并发下偶发段错误(SIGSEGV),非并发时稳定。
  • 跨 goroutine 共享 ImageRef 时,读到怪异的返回值(一个已知案例是 Bands() 返回 5376256,而真实通道数只有几个)。
  • 单连接压测正常,并发后开始崩溃。

关键区别:libvips 与 govips 的线程模型不同

Section titled “关键区别:libvips 与 govips 的线程模型不同”

libvips 自身在一个调用内是线程安全的——每次操作内部会开线程池,共享的 vips_concurrency 控制线程数。但 govips 的 ImageRef 不是线程安全的:它是持有 VipsImage* 的 cgo 句柄,一个 ImageRef 同时被多个 goroutine 操作会产生数据竞争、悬垂指针和段错误。

别把 libvips 的并发能力误解为可以共享 ImageRef。

不要在请求之间复用同一个 ImageRef。每次请求各自 LoadImageFromFile / LoadImageFromBuffer / LoadThumbnailFromFile,用完各自 Close()。

func dimensions(buf []byte) (int, int, error) {
importParams := vips.NewImportParams()
image, err := vips.LoadImageFromBuffer(buf, importParams)
if err != nil {
return 0, 0, err
}
defer image.Close()
return image.Width(), image.Height(), nil
}

并把并发调用交给 Go 的 goroutine 调度;不要在函数外缓存并共享那个 ImageRef。

2. 不要过早关闭被传入其他算子的源 ImageRef

Section titled “2. 不要过早关闭被传入其他算子的源 ImageRef”

当 A 传入需要 B 作为输入的算子时(例如合成、叠加),源 ImageRef B 的 C 内存被算子引用。此时过早 Close() B 会让后续 GC 阶段访问悬垂指针。确保 B 的生命周期覆盖整个算子调用,必要时用 runtime.KeepAlive 固定。

3. 用并发压测验证修复,而不是只看单请求

Section titled “3. 用并发压测验证修复,而不是只看单请求”

考虑链路的边角情况:共享切片、把 ImageRef 塞进全局缓存、或把 slice header 直接当 backing array 传给 C。这类问题只在并发下暴露。

  • 不同 goroutine 各自加载各自的图,这是推荐用法,不属于竞态。
  • 崩溃由版本缺陷引起:govips 在旧版本中合入过内存/竞态修复(#493),并正在重构锁模型(#524/#525)。先升级到含修复的版本再排查自身代码。
  • 本页针对 govips v2.19.0;其他 binding 的并发保证和对象所有权模型必须分别核对。

libvips 内部线程池与 binding 对象的并发安全是两个层次;不要把本页对 ImageRef 的结论直接套用到其他 binding。