首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 單一實例應用程式完整實作:Mutex、檔案鎖定、WMI 三種方案對比與最佳實踐

     編輯:關於C語言

前言

防止應用程式重複啟動是桌面應用基本需求。本文實作三種核心方案:Mutex (核心物件)、檔案鎖定 (跨會話可見)、WMI 進程檢查 (診斷用),並提供生產級 SingleInstanceManager 整合方案。

方案對比

方案原理跨用戶會話跨機器殘留風險推薦度
Mutex核心命名物件✅ Global\ 前綴❌低 (進程結束自動釋放)⭐⭐⭐⭐⭐ 首選
檔案鎖定獨佔開啟檔案✅ 共享目錄✅ 網路共享中 (崩潰可能殘留檔案)⭐⭐⭐ 補充
WMI/Process掃描進程清單⚠️ 需權限❌無⭐⭐ 診斷用
RegisterWindowMessage視窗訊息廣播❌ 僅當前會話❌低⭐⭐ 老舊

核心實作:SingleInstanceManager


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();
    }
}

Program.cs 整合範例


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);
    }
}

MainForm 接收 IPC 參數


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;
}

進階主題

單一實例 + 命令列參數傳遞 (現代 IPC)


// 使用 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; // 已是管理員
}

測試矩陣驗證

場景MutexFileLockProcessCheck
同目錄雙擊啟動✅✅✅
不同目錄同名啟動✅✅❌ (檔名相同視為同一)
重命名 exe 啟動❌ (Mutex 名稱含組件名)❌❌
不同用戶登入 (Fast User Switching)✅ (Global\ 前綴)✅ (CommonAppData)⚠️ 需權限
遠端桌面會話✅✅⚠️
崩潰後殘留✅ 自動釋放⚠️ 需啟動時清理✅ 無殘留

最佳實踐總結

  1. 主方案:Global\\{Guid} Mutex,進程結束自動釋放,無殘留風險
  2. 輔助方案:Process.GetProcessesByName + 路徑比對,喚醒現有視窗
  3. IPC:Named Pipe 傳遞命令列參數給現有實例
  4. 清理:Application.ApplicationExit / AppDomain.ProcessExit 確保釋放
  5. 測試:覆蓋同目錄、不同目錄、重命名、不同用戶、遠端桌面、崩潰重啟
  6. 避坑:勿用檔案存在性判斷 (殘留風險)、勿硬編碼 Mutex 名稱 (衝突風險)、注意 32/64 位進程互通

總結

單一實例核心是 命名系統物件互斥。Mutex 是 Windows 核心提供、自動清理、跨會話可見的最佳選擇。生產代碼建議:SingleInstanceManager 封裝 Mutex + Process 喚醒 + Named Pipe IPC,統一處理啟動邏輯、參數傳遞、優雅關閉。理解各方案適用邊界,才能在複雜部署環境 (Citrix、RemoteApp、容器、多用戶) 保證正確行為。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved