首页
/ moby 仓库中的 gofrs/flock:使用 flock 库在 Go 中实现线程安全的文件锁

moby 仓库中的 gofrs/flock:使用 flock 库在 Go 中实现线程安全的文件锁

2026-09-07 23:13:05作者:劳婵绚Shirley

本篇技术指南以 Moby 项目(Docker 的容器生态系统协作平台)vendor 目录中集成的 gofrs/flock 库 的官方 README 为主体,结合其完整源码实现展开讲解。读者读完将能理解 flock 提供的阻塞/非阻塞、独占/共享文件锁 API 的用法,掌握线程安全设计、跨平台实现原理,并看到该库在镜像仓库索引读写、容器网络管理等真实场景下的落地方式。

flock 是什么

flock 是一个实现了 线程安全(thread-safe)文件锁 的 Go 库。所谓"线程安全",体现在两方面:其一是库本身内部通过读写互斥锁保护状态,多个 goroutine 并发操作同一个 *Flock 实例是安全的;其二是它封装了操作系统底层的 flock(2) / LockFileEx / fcntl 锁,供应用层在多个进程之间协调对同一资源(通常以锁文件为标志)的互斥访问。

此外,该库还提供了非阻塞TryLock() / TryRLock() 函数,允许调用方在不阻塞执行的前提下探测锁是否可用——这正是它与传统阻塞式加锁最核心的差异点。

在 Moby 仓库中,该库被以间接依赖形式 vendored,版本为 github.com/gofrs/flock v0.13.0(见仓库根目录 go.mod),源码位于 vendor/github.com/gofrs/flock

安装与引入

根据 README 的说明,安装方式为标准 Go 模块命令:

go get -u github.com/gofrs/flock

引入方式:

import "github.com/gofrs/flock"

在 Moby 这类 vendor 化管理的仓库中无需手动安装,相关模块的版本与校验和记录在 go.modgo.sum 中,构建时直接复用 vendor/ 目录下的源码即可。

基本用法:TryLock 非阻塞加锁

README 给出最小可用示例。它创建锁文件、尝试获取独占(写)锁,若获取成功则执行受保护工作,最后释放锁:

import "github.com/gofrs/flock"

fileLock := flock.New("/var/lock/go-lock.lock")

locked, err := fileLock.TryLock()

if err != nil {
	// handle locking error
}

if locked {
	// do work
	fileLock.Unlock()
}

需要注意该示例的返回值语义:TryLock() 返回的第一个布尔值表示"本次是否成功拿到锁"。当锁正被其他进程持有时,它立即返回 false, nil(而非阻塞等待),因此 locked == false 时直接跳过临界区即可。若文件打开、底层加锁系统调用本身失败,才通过 err 返回错误。

对于需要"拿不到就等待"的场景,应改用阻塞 API Lock()(只返回 error),或者使用带上下文取消的 TryLockContext(见下文)。在多数分布式/并发场景中,官方文档与源码注释均推荐优先使用非阻塞的 TryLock()

完整 API 与参数说明

通过阅读 flock.go,可以将 API 归纳为以下五类:

构造与配置

  • New(path string, opts ...Option) *Flock:创建锁实例,只要求锁文件路径。库不会立即打开文件,而是在首次加锁时才打开。
  • NewFlock(path string) *Flock:历史命名,现已标记 Deprecated,等价于 New
  • SetFlag(flag int) Option:覆盖创建/打开锁文件时使用的 os.OpenFile flag。默认值为 os.O_CREATE | os.O_RDONLY(详见 flock.go),即"不存在则创建,并以只读方式打开"。特例是 aixsolarisillumos 平台会追加 os.O_RDWR,因为 AIX 无法对只读文件施加独占写锁。
  • SetPermissions(perm fs.FileMode) Option:覆盖创建锁文件时的权限,默认 0o600(仅属主可读写)。
// 自定义打开 flag 与权限的示例
lock := flock.New("/var/lock/app.lock",
    flock.SetFlag(os.O_CREATE|os.O_RDWR),
    flock.SetPermissions(0o644),
)

加锁(阻塞式)

  • Lock() error:阻塞直至取得独占(排他/写)锁。若当前实例已持有锁则短路直接返回。
  • RLock() error:阻塞直至取得共享(读)锁,可与多个读锁持有者共存,但与独占锁互斥。

加锁(非阻塞式,推荐)

  • TryLock() (bool, error):尝试取独占锁,失败立即返回 false,不等待。
  • TryRLock() (bool, error):尝试取共享锁,失败立即返回 false,不等待。

