防止應用程式重複啟動是桌面應用基本需求。本文實作三種核心方案:Mutex (核心物件)、檔案鎖定 (跨會話可見)、WMI 進程檢查 (診斷用),並提供生產級 SingleInstanceManager 整合方案。
| 方案 | 原理 | 跨用戶會話 | 跨機器 | 殘留風險 | 推薦度 |
|---|---|---|---|---|---|
| Mutex | 核心命名物件 | ✅ Global\ 前綴 | ❌ | 低 (進程結束自動釋放) | ⭐⭐⭐⭐⭐ 首選 |
| 檔案鎖定 | 獨佔開啟檔案 | ✅ 共享目錄 | ✅ 網路共享 | 中 (崩潰可能殘留檔案) | ⭐⭐⭐ 補充 |
| WMI/Process | 掃描進程清單 | ⚠️ 需權限 | ❌ | 無 | ⭐⭐ 診斷用 |
| RegisterWindowMessage | 視窗訊息廣播 | ❌ 僅當前會話 | ❌ | 低 | ⭐⭐ 老舊 |
using System;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Reflection;
using System.Runtime.InteropServices;
public static class SingleInstanceManager
{
// ========== 1. Mutex 方案 (推薦主方案) ==========
private static Mutex _mutex;
private static bool _mutexOwned = false;
public static bool TryAcquireMutex(string appId = null)
{
string mutexName = $"Global\\{appId ?? Assembly.GetEntryAssembly().GetName().Name}";
try
{
_mutex = new Mutex(true, mutexName, out _mutexOwned);
return _mutexOwned;
}
catch (AbandonedMutexException)
{
// 前一擁有者異常結束,我們獲得擁有權
_mutexOwned = true;
return true;
}
catch
{
return false;
}
}
public static void ReleaseMutex()
{
if (_mutexOwned && _mutex != null)
{
_mutex.ReleaseMutex();
_mutex.Dispose();
_mutex = null;
_mutexOwned = false;
}
}
// ========== 2. 檔案鎖定方案 (補充/跨機器) ==========
private static FileStream _lockFile;
public static bool TryAcquireFileLock(string lockPath = null)
{
try
{
string path = lockPath ?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
Assembly.GetEntryAssembly().GetName().Name + ".lock");
_lockFile = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
return true;
}
catch (IOException)
{
return false; // 已被鎖定
}
}
public static void ReleaseFileLock()
{
_lockFile?.Dispose();
_lockFile = null;
}
// ========== 3. 進程檢查 + 啟動現有實例 ==========
public static Process GetRunningInstance()
{
Process current = Process.GetCurrentProcess();
string currentPath = current.MainModule.FileName;
foreach (Process proc in Process.GetProcessesByName(current.ProcessName))
{
try
{
if (proc.Id != current.Id && proc.MainModule.FileName == currentPath)
return proc;
}
catch { /* Access denied */ }
}
return null;
}
public static bool ActivateExistingInstance(Process existing)
{
if (existing == null) return false;
// 1. 還原視窗
ShowWindowAsync(existing.MainWindowHandle, SW_RESTORE);
// 2. 置頂
SetForegroundWindow(existing.MainWindowHandle);
return true;
}
// Win32 API
private const int SW_RESTORE = 9;
[DllImport("user32.dll")] private static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
// ========== 綜合啟動邏輯 ==========
public enum StartupResult
{
FirstInstance, // 首個實例,正常啟動
ActivatedExisting, // 已有實例,已喚醒
Failed // 獲取鎖失敗 (異常情況)
}
public static StartupResult Startup(string[] args, Action onActivatedExisting = null)
{
// 策略:優先 Mutex,失敗則檢查進程
if (TryAcquireMutex())
{
return StartupResult.FirstInstance;
}
// Mutex 失敗,嘗試喚醒現有實例
var existing = GetRunningInstance();
if (existing != null)
{
if (ActivateExistingInstance(existing))
{
onActivatedExisting?.Invoke(existing);
return StartupResult.ActivatedExisting;
}
}
return StartupResult.Failed;
}
public static void Shutdown()
{
ReleaseMutex();
ReleaseFileLock();
}
}
using System;
using System.Windows.Forms;
static class Program
{
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var result = SingleInstanceManager.Startup(args, existing =>
{
// 可選:傳遞命令列參數給現有實例
// 透過 WM_COPYDATA、Named Pipe、檔案、Registry 等 IPC
SendArgsToExistingInstance(existing, args);
});
switch (result)
{
case SingleInstanceManager.StartupResult.FirstInstance:
Application.Run(new MainForm());
break;
case SingleInstanceManager.StartupResult.ActivatedExisting:
// 已喚醒現有實例,直接退出
Environment.Exit(0);
break;
case SingleInstanceManager.StartupResult.Failed:
MessageBox.Show("應用程式啟動失敗:無法獲取單一實例鎖定。", "錯誤",
MessageBoxButtons.OK, MessageBoxIcon.Error);
break;
}
SingleInstanceManager.Shutdown();
}
// 簡易 IPC:將參數寫入暫存檔,現有實例透過 FileSystemWatcher 讀取
private static void SendArgsToExistingInstance(Process existing, string[] args)
{
if (args.Length == 0) return;
string ipcPath = Path.Combine(Path.GetTempPath(), "MyApp_Ipc_" + existing.Id + ".txt");
File.WriteAllLines(ipcPath, args);
// 發送自訂訊息通知 (需現有實例掛鉤 WndProc)
// PostMessage(existing.MainWindowHandle, WM_CUSTOM, IntPtr.Zero, IntPtr.Zero);
}
}
public partial class MainForm : Form
{
private const int WM_COPYDATA = 0x004A;
private FileSystemWatcher _ipcWatcher;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_COPYDATA)
{
var cds = (COPYDATASTRUCT)m.GetLParam(typeof(COPYDATASTRUCT));
string data = Marshal.PtrToStringUni(cds.lpData);
HandleIpcData(data);
}
base.WndProc(ref m);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
StartIpcListener();
}
private void StartIpcListener()
{
string ipcPath = Path.Combine(Path.GetTempPath(), "MyApp_Ipc_" + Process.GetCurrentProcess().Id + ".txt");
if (File.Exists(ipcPath)) File.Delete(ipcPath);
_ipcWatcher = new FileSystemWatcher(Path.GetDirectoryName(ipcPath), Path.GetFileName(ipcPath))
{
EnableRaisingEvents = true
};
_ipcWatcher.Created += (s, e) => ProcessIpcFile(e.FullPath);
_ipcWatcher.Changed += (s, e) => ProcessIpcFile(e.FullPath);
}
private void ProcessIpcFile(string path)
{
try
{
string[] args = File.ReadAllLines(path);
File.Delete(path);
HandleCommandLineArgs(args); // 處理如開啟檔案、跳轉頁面等
}
catch { }
}
private void HandleIpcData(string data) { /* WM_COPYDATA 方式 */ }
private void HandleCommandLineArgs(string[] args) { /* 檔案方式 */ }
}
[StructLayout(LayoutKind.Sequential)]
struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
// 使用 Named Pipe (System.IO.Pipes) - 跨進程、雙向、高效能
// Server (現有實例)
var server = new NamedPipeServerStream("MyAppPipe", PipeDirection.InOut, 1);
await server.WaitForConnectionAsync();
using (var reader = new StreamReader(server))
{
string argsJson = await reader.ReadToEndAsync();
// 反序列化並處理
}
// Client (新啟動實例)
var client = new NamedPipeClientStream(".", "MyAppPipe", PipeDirection.Out);
await client.ConnectAsync(1000);
using (var writer = new StreamWriter(client))
{
await writer.WriteLineAsync(JsonSerializer.Serialize(args));
}
public static bool RunAsAdminIfNeeded()
{
if (!new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
{
var psi = new ProcessStartInfo
{
FileName = Assembly.GetEntryAssembly().Location,
UseShellExecute = true,
Verb = "runas"
};
try { Process.Start(psi); return true; }
catch { return false; }
}
return false; // 已是管理員
}
| 場景 | Mutex | FileLock | ProcessCheck |
|---|---|---|---|
| 同目錄雙擊啟動 | ✅ | ✅ | ✅ |
| 不同目錄同名啟動 | ✅ | ✅ | ❌ (檔名相同視為同一) |
| 重命名 exe 啟動 | ❌ (Mutex 名稱含組件名) | ❌ | ❌ |
| 不同用戶登入 (Fast User Switching) | ✅ (Global\ 前綴) | ✅ (CommonAppData) | ⚠️ 需權限 |
| 遠端桌面會話 | ✅ | ✅ | ⚠️ |
| 崩潰後殘留 | ✅ 自動釋放 | ⚠️ 需啟動時清理 | ✅ 無殘留 |
Global\\{Guid} Mutex,進程結束自動釋放,無殘留風險Process.GetProcessesByName + 路徑比對,喚醒現有視窗Application.ApplicationExit / AppDomain.ProcessExit 確保釋放單一實例核心是 命名系統物件互斥。Mutex 是 Windows 核心提供、自動清理、跨會話可見的最佳選擇。生產代碼建議:SingleInstanceManager 封裝 Mutex + Process 喚醒 + Named Pipe IPC,統一處理啟動邏輯、參數傳遞、優雅關閉。理解各方案適用邊界,才能在複雜部署環境 (Citrix、RemoteApp、容器、多用戶) 保證正確行為。