【MFC】二重起動防止:CreateMutexをGetLastError 183で判定

【MFC】二重起動防止:CreateMutex を GetLastError 183 で判定

MFCアプリで二重起動防止をするときは、InitInstanceで名前付きミューテックスをCreateMutexで作り、直後のGetLastError()が183(ERROR_ALREADY_EXISTS)かどうかで、同じアプリがすでに動いているかを判断します。同じ名前のミューテックスがすでにあってもCreateMutexは失敗せず、既存のミューテックスのハンドルを返すので、戻り値がNULLかどうかでは1つめと2つめの起動を区別できません。


目次

CreateMutexの戻り値とGetLastErrorの組み合わせ

CreateMutexWの説明では、名前付きミューテックスがこの呼び出しの前からあった場合、戻り値は既存のミューテックスのハンドルになり、GetLastErrorはERROR_ALREADY_EXISTSを返すとされています。

戻り値GetLastError()意味アプリがすること
NULL以外0(ERROR_SUCCESS)このプロセスがミューテックスを新しく作った。最初の起動そのまま画面を開く。ハンドルはアプリが終わるまで閉じない
NULL以外183(ERROR_ALREADY_EXISTS)同じ名前のミューテックスがすでにあり、そのハンドルが返った。2つめの起動1つめのウィンドウを前に出し、InitInstanceでFALSEを返して終わる
NULL6(ERROR_INVALID_HANDLE)同じ名前のイベント・セマフォ・ファイルマッピングなど、ミューテックス以外のオブジェクトがあるミューテックスの名前を、ほかと重ならないものに変える
NULL上記以外ミューテックスを作ることも開くこともできなかった番号をログに残して起動をやめる

183をFormatMessageで文章にすると「既に存在するファイルを作成することはできません。」になります。ファイルの話に読めますが、ミューテックスでも同じ番号が使われます。


同じexeを2つ起動したときのCreateMutexとGetLastError 183

Visual Studio 2026、Debug/x64のMFCダイアログで試しました。InitInstanceの最初でCreateMutex(NULL, FALSE, _T("Local\\MFCGuide.251.SingleInstance"))を呼び、1つめの起動が画面を開いている間に、同じexeをもう1つ起動しています。2つめの起動は、自分が見た値を1つめのウィンドウへメッセージで送ってから終わるので、両方の値が1つめの画面に並んでいます。

同じexeを2回起動し、1つめのCreateMutexはGetLastError 0、2つめは戻り値がNULL以外のままGetLastError 183になったMFCダイアログの実行結果
2つめの起動(B)も戻り値はNULL以外で、GetLastErrorが0から183に変わった

Aの最初の起動では、戻り値がNULL以外でGetLastError()は0でした。Bの2つめの起動でも戻り値はNULL以外で、GetLastError()だけが183に変わりました。戻り値がNULLかどうかで判定するコードだと、2つめも最初の起動と同じ扱いになり、画面が2つ開きます。

Bの2つめは183を見て1つめのウィンドウを探し、SetForegroundWindowを呼びました(戻り値は0以外で、ウィンドウは前面に出ました)。そのあとInitInstanceでFALSEを返して、画面を開かずに終わりました。

CとDは、1つめの画面の中でCMutexを作って試した結果です。Cは同じ名前でCMutexを作った直後の値、Dは関数のローカル変数のCMutexが閉じたあとに同じ名前で作り直したときの値です。


InitInstanceに書く二重起動防止のコード

ミューテックスのハンドルは、アプリのクラスのメンバー変数に持たせます。ミューテックスは最後のハンドルが閉じられたときに消えるので、1つめの起動がハンドルを閉じてしまうと、その後に起動した2つめは183を受け取れません。

// MyApp.h
class CMyApp : public CWinApp
{
// Construction
public:
    CMyApp();

// Overrides
public:
    virtual BOOL InitInstance();
    virtual int ExitInstance();

// Implementation
protected:
    HANDLE m_hSingleInstance; // アプリが終わるまで閉じない

    DECLARE_MESSAGE_MAP()
};

