文件锁无法可靠实现跨平台单例,因其不跨进程生命周期管理且各系统对“同一文件”语义理解不一致:NFS路径在Linux/macOS可能指向不同inode,Windows的CreateMutex默认会话级作用域导致服务与用户进程不可见;POSIX命名信号量(sem_open)更统一,但需配合PID心跳检测与RAII封装防残留。

为什么不能只靠文件锁实现单例
很多开发者一上来就用 flock 或 CreateMutex 封装一个文件锁,结果在 macOS 上启动失败、Linux 下重启后残留锁、Windows 服务场景下权限报错。根本原因是:文件锁不跨进程生命周期管理,且不同系统对“同一文件”的语义理解不一致——比如 NFS 挂载路径在 Linux 和 macOS 上可能指向不同 inode,而 Windows 的 CreateMutex 默认作用域是会话级,服务进程和用户进程根本看不到同一个 mutex。
用命名信号量(POSIX semaphores)统一接口
POSIX 信号量(sem_open)在 Linux/macOS 上原生支持,在 Windows 上可通过 WSL2 或 MinGW-w64 的 pthread 实现桥接;但更稳妥的做法是用抽象层封装三端原语。核心思路是:用进程名 + 校验字符串生成唯一键名,再调用对应平台的命名同步对象。
- Linux/macOS:用
sem_open("/myapp_singleton", O_CREAT, 0644, 1),初始值设为 1,首次sem_wait成功即获得锁,失败说明已有实例 - Windows:用
CreateSemaphore(NULL, 1, 1, L"Global\myapp_singleton"),注意必须加Global\前缀才能跨会话(否则仅限当前登录会话) - 关键细节:
sem_close/CloseHandle必须在进程退出前调用,否则锁可能残留;但更要防 crash——需配合 atexit 或 RAII 在析构时清理
如何避免锁残留导致后续启动失败
进程异常退出(如 SIGKILL、Windows 强制结束)会导致信号量/互斥体未释放。不能依赖超时自动清理(POSIX semaphores 不支持超时销毁),必须引入“心跳+过期”机制。
- 启动时先尝试打开已存在的信号量/互斥体,若成功则立即
sem_trywait(Linux/macOS)或WaitForSingleObject(hSem, 0)(Windows)检测是否真被占用 - 若检测到占用,读取共享内存中记录的 PID + 启动时间戳;用
kill(0)(Linux/macOS)或OpenProcess(Windows)验证该 PID 是否存活且属于本程序 - 若 PID 已死但信号量仍存在,调用
sem_unlink(Linux/macOS)或CloseHandle后不再重建(Windows 会自动清理已关闭句柄)
C++ RAII 封装的关键点
直接裸调系统 API 容易漏掉清理逻辑。推荐用 RAII 类包装,构造时尝试获取锁,析构时释放;但要注意:析构函数里不能 throw 异常,且需区分“主动释放”和“进程崩溃未释放”的场景。
立即学习“C++免费学习笔记(深入)”;
- 构造函数内调用平台适配的创建/打开逻辑,失败则抛
std::runtime_error("singleton lock failed") - 析构函数只做
sem_close或CloseHandle,不调sem_unlink—— unlink 应由首次启动者在确认无其他实例时主动调用 - 静态局部变量初始化保证线程安全:
static SingletonLock instance("myapp");,避免多线程竞争导致重复创建 - Windows 下特别注意:如果程序以服务身份运行,
Global\命名空间需要 SeCreateGlobalPrivilege 权限,开发阶段可先用Local\测试
跨平台单例真正的难点不在“加锁”,而在“判断锁是否失效”。多数人卡在 macOS 上 sem_unlink 后新进程仍打不开同名信号量——其实是旧信号量没被彻底销毁,得等系统回收,这时候就得靠 PID 检查兜底。


















