Mutex 是 Windows 下保证 C# 程序单实例最可靠的方式,基于内核对象、跨进程可见、自动释放;需用 Global 前缀命名、持有实例至退出、配合 API 激活已有窗口。

用 Mutex 判断进程是否已存在是最可靠的方式
Windows 下保证 C# 程序单实例运行,Mutex 是最常用也最稳妥的方案。它基于系统内核对象,跨进程可见、线程安全,且能自动释放(进程退出时系统回收)。比检查进程名或窗口句柄更准确——后者在多用户会话、服务账户或同名进程干扰下容易误判。
实操要点:
-
Mutex名称必须带全局前缀(如"Global\MyApp_123e4567-e89b-12d3-a456-426614174000"),否则在 Terminal Services 多会话环境下可能被隔离成会话局部对象,导致“单实例”失效 - 创建时需捕获
UnauthorizedAccessException(UAC 提权场景)和IOException(名称非法或长度超限),但主要靠mutex.WaitOne(0)的返回值判断是否首次获取成功 - 不要在
Main方法里直接new Mutex(...).WaitOne()后就跑 UI —— 必须把Mutex实例持有住,直到主窗体关闭,否则 GC 回收后锁立即释放,第二个实例就能抢入
WinForms 程序中如何把焦点切到已有窗口而不是启动新实例
仅阻止新进程启动不够,用户双击图标或从快捷方式启动时,得把已有窗口激活并带到前台。这需要组合使用 Windows API 和 .NET 窗体控制逻辑。
关键步骤:
- 在主窗体
Load事件中调用NativeMethods.SetForegroundWindow和NativeMethods.ShowWindow(参数用SW_RESTORE),确保最小化/隐藏状态也能恢复 - 通过
Process.GetCurrentProcess().MainWindowHandle获取当前窗体句柄,并在另一个进程检测到互斥锁已被占用时,用FindWindow+ 类名或窗口标题定位目标窗体(推荐用自定义Application.SetCompatibleTextRenderingDefault(false)后设置的this.Text做唯一标识) - 避免用
BringToFront()或Focus()单独调用——它们在无焦点策略或高 DPI 缩放下常失效;必须搭配ShowWindow和SetForegroundWindow
WPF 程序单实例需额外处理 App 生命周期和消息路由
WPF 的 Application 类默认不暴露主窗口句柄,且启动流程比 WinForms 更抽象,直接套用 Mutex 模式容易漏掉激活逻辑。
正确做法:
- 在
App.xaml.cs的OnStartup中创建并持有Mutex,用if (!mutex.WaitOne(0))快速退出新实例,并在此分支里发送自定义 Windows 消息(如WM_COPYDATA)唤醒原进程 - 原进程需在主窗口重写
WndProc(通过HwndSource.AddHook注册回调),监听唤醒消息并执行Activate()、WindowState = WindowState.Normal、Topmost = true; Topmost = false(绕过系统焦点限制) - 别依赖
Application.Current.MainWindow在OnStartup阶段就可用——它可能为 null,应在MainWindow.Loaded事件中才真正初始化完成
调试时常见陷阱:VS 启动多个实例、热重载干扰、ClickOnce 部署冲突
开发阶段看到“单实例失效”,大概率不是代码问题,而是环境干扰。
典型表现与对策:
- Visual Studio 默认启用“启用 Visual Studio 承载进程”(
vshost.exe),会导致每次调试都新建一个托管宿主进程,Mutex名称相同但实际是不同进程竞争——关掉项目属性 → 调试 → “启用 Visual Studio 承载进程”复选框即可 - .NET 6+ 热重载(Hot Reload)会动态注入新程序集,但不重启进程,所以不影响
Mutex;但若你手动改了Mutex名称又没清缓存,旧实例锁还占着,新调试实例就会被拦住——此时任务管理器结束所有相关进程最干脆 - ClickOnce 发布时若版本号未递增,系统可能复用旧部署缓存,导致两个“不同版本”共存;务必检查
PublishVersion并确保每次发布都变更,或在Mutex名称中嵌入版本哈希(如SHA256("MyApp" + AssemblyVersion))
真正难处理的是多用户登录(比如远程桌面同时两人)、服务账户后台运行、以及 .NET Core / .NET 5+ 跨平台需求——这些场景下 Mutex 依然有效,但激活窗口逻辑必须降级为仅发通知,不能再强求 SetForegroundWindow 成功。