2つめの起動は、1つめのメインウィンドウを探して前に出してから終わります。ウィンドウの題名で探すと、題名にファイル名が入るアプリでは見つからないことがあります。この例では、1つめのメインウィンドウにSetPropで目印を付けておき、2つめはEnumWindowsで全部のトップレベルウィンドウを順に調べて、目印の付いたウィンドウを探しています。ミューテックスの名前と目印の名前は、アプリのクラスとメインウィンドウのクラスの両方で使います。

// SingleInstance.h(MyApp.cpp と MyDlg.cpp からインクルードする)
#pragma once

// 同じログオンセッションの中で1つだけ起動させる
static const TCHAR szSingleInstanceMutexName[] = _T("Local\\MyCompany.MyApp.SingleInstance");

// 1つめの起動のメインウィンドウに付ける目印
static const TCHAR szMainWindowProp[] = _T("MyCompany.MyApp.MainWindow");
// MyApp.cpp
#include "SingleInstance.h"

static BOOL CALLBACK FindMainWindow(HWND hWnd, LPARAM lParam)
{
    if (::GetProp(hWnd, szMainWindowProp) == NULL)
        return TRUE; // 次のウィンドウを調べる

    *reinterpret_cast<HWND*>(lParam) = hWnd;
    return FALSE; // 見つかったので列挙をやめる
}

static void ActivateRunningInstance()
{
    HWND hWnd = NULL;
    ::EnumWindows(FindMainWindow, reinterpret_cast<LPARAM>(&hWnd));
    if (hWnd == NULL)
        return;

    if (::IsIconic(hWnd))
        ::ShowWindow(hWnd, SW_RESTORE); // 最小化されていたら元に戻す
    ::SetForegroundWindow(hWnd);
}

CMyApp::CMyApp()
    : m_hSingleInstance(NULL)
{
}

BOOL CMyApp::InitInstance()
{
    m_hSingleInstance = ::CreateMutex(NULL, FALSE, szSingleInstanceMutexName);
    const DWORD dwError = ::GetLastError(); // ほかのAPIを呼ぶ前に保存する
    if (m_hSingleInstance == NULL)
    {
        TRACE(_T("CreateMutex failed: %lu\n"), dwError);
        return FALSE;
    }

    if (dwError == ERROR_ALREADY_EXISTS)
    {
        ActivateRunningInstance();
        return FALSE; // 画面を開かずに終わる
    }

    CWinApp::InitInstance();
    // ここから下は、ウィザードが作ったコードのまま
    // ...
}

int CMyApp::ExitInstance()
{
    if (m_hSingleInstance != NULL)
    {
        ::CloseHandle(m_hSingleInstance);
        m_hSingleInstance = NULL;
    }
    return CWinApp::ExitInstance();
}

GetLastError()は、CreateMutexの次の行で変数に保存しています。間にほかのAPIを呼ぶと、そのAPIが値を書き換えることがあるためです(GetLastErrorは失敗直後に読む)。CreateMutexは成功したときにも183を残すので、成功か失敗かにかかわらず、次の行で保存しています。

MFCのAfxWinMainは、InitInstanceがFALSEを返したときもExitInstanceを呼んでから終わります。2つめの起動が開いたハンドルも、ここで閉じられます。ExitInstanceで閉じなくても、プロセスが終わればハンドルはシステムが閉じます。

目印は、ダイアログベースのアプリならOnInitDialogでSetPropを呼んで付け、OnDestroyでRemovePropを呼んで外します。SDI / MDIのアプリなら、CMainFrameのOnCreateとOnDestroyに同じ処理を書きます。

// MyDlg.cpp
#include "SingleInstance.h"

BOOL CMyDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    ::SetProp(GetSafeHwnd(), szMainWindowProp, reinterpret_cast<HANDLE>(1));
    // ...
    return TRUE;
}

void CMyDlg::OnDestroy()
{
    ::RemoveProp(GetSafeHwnd(), szMainWindowProp);
    CDialogEx::OnDestroy();
}

SetForegroundWindowは、呼んだプロセスが前面のプロセスから起動された、最後の入力を受け取った、などの条件を満たすときだけウィンドウを前面に出せます。条件を満たさないときはウィンドウが前に出ず、タスクバーのボタンが点滅します。