二者的实现(flock_unix.go)在底层调用时追加 LOCK_NB(non-blocking)标志;若内核返回 EWOULDBLOCK,直接映射为 (false, nil)

带超时/取消的重试加锁

  • TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error)
  • TryRLockContext(ctx context.Context, retryDelay time.Duration) (bool, error)

这两个函数在取锁失败后,每间隔 retryDelay 重试一次,直到以下任一条件满足:加锁成功、加锁返回错误、或 context.Context 被取消/超时。其核心重试循环实现在 tryCtx 辅助函数中(flock.go),通过 select 监听 ctx.Done()time.After(retryDelay),实现优雅的"竞态窗口内轮询拿锁":

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

locked, err := fileLock.TryLockContext(ctx, 100*time.Millisecond)
if err != nil {
    // context 超时或其他错误
}
if locked {
    defer fileLock.Unlock()
    // do work
}

解锁与关闭

  • Unlock() error:释放锁并关闭内部文件描述符。不会删除磁盘上的锁文件,是否清理由应用自行决定(源码注释在 flock_unix.go 中反复强调)。
  • Close() error:等价于 Unlock()

状态查询与辅助

  • Path() string:返回构造时传入的锁文件路径。
  • Locked() bool / RLocked() bool:返回当前实例是否持有独占/共享锁。注意返回值是快照,读取到使用时状态可能已变化,仅可用于诊断/日志。
  • Stat() (fs.FileInfo, error):返回锁文件的 FileInfo。官方注释指出可用于检查锁文件的修改时间以探测陈旧锁(stale lock)flock.go)。
  • String() string:返回锁文件路径,便于日志输出。

线程安全:内部 RWMutex 如何守护状态

Flock 结构体(flock.go)包含以下字段:

type Flock struct {
	path string
	m    sync.RWMutex   // 串行化所有锁操作
	fh   *os.File       // 底层文件句柄
	l    bool           // 是否持有独占锁
	r    bool           // 是否持有共享锁
	flag int            // 打开文件所用 flag
	perm fs.FileMode    // 锁文件权限
}

从源码结构看,所有会改变状态的加解锁方法(Lock/RLock/TryLock/TryRLock/Unlock)都先获取写锁 f.m.Lock(),防止同一进程内多个 goroutine 并发读写 fhlr 造成竞态;而 Locked()RLocked()Stat() 等查询方法仅取读锁 f.m.RLock(),可在加锁操作间隙并发安全地读取状态。这就是"线程安全文件锁"的含义——不同 goroutine 间靠这把内存互斥锁协调,不同进程间则靠操作系统文件锁协调。

ensureFhState()flock.go)还在每次加锁失败后自动回收不再持有的文件句柄,避免在未加锁状态下长期占用 fd。

跨平台实现:三种后端与平台差异

库的 README 及 doc.go 头注释 都提醒:加锁行为在不同平台上并不能保证一致,例如某些类 UNIX 系统会把共享锁透明地升级为独占锁。具体实现按构建标签分为四个文件:

主流 UNIX(Linux / macOS / BSD 等):flock(2)

flock_unix.go 覆盖 darwindragonflyfreebsdillumoslinuxnetbsdopenbsd。底层调用 unix.Flock(fd, flag)

  • Lock() 使用 LOCK_EX(排他锁);
  • RLock() 使用 LOCK_SH(共享锁);
  • TryLock() / TryRLock() 追加 LOCK_NB 并识别 EWOULDBLOCK

实现里还有一个值得注意的容错逻辑 reopenFDOnErrorflock_unix.go):当 flock(2) 返回 EIOEBADF(例如 NFSv4 上 flockfcntl 模拟,或内核 3.4 之后的某些网络文件系统场景),库会校验文件权限后用 O_RDWR 重新打开文件句柄再重试一次。此策略的注释直接援引了 util-linux 项目中 flock.c 的做法,属于对"在网络文件系统上取锁"这类边界情况的工程化防御。

AIX / Solaris:fcntl 锁

flock_unix_fcntl.go 覆盖 aixsolaris && !illumos,代码改编自 Go 官方 cmd/go/internal/lockedfile 中基于 POSIX fcntl 的文件锁实现。由于 fcntl 锁绑定的是 (inode, process)对 而非文件描述符,任何指向同一 inode 的描述符被关闭都会释放锁,所以该文件内额外用包级 mu sync.Mutexinodes/locks 两张映射表做进程内同步,并通过排队 channel 协调等待者交接锁所有权;同时对 AIX/Solaris 上"按进程而非线程计算死锁图"导致的EDEADLK 做了指数退避重试(1ms 起步、上限 500ms、附加 10% 抖动)处理。

Windows:LockFileEx / UnlockFileEx

flock_windows.go 基于 Win32 API:加锁走 windows.LockFileEx,独占锁标志为 LOCKFILE_EXCLUSIVE_LOCK,共享锁标志为 0;非阻塞版本追加 LOCKFILE_FAIL_IMMEDIATELY。当系统调用返回锁冲突错误码 0x21ErrorLockViolation,即 33)或 ERROR_IO_PENDING 时,Try* 函数返回 (false, nil) 表示"锁正被占用"。

plan9 / 其他平台:显式不支持

flock_others.go 的构建标签为 (!unix && !windows) || plan9,所有方法直接返回包装为 fs.PathErrorerrors.ErrUnsupported,即在不支持文件锁语义的平台上显式报错而非静默失效。

在 Moby 仓库中的真实使用场景

作为 vendor 依赖,flock 在 Moby 仓库中有多处实际调用,可作为最佳实践参考。

场景一:OCI 镜像索引的并发读写保护

BuildKit 客户端实现 ociindex.go 用锁文件保护 OCI layout 的 index.json 读写。读取时取共享锁,写入时取独占锁,并且对 EPERM/EROFS(只读文件系统)做了容错降级:

func (s StoreIndex) Read() (*ocispecs.Index, error) {
	lock := flock.New(s.lockPath)
	locked, err := lock.TryRLock()
	if err != nil {
		if !errors.Is(err, syscall.EPERM) && !errors.Is(err, syscall.EROFS) {
			return nil, errors.Wrapf(err, "could not lock %s", s.lockPath)
		}
	} else {
		if !locked {
			return nil, errors.Errorf("could not lock %s", s.lockPath)
		}
		defer func() {
			lock.Unlock()
			os.RemoveAll(s.lockPath)
		}()
	}
	// ... 读取并解析 index.json
}

注意其惯用法:Unlock() 后主动 os.RemoveAll(s.lockPath) 清理锁文件——这正是 README 强调的"释放锁不等于删除文件,删除由应用负责"的实践印证。Put 写路径(ociindex.go)则使用 TryLock() 独占锁串行化多个写者。

场景二:CNI 网络插件缓存目录的互斥

CNI 网络配置提供方 cni.go 也在生成/写入网络配置前引入 flock,避免多个容器并发初始化网络插件时互相覆盖配置文件。

使用注意事项与陷阱

综合 README、包文档与源码注释,实际使用时应特别留意以下几点:

  1. 锁文件不会被自动删除Unlock()/Close() 只释放锁并关闭 fd。若希望进程退出后不留残留文件,需要应用自行清理,或在明确无需再使用后删除。也正因为此,基于"锁文件是否存在"来判断是否加锁是错误的——应通过加锁调用的返回值判断。
  2. 共享锁可能被透明升级为独占锁:部分类 UNIX 系统上,进程先持共享锁再请求独占锁时,系统可能直接把既有共享锁升级为独占锁。此时若持有者自认为仍在读锁状态下调用 Unlock(),可能误释放后来获得的独占锁(flock.goflock_unix.go)。因此不建议在同一实例上混用 RLockLock
  3. Locked()/RLocked() 是易变快照:它们仅供查询,两次调用之间状态可能已变化,不能替代返回值判断加锁结果。
  4. 文件系统差异:NFS 等网络文件系统上 flock 行为可能与本地文件系统不同(库对此做了 EIO/EBADF 重试防御,但并不能保证所有文件系统语义一致);跨平台部署时建议阅读对应平台的实现文件。
  5. 默认以只读方式打开文件:如果锁文件已由其他程序以不可写权限创建,某些平台(如 AIX)会因无法写锁而失败,此时可用 SetFlag(os.O_RDWR) 覆盖默认 flag。

许可证与项目历史

flockBSD 3-Clause 许可证发布,完整条款见 vendor/github.com/gofrs/flock/LICENSE

该项目最初名为 github.com/theckman/go-flock,后由原作者 Tim Heckman 移交至 Gofrs 组织(README 的 Project History 一节记载了这段历史),因此源码文件的版权头同时包含 Tim Heckman(2015)与 The Gofrs(2018-2025)两份归属。

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388