结论:必须用带"global"前缀的命名mutex并检查creatednew参数;process.getprocessesbyname因重命名、vshost进程、多会话隔离和权限问题极易误判,不可靠。

直接说结论:用 Mutex,但必须带 "Global\" 前缀 + 显式检查 createdNew 输出参数;其他方法要么靠不住,要么治标不治本。
为什么不能只靠进程名判断(Process.GetProcessesByName)
这个方法表面简单,实际在开发和多用户环境下极易误判:
-
ProcessName是可执行文件名(不含路径),重命名 EXE 后再双击,就变成“另一个程序”; - Visual Studio 调试时会生成
*.vshost.exe进程,和你真正的程序名不同,但GetProcessesByName可能匹配到它或漏掉它; - 多个用户登录同一台机器时,
GetProcessesByName默认只查当前会话的进程,不同用户的同名程序不会被发现 —— 也就起不到全局单实例作用; - 没有权限读取某些系统进程的
MainModule.FileName,会抛Win32Exception,不加try/catch就直接崩。
Mutex 怎么写才真正可靠
关键不在“用了没”,而在“怎么用” —— 错一个参数,跨用户、调试、UAC 提权场景全失效:
- 互斥体名称必须以
"Global\"开头(例如"Global\MyApp_2026"),否则默认是会话级(Session-scoped),不同 Windows 用户各自能跑一个; - 必须使用
out bool createdNew判断,而不是靠WaitOne()返回值 —— 因为首次创建时initiallyOwned: true会让当前线程自动获得所有权,WaitOne(0)永远返回true; - 不要手动调用
ReleaseMutex()后再Dispose();用using最安全,它会在作用域结束时自动释放,哪怕中途异常也不会泄漏; - 名称里避免硬编码空格或特殊字符,Windows 对互斥体名解析严格,
"My App"可能被截断或拒绝创建。
正确写法示例(Windows Forms):
static void Main() {
string mutexName = "Global\" + Application.ProductName.Replace(" ", "_");
using (var mutex = new Mutex(true, mutexName, out bool createdNew)) {
if (!createdNew) {
MessageBox.Show("应用程序已在运行!");
return;
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
调试时 Mutex 为啥总说“已运行”
不是代码错了,是 Visual Studio 的调试机制在捣鬼:
- 热重载(Hot Reload)或附加调试器时,旧进程可能没完全退出,
Mutex还被持有; - 调试中断后点“停止调试”,.NET 有时来不及走完
using的Dispose,导致Mutex残留; - 解决办法:任务管理器里手动杀掉所有同名进程,或改个临时
mutexName(比如加_debug后缀)专供调试用; - 更稳的做法:在开发配置下跳过单实例检查,用
#if DEBUG包一层,上线前删掉。
想把焦点切到已有窗口?Mutex 本身做不到
Mutex 只负责“拦住第二个实例”,不负责“唤醒第一个”。如果用户双击图标希望唤起已隐藏的主窗口,得额外做两件事:
- 第一个实例启动后,保存自己的主窗体句柄(
mainForm.Handle)到共享位置(如内存映射文件或注册表); - 第二个实例检测到
!createdNew后,用ShowWindowAsync和SetForegroundWindowAPI 激活它; - 注意:UAC 权限隔离下,高权限进程无法激活低权限进程的窗口,反之亦然 —— 所以最好统一以相同权限启动。
真正难的从来不是“怎么写第一行 Mutex”,而是“怎么让 Mutex 在所有真实环境里都不掉链子”:跨用户、调试中断、权限切换、服务化部署……这些地方一漏,单实例就形同虚设。