CMutexで書く場合:構築の直後にGetLastErrorを読み、ローカル変数にしない

MFCのCMutexを使っても、判定の仕方は同じです。Visual Studio 2026のMFCのソース(mtex.cpp)では、CMutexのコンストラクタは::CreateMutexを呼んだあと、戻り値がNULLなら例外を投げるだけで、ほかのAPIは呼んでいません。そのため、構築の次の行でGetLastError()を読めば183が取れます。CMutexの説明にも、作ったあとにGetLastErrorで、ミューテックスが前からあったかを確かめるよう書かれています。

// MyApp.h(CMutexで書く場合)
#include <afxmt.h>

class CMyApp : public CWinApp
{
    // ...
// Implementation
protected:
    CMutex* m_pSingleInstance; // アプリが終わるまで持つ
};

// MyApp.cpp
CMyApp::CMyApp()
    : m_pSingleInstance(NULL)
{
}

BOOL CMyApp::InitInstance()
{
    m_pSingleInstance = new CMutex(FALSE, szSingleInstanceMutexName);
    if (::GetLastError() == ERROR_ALREADY_EXISTS)
    {
        ActivateRunningInstance();
        return FALSE;
    }
    // ...
}

int CMyApp::ExitInstance()
{
    delete m_pSingleInstance;
    m_pSingleInstance = NULL;
    return CWinApp::ExitInstance();
}

CMutexをInitInstanceのローカル変数にすると、関数を抜けたときにデストラクタがハンドルを閉じます。ダイアログベースのアプリは、InitInstanceの中でDoModalを呼んで画面が閉じるまで待つので、ローカル変数でも画面を開いている間はハンドルが残ります。SDI / MDIのアプリは、InitInstanceの中でメインウィンドウを表示してからTRUEを返し、そのあとRunでメッセージループを回します。ローカル変数のCMutexはInitInstanceを抜けるときに閉じるので、画面を使っている間はもうハンドルがありません。

ほかにハンドルを持つプロセスがなければミューテックスも消え、次に起動したプロセスのGetLastError()は0になります。

new CMutexの直後のGetLastErrorが183になり、ローカル変数のCMutexが閉じたあとに同じ名前でCreateMutexを呼ぶとGetLastErrorが0になったMFCダイアログの実行結果
CはCMutexでも183が読め、Dはハンドルが閉じたあとなので0になった

上の画像は、最初に載せた実行結果と同じものです。Cは、1つめのハンドルが開いている間に同じ名前でnew CMutexを呼んだ直後の値で、183になりました。Dは、関数のローカル変数のCMutexが閉じたあとに同じ名前でCreateMutexを呼んだ結果で、183ではなく0が返りました。


ミューテックスの名前の付け方(Local\とGlobal\)

Kernel object namespacesによると、ミューテックスなどの名前は、ログオンのセッションごとに分かれた場所に作られます。名前の先頭に何も付けない場合とLocal\を付けた場合は、そのセッションの中だけで同じ名前として扱われます。リモートデスクトップやユーザーの切り替えで同じPCに別のセッションがあっても、それぞれのセッションで1つずつ起動できます。PC全体で1つにしたいときは、名前の先頭にGlobal\を付けます。

名前には、ほかのアプリと重ならないように会社名やアプリ名を入れます。CreateMutexWの説明には、悪意のあるユーザーが同じ名前のミューテックスを先に作ると、アプリを起動できなくされるおそれがあることと、その対策(名前をランダムにして権限のあるユーザーだけが読める場所に保存する、ユーザーのプロファイルのフォルダーにロックしたファイルを作る)も書かれています。


まとめ

  • 2つめの起動でもCreateMutexの戻り値はNULL以外になります。二重起動は、直後に保存したGetLastError()が183(ERROR_ALREADY_EXISTS)かどうかで判定します。
  • ミューテックスのハンドルは、アプリのメンバー変数に持たせて、アプリが終わるまで閉じません。CMutexをInitInstanceのローカル変数にすると、SDI / MDIではInitInstanceを抜けるときに閉じてしまい、その後の2つめの起動を見分けられません。
  • 2つめの起動は、1つめのメインウィンドウを探して前に出し、InitInstanceでFALSEを返して終わります。
目次