windows 下用 createmutex 实现单实例需检查 getlasterror() 是否为 error_already_exists,命名必须以 "global" 开头;linux/macos 推荐 flock 文件锁;跨进程互斥不能用 std::mutex;唤醒旧实例比阻止新实例更关键。

Windows 下用 CreateMutex 实现进程级单实例互斥
Windows 平台最直接的方式是调用 Win32 API 的 CreateMutex,靠内核对象名称(Named Mutex)做跨进程排他判断。关键不是“创建成功”,而是看返回值是否为 ERROR_ALREADY_EXISTS。
常见错误是只检查 NULL 返回值,漏掉已存在但句柄有效的场景;或者没加前缀导致命名冲突(比如多个软件用了相同的 "MyAppMutex")。
- 必须用全局命名空间:名称以
"Global\"开头(如"Global\MyApp_12345"),否则在 Terminal Services 或多会话环境下可能被隔离 - 调用后立即用
GetLastError()判断是否为ERROR_ALREADY_EXISTS,而不是仅依赖返回句柄是否非NULL - 主窗口启动前就要完成检查,否则可能两个进程都走到了 UI 初始化阶段才退出一个,视觉上仍有“闪现双开”
HANDLE hMutex = CreateMutex(NULL, TRUE, L"Global\MyApp_7F2A1E9C");
if (hMutex == NULL) {
// 创建失败(权限/内存等)
}
else if (GetLastError() == ERROR_ALREADY_EXISTS) {
// 已有实例在运行 → 通知已有窗口并退出当前进程
CloseHandle(hMutex);
return 0;
}
// 否则继续运行,并保持 hMutex 活跃直到程序退出
Linux/macOS 下用 sem_open 或文件锁模拟 Named Mutex
POSIX 没有原生命名互斥量,sem_open 是最接近的替代,但它本质是命名信号量,需手动管理初值和清理;更稳妥的做法是用带 O_EXCL 的临时文件 + flock,兼容性更好、无残留风险。
容易踩的坑是信号量未销毁(sem_unlink 忘调),下次启动时 sem_open 失败;或文件锁路径写死在 /tmp 下被其他用户进程干扰。
- 推荐用
flock:打开一个固定路径的文件(如/tmp/myapp.lock),用open(..., O_CREAT | O_RDWR)+flock(fd, LOCK_EX | LOCK_NB) - 失败时检查
errno == EWOULDBLOCK,而非errno == EEXIST(那是open阶段的错误) - 路径尽量带用户标识,比如
/tmp/myapp_$UID.lock,避免 root 和普通用户互相干扰
C++ 跨平台封装要注意 std::mutex 不行,必须用系统原语
std::mutex 是线程级同步原语,完全不跨进程——哪怕你在两个进程里各建一个同名 std::mutex,也毫无互斥效果。所有“跨进程单实例”必须依赖操作系统提供的命名同步对象。
有人试图用共享内存 + 自旋锁模拟,结果因缓存一致性、信号中断、进程崩溃未释放等问题,极易出现假死或漏判。别省这一步。
- 不要封装成“自动跨平台类”,不同系统机制差异太大;宁可写两套逻辑,用
#ifdef _WIN32分离 - Linux 上慎用
boost::interprocess::named_mutex:它底层仍依赖sem_open或shm_open,同样面临清理和权限问题 - 如果用 Qt,优先用
QSharedMemory+QSystemSemaphore组合,比自己调 POSIX 更稳(Qt 已处理了sem_unlink时机)
真正难的是“唤醒旧实例”而不是“阻止新实例”
单纯退出新进程只是半截方案。用户双击图标时,期望是焦点切到已有窗口,而不是弹个错误框再消失。这需要额外 IPC 机制:Windows 可用 FindWindow + SetForegroundWindow,Linux 可用 D-Bus 或 Unix domain socket 发送激活信号。
很多人卡在这步:找到窗口句柄后没调 ShowWindow(hwnd, SW_RESTORE),或没处理最小化状态;Linux 下 D-Bus 名称注册失败却不报错,导致唤醒静默失败。
- Windows:用
FindWindow(L"MyAppWindowClass", NULL)查找,再用IsIconic判断是否最小化,必要时OpenIcon或ShowWindow - Linux:D-Bus 服务名建议带包名前缀(如
com.mycompany.MyApp),避免和其他软件冲突;dbus-send 命令调试时加--print-reply看实际响应 - 无论哪边,都要在主消息循环启动前完成唤醒,否则旧进程还没开始处理消息就返回了
命名互斥本身不难,难的是让“单实例”体验真正无缝——用户感知不到你做了互斥,只觉得点两次图标,第二次就是把窗口带到前台。那个“带到前台”的逻辑,才是最容易被跳过、也最影响体验的一环。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











