Windowsアプリの多重起動防止 ── 名前付きMutexと二重起動時のアクティブ化

· 更新日: · · 多重起動防止, Mutex, Windows, .NET, C#, SetForegroundWindow, Remote Desktop, 名前付きパイプ, Windows開発, 技術相談

更新履歴(4件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
既存インスタンスの前面化で、フォアグラウンド設定権限を`ASFW_ANY`で渡していたのを直しました。リファレンスは「このパラメーターが`ASFW_ANY`の場合、すべてのプロセスがフォアグラウンドウィンドウを設定できるようになる」と定義しています。渡す相手は1つに決まっているのに全プロセスへ配ることになり、しかもこの許可は次の入力または次の`AllowSetForegroundWindow`呼び出しまで有効なので、利用者がアプリを起動したまさにその瞬間に無関係な常駐プロセスがフォーカスを奪える窓が開きます。接続済みのパイプから`GetNamedPipeServerProcessId`で相手のプロセスIDを取り、その1つにだけ渡す形にしました。
多重起動防止の章を、スコープ・名前の占有・昇格レベルの罠に分けて整理しました。あわせて3つのスコープでMutexがいくつできるかの図、長いコード例を骨格と完全版に分ける構成、動作確認の手順を追加しています。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589933)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Windowsアプリの多重起動防止 ── 名前付きMutexと二重起動時のアクティブ化」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589933 https://staging.comcomponent.com/blog/single-instance-mutex-guide/

DOI(最新版)
10.5281/zenodo.21589933
DOI(この版)
10.5281/zenodo.21732922

「業務アプリを誤ってもう一度起動してしまい、同じファイルを2つのウィンドウで編集して片方の変更が消えた」「常駐ツールが二重に立ち上がって、同じイベントを二重処理していた」。デスクトップアプリの多重起動は地味なテーマですが、実務では意外と事故の原因になります。対策の骨格自体はシンプルで、名前付き Mutex(mutual exclusion = 相互排他の略で、同時に1つのスレッドだけが持てる「鍵」にあたる Windows のカーネルオブジェクトです)で「自分は最初のインスタンスか」を判定するだけです。

ところが、このシンプルな実装には周辺の落とし穴が付きまといます。リモートデスクトップ環境で防止が効かなくなる名前空間の罠、他の同期オブジェクトとは違う Mutex 特有の解放ルール、そして「見つかった既存インスタンスをどう前面に出すか」という Win32 のフォアグラウンドウィンドウ制限が絡む設計課題です。この記事では、名前付き Mutex による多重起動検出の基本から、実務で必要になる周辺の設計判断までを一通り整理します。

1. まず結論

  • 多重起動検出の基本形は new Mutex(true, name, out bool createdNew) です。同名の Mutex が既に存在していても例外は発生せず、createdNewfalse になるだけなので、この値で分岐します。1
  • 名前にプレフィックスを付けないと、既定で Local\(セッション限定)の名前空間に作られます。 リモートデスクトップで同じユーザーが複数セッションを持つ環境では、セッションごとに別の Mutex として扱われ、多重起動防止が効きません。セッションをまたいで1つに絞りたいなら Global\ プレフィックスが必須です。23
  • Mutex取得したスレッドと同じスレッドでしか解放できません。別スレッドから ReleaseMutex を呼ぶと ApplicationException になります。所有プロセスが解放せずに終了した場合は次に取得したスレッドに AbandonedMutexException が送出されますが、これは待機自体は成功しているという合図で、握りつぶさずに状態の整合性を確認してから使うのが正しい扱いです。45
  • 「既に起動している」と分かった後の本題は、既存インスタンスのウィンドウを前面に出すことです。SetForegroundWindow はフォアグラウンドプロセス以外からの呼び出しを OS が制限しており、単純に呼ぶだけでは失敗し、タスクバーボタンの点滅に留まります。6 今起動した2つ目のプロセスが持つ「フォアグラウンドを設定できる権限」を AllowSetForegroundWindow で既存インスタンスに明け渡す設計が定石です。7
  • 起動引数(開くべきファイルパスなど)を既存インスタンスへ渡すには、名前付きパイプで転送するのが定石です。手段の比較は「Windowsのプロセス間通信をどう選ぶか」に譲り、この記事では「検出 → 通知 → アクティブ化」の設計に絞ります。
  • コンソールアプリやサービスは、この記事の設計をそのまま持ち込む対象ではありません。サービスは SCM がそもそも同名サービスを1つしか起動しないため考え方が異なります(第8章)。

この記事の知識マップ

Windowsアプリの多重起動防止は、名前付きMutexの新規作成に成功したかを判定条件として利用する設計が基本です。Mutexはプレフィックスを付けないと既定でセッション限定のLocal名前空間に作られるため、リモートデスクトップで複数セッションを持つ環境では、Globalプレフィックスとユーザー識別子を組み合わせないと意図した単位で多重起動を防げません。ACLを明示したMutexは名前の占有を防げる一方、作成時と異なる昇格レベルでは生成・オープン自体が拒否されることがあります。検出後は名前付きパイプで通知し、AllowSetForegroundWindowで権限を委譲したうえでSetForegroundWindowを呼ぶ設計が、既存インスタンスを前面化する定石です。

Windowsアプリの多重起動防止の知識マップ名前付きMutexによる多重起動検出が、カーネルオブジェクト名前空間の選び方・ACLによる保護・既存インスタンスの前面化手段とどう関係するかを示す図利用する利用する前提とする両立しない防止する前提とする前提とするで構成できる利用するで確認できる前提とする前提とする前提とする利用する利用する両立しないで確認できる原因になり得るで構成できる名前付きMutex多重起動防止(シングルインスタンス化)カーネルオブジェクト名前空間(Global\/Local\)MutexSecurity/ACLによるMutex保護管理者権限Mutexの名前占有(スクワッティング).NET(Core以降)PipeOptions.CurrentUserOnly整合性レベル名前付きパイプGetNamedPipeServerProcessIdAllowSetForegroundWindowSetForegroundWindowタスクスケジューラの無人実行WinObjAbandonedMutexException

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全19件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. 名前付きMutexによる検出の基本

Mutex は本来、複数のスレッド・プロセスが同じ資源を同時に使わないようにするための排他制御用カーネルオブジェクトです。名前を付けて作成すると、その名前を知っている他のプロセスから同じオブジェクトを参照できます。多重起動検出では排他制御そのものではなく、この「名前の共有」だけを利用します。

using var mutex = new Mutex(initiallyOwned: true, name: MutexName, out bool createdNew);
if (!createdNew)
{
    // 既に起動している
    return;
}
// createdNew が true のときだけ、このスレッドが Mutex を所有している

ここで押さえておきたいのは次の2点です。

  • 同名の Mutex が既に存在しても例外にはなりません。 createdNewfalse が入り、既存のオブジェクトへの参照が返るだけです。分岐は必ず createdNew で行います。1
  • initiallyOwned: true は「新規作成できたとき」だけ有効です。 既に存在していた場合(createdNew == false)、このスレッドは自動的には所有者になりません。多重起動検出ではこの副作用は問題になりませんが、「2つ目のプロセスが誤って ReleaseMutex を呼んでしまう」事故を防ぐためにも、createdNewfalse の分岐では Mutex に一切触らず即座に処理を終えるのが安全です。1

名前は他社アプリと衝突しないよう、製品固有の GUID を埋め込んだ文字列にするのが定石です("KomuraSoft.MyApp.SingleInstance.{3F1E2B10-...}" のように)。パイプ名の占有(スクワッティング)と同様、名前付きカーネルオブジェクトの名前空間はマシン上の他のプロセスからも見えるため、推測されにくい名前にしておく意味があります。

3. Global\ と Local\ ── セッションをまたぐと何が起きるか

Windows のカーネルオブジェクト名前空間は、セッションごとに独立した名前空間と、システム全体で共有されるグローバル名前空間に分かれています。名前付きの Mutex を作るとき、プレフィックスを指定しなければ既定で呼び出し元のセッション名前空間(Local\)に作られ、Global\ を付けたときだけ全セッション共通の名前空間に作られます。3 .NET の Mutex クラスもこの挙動をそのまま踏襲しており、ドキュメントには「プレフィックスを指定しない名前付き Mutex は既定で Local\ を持つ」と明記されています。2

これが問題になるのは、次のような場面です。

  • 業務用の管理端末に、コンソールから直接ログインしたユーザーが、リモートデスクトップでも自分自身にもう1セッション接続している(運用ではよくあります)。
  • 同じユーザーが RDP 接続を切断・再接続を繰り返し、そのたびに新しいセッション ID が割り当てられる。

Local\ のまま(プレフィックスなし)で Mutex を作ると、これらは別セッション扱いになり、それぞれのセッションで多重起動防止が独立して働きます。つまり「同じユーザーなのに2つ目のセッションからは普通に起動できてしまう」という、多重起動防止が意図どおりに効かない不具合になります。逆に言えば、単一セッションのみを想定した一般的なデスクトップ利用では Local\(既定)のままでも実害はありません。

複数ユーザーが同居する端末の設計一般については「Windowsユーザープロファイル入門」でも扱っていますが、この記事のテーマに絞って言えば注意が必要です。Global\ はマシン上の全ユーザー・全セッションが共有する単一の名前空間であり、「1ユーザーにつき1インスタンス」を自動的には意味しません。 Global\KomuraSoft.MyApp.SingleInstance のように名前を固定したまま Global\ を付けるだけだと、ユーザー A が起動中はユーザー B の起動も同じ Mutex に阻まれてしまい、実質的に「マシン全体で1インスタンス」(第5章の表の3行目)になります。「1ユーザーにつき1インスタンス、ただしそのユーザーの複数セッションはまとめる」を実現したいなら、Global\ に加えて、名前にユーザー固有の識別子(SID など)を埋め込む必要があります。 逆にマシン全体で1インスタンスに絞りたい場合(全ユーザー共有が意図どおり)は、SIDを含めない Global\ のままで構いません。

4. Mutexの解放ルール ── 所有スレッドとAbandonedMutexException

Mutex には、SemaphoreAutoResetEvent などの他の同期オブジェクトにはない制約があります。取得したスレッドと同じスレッドでしか解放できないという、スレッド ID を強制するルールです。2 別スレッドから ReleaseMutex を呼ぶと ApplicationException(「呼び出し元のスレッドがミューテックスを所有していません」)が送出されます。4

.NET の async/await を使ったコードでは、await の後に処理が別のスレッドプールスレッドで再開されることがあります。「Mutex を取得した直後に非同期処理を挟み、その続きで解放する」ようなコードを書くと、この制約に静かに違反する可能性があるので注意してください。多重起動検出用途では取得も解放も同期的な短いコードに閉じるのが安全です。

もう1つの注意点が放棄(abandoned)です。Mutex を所有していたスレッドが、ReleaseMutex を呼ばずに終了する(プロセスがクラッシュした、未処理例外で落ちた、など)と、その Mutex は放棄された状態になります。次にこの Mutex を取得したスレッドには AbandonedMutexException が送出されますが、これは待機自体は成功しており、呼び出し元は既に Mutex の所有権を得ていることを示す例外です。5 多重起動検出だけの用途では通常発生しません(取得後に解放せず持ち続けたままプロセス終了まで使うため)が、同じ Mutex を他の排他制御にも流用している場合は、保護対象の状態が壊れている可能性を踏まえて次のように扱います。

try
{
    if (mutex.WaitOne(TimeSpan.FromSeconds(5)))
    {
        // 通常どおりの処理
    }
}
catch (AbandonedMutexException)
{
    // 待機は成功しており、このスレッドは既に所有権を得ている。
    // 保護対象の状態を検査してから使うか、安全に初期化し直す
}

「例外を無視するか、状態を検査して継続するか、諦めて異常終了するか」の切り分けは、当ブログの「想定外例外で終了すべきか継続すべきかの判断表」の考え方がそのまま当てはまります。AbandonedMutexException は「壊れた可能性がある範囲」を明示的に教えてくれる例外なので、握りつぶさずにその範囲(保護対象のデータ)だけを検査するのが基本線です。

なお、正常終了する経路では finally で明示的に ReleaseMutex を呼んでおくべきです。所有スレッドがプロセス終了で消えるとき、Mutex は「放棄された」ものとして扱われるため、明示的に解放しておかないと次回起動時に無用な AbandonedMutexException を発生させることになります。

5. 判断表 ── どのスコープでMutexを使うか

多重起動を「どの単位」で防ぎたいかによって、名前空間とパイプ側の設計は変わります。

5.1 3つのスコープの対応関係

名前の付け方によって「いくつの Mutex ができるか」が変わり、その個数がそのまま「同時に起動できるインスタンスの上限」になります。同じ端末にユーザーAが2セッション(コンソールとRDP)、ユーザーBが1セッションでログオンしている状況を例にすると、次のようになります。

マシン単位 ── Global プレフィックスのみMutex 端末に1つユーザーA セッション1ユーザーA セッション2ユーザーB セッション3ユーザー単位 ── Global プレフィックス と ユーザーSIDMutex ユーザーA用ユーザーA セッション1ユーザーA セッション2ユーザーB セッション3Mutex ユーザーB用セッション単位 ── プレフィックスなしMutex その1ユーザーA セッション1ユーザーA セッション2Mutex その2ユーザーB セッション3Mutex その3

矢印が同じ箱に集まっているところが、1インスタンスに絞られる範囲です。この例では、セッション単位なら合計3インスタンス、ユーザー単位なら合計2インスタンス、マシン単位なら合計1インスタンスまで起動できることになります。要件が「同じ人が2回開けないようにしたい」なのか「端末で1つだけ動かしたい」なのかで、選ぶ行が変わります。

5.2 判断表

単位 想定シナリオ Mutex名前空間 起動引数の受け渡し 注意点
セッション単位(既定) RDPを使わない、または「セッションごとに1つ」でよい通常のデスクトップ利用 プレフィックスなし(=Local\) CurrentUserOnlyに加え、パイプ名にセッションIDを含める RDP併用環境では、別セッションからの起動を防げない。同じユーザーが複数セッションを持つ場合、パイプ名にセッションIDを含めないと固定名の衝突が起きる(名前付きパイプはMutexのセッション名前空間の対象外のため)
ユーザー単位(セッション横断) 同じユーザーがコンソールとRDPを行き来する、RDP接続を繰り返す運用 Global\ + ユーザーSIDを名前に含める+MutexSecurityでACLを明示 CurrentUserOnly(ユーザーSIDで判定するためセッションをまたいでも機能する) 実務で最も必要とされるケース。SIDを含めずにGlobal\だけにすると、意図せずマシン単位(次の行)の挙動になる。SIDはユーザーの秘密情報ではないため、共有PC/RDS環境では他ユーザーによる名前スクワッティングも想定してACLで保護する
マシン単位(全ユーザー横断) ライセンス上マシンに1インスタンスまで、共有リソースを全ユーザーで排他したい Global\(ユーザー固有情報は含めない)+MutexSecurityでACLを明示 CurrentUserOnly を外し、PipeSecurity で許可ユーザーを明示 複数ユーザーが同時ログオンするリモートデスクトップサービス環境では、業務上望ましくない挙動になりがちなので要件と一致するか要確認。別ユーザーの起動引数をそのまま最初のユーザーのウィンドウへ転送しないこと。ファイルパスの漏えいや、意図しないユーザーのセッションで他人のドキュメントが開く事故につながるため、他ユーザーからの要求は拒否するか、UIを持たないブローカー経由に設計を変えるべき

5.3 通知用パイプのスコープをMutexに揃える

名前付きパイプは Mutex のような Local\/Global\ セッション名前空間の対象ではなく、既定でセッションをまたいで到達可能です。つまり CurrentUserOnly を付けなくても、パイプ名さえ一致していれば別セッションの既存インスタンスへ接続自体は届きます。CurrentUserOnly はここでは到達性の話ではなく認可の話で、「誰の接続を許可するか」を現在のユーザー(かつ同じ昇格レベル)に絞り込むアクセス制御です。付けなければ既定のセキュリティ記述子により Everyone にも読み取りアクセスが許可されるため、意図しない他ユーザーからの接続まで届いてしまいます。Global\ + SID の Mutex で「ユーザー単位・セッション横断」を実現する設計では、通知用パイプにも CurrentUserOnly を付け、Mutex と同じ「対象ユーザーのみ」に接続元を絞り込んでおくのが妥当です。幸い PipeOptions.CurrentUserOnly はセッション ID ではなくユーザーの SID(および昇格レベル)で判定するため、上の表の2行目(ユーザー単位・セッション横断)であればそのまま組み合わせて使えます。

5.4 名前の占有(スクワッティング)とACLの限界

マシン単位(表の3行目)で固定名の Global\ Mutex を使う場合は名前の占有(スクワッティング)に注意してください。名前付きMutexを使ってアプリを単一インスタンスに限定しようとする場合、悪意あるユーザーが先回りして同じ名前のMutexを作成し、アプリの起動を妨害できてしまいます。8 MutexSecurity(MutexAcl.Create)で作成時にACLを明示しておくと、自分が先に作成できた場合に、他ユーザーがそのMutexを乗っ取ったり不正に保持し続けたりするのを防げます。ただし、これは「後から邪魔されない」ための対策であって、「先に占有される」こと自体は防げない点に注意してください。ACLは自分がMutexを新規作成したときに初めて適用されるものなので、悪意あるユーザーが自分より先に同じ名前でMutexを作成していた場合、こちらの呼び出しは(ACLをいくら用意していても)相手が設定した既存オブジェクトを開きにいくだけになり、相手のACLに従うしかありません。名前そのものが推測困難であること自体には価値がありますが、ローカルコード実行権を持つ悪意あるユーザーを完全に締め出したいなら、Mutexの名前占有だけに頼らず、ユーザーごとに保護されたディレクトリ配下のロックファイルなど、別の排他制御と組み合わせる設計を検討してください。9

5.5 昇格レベルの罠 ── CurrentUserOnly と整合性レベル

CurrentUserOnly には見落としやすい制約があります。Windows では、ユーザーアカウントだけでなく昇格レベル(管理者として実行しているかどうか)まで一致していないと接続を許可しません。10 以前は「Mutex の名前はユーザーの SID だけで組み立てているため、検出自体は昇格の有無によらず動く」と考えがちですが、ACLを明示した Mutex を使う場合はここも昇格の影響を受けます。 Windowsは既定で、高い整合性レベル(管理者として実行)のプロセスが作成したオブジェクトに高い整合性レベルのラベルを付与し、より低い整合性レベルのプロセスからの書き込み系アクセスを拒否します。.NETの Mutex/MutexAcl.Create は内部的に SYNCHRONIZEMUTEX_MODIFY_STATE に加えて DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER(STANDARD_RIGHTS_REQUIRED)まで要求するため、「管理者として実行」で1つ目を起動し、通常権限で2つ目を起動した場合、Mutexの生成・オープン呼び出し自体が UnauthorizedAccessException を送出することがあります。つまり第7章で書いた「通知パイプだけがACLで弾かれ、多重起動防止自体は成功する」という前提が崩れ、検出のより手前で例外が飛ぶ可能性があるということです。この経路も「別インスタンスが既に動いている、あるいは昇格レベルが違うため通常の手段では判定できない」ことを示すシグナルとして扱い、Mutex/MutexAcl.Create の呼び出し自体を try/catch (UnauthorizedAccessException) で囲み、例外時は起動を諦めて静かに終了する(あるいは第7章の「通知は届かなくてもよい」と同様、多重起動防止としては安全側に倒す)実装にしておくのが妥当です。昇格レベルをまたいだ通知まで必要な場合は、CurrentUserOnly を使わず PipeSecurity でユーザー SID ベースの ACL を明示的に組む設計に切り替えてください。

6. 既存インスタンスを前面に出す ── SetForegroundWindowの制限

Mutex で「既に起動している」と分かった後、多くのアプリは既存インスタンスのウィンドウを前面に出したいはずです。ここで単純に既存プロセスに対して SetForegroundWindow を呼んでも、多くの場合は失敗します。

Windows は、どのプロセスがフォアグラウンドウィンドウを設定できるかを厳しく制限しています。公式ドキュメントによれば、呼び出し元プロセスが次のいずれかに該当しない限り、SetForegroundWindow はウィンドウを実際には前面化せず、タスクバーボタンを点滅させるだけに留まります。6

  • 呼び出し元プロセス自身が現在のフォアグラウンドプロセスである
  • 呼び出し元プロセスがフォアグラウンドプロセスによって起動された
  • 呼び出し元プロセスが直近の入力イベントを受け取った
  • 現在フォアグラウンドウィンドウが存在しない
  • フォアグラウンドプロセスまたは呼び出し元プロセスがデバッグ中である

多重起動検出のシナリオでは、既存インスタンス(バックグラウンドで動いている1つ目のプロセス)はこの条件をまず満たしません。一方、今ユーザーがダブルクリックして起動した2つ目のプロセスは、直近の入力イベントを受け取ってすぐの状態であることが多く、フォアグラウンドを設定できる権限を持っています。この非対称性を利用するのが実務での定石です。

AllowSetForegroundWindow は、フォアグラウンドを設定できるプロセスが、その権限をプロセスIDで指定した相手へ譲渡するための API です。7 2つ目のプロセスが自分の持つ権限を既存インスタンスへ渡してから、名前付きパイプでアクティブ化を要求すれば、既存インスタンス側の SetForegroundWindow 呼び出しが成功するようになります。

ここで ASFW_ANY(-1)を渡さないでください。リファレンスは「このパラメーターが ASFW_ANY の場合、すべてのプロセスがフォアグラウンドウィンドウを設定できるようになる」と定義しています。7 渡す相手は1つに決まっているのに、全プロセスへ配ることになります。この許可は「次にユーザーが入力を発生させるか、次にどこかのプロセスが AllowSetForegroundWindow を呼ぶまで」有効なので、7 利用者がアプリを起動したまさにその瞬間に、無関係な常駐プロセスがフォーカスを奪える窓が開きます。自分のアプリの前面化のために、システム全体の保護を一時的に外しているわけです。

渡すべき相手 ── 既存インスタンスのプロセスID ── は、接続したパイプから取れます。GetNamedPipeServerProcessId にクライアント側のパイプハンドルを渡すと、そのパイプのサーバー側プロセスIDが返ります。11 「いま自分がつながっている相手」なので、名前やウィンドウから探し当てる必要はありません。

[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(uint dwProcessId);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetNamedPipeServerProcessId(
    SafePipeHandle pipe, out uint serverProcessId);

// 接続済みのパイプから相手のプロセスIDを取り、その1つにだけ権限を渡す。
// 取れなかったときは前面化を諦める(通知は届くので、主目的は達成できている)
if (GetNamedPipeServerProcessId(pipe.SafePipeHandle, out uint serverProcessId))
{
    AllowSetForegroundWindow(serverProcessId);
}

これでも救えないケース(タスクスケジューラ経由の起動など、2つ目のプロセス自身にもフォアグラウンド権限がない場合)も残ります。その場合はタスクバーの点滅で通知するだけに留め、無理に前面化しようとしないのが妥当な設計です。ユーザーへの通知手段としてはトースト通知や、ウィンドウの WindowStateMinimized から Normal に戻すところまでは常に行い、最終的な前面化は「できれば行う」程度の位置づけにしておくと破綻しません。

7. 起動引数を既存インスタンスへ渡す ── 名前付きパイプ

多重起動検出だけでなく、「開くべきファイルを引数付きで起動したら、既存インスタンスがそのファイルを開く」という要件もよくあります。この用途で WM_COPYDATA(ウィンドウメッセージでデータを送る古典的な手段)も選択肢としては存在しますが、ウィンドウハンドルの取得やメッセージのマーシャリングの面倒さに対して得るものが少なく、新規設計では名前付きパイプを使うのが素直です。

設計はシンプルで、Mutex によって「既に起動している」と分かった2つ目のプロセスが、既存インスタンスが待ち受けている名前付きパイプへ接続し、コマンドライン引数を JSON などでシリアライズして送るだけです。パイプ名の占有(スクワッティング)対策や CurrentUserOnly によるアクセス制御など、名前付きパイプ自体の実装上の注意点は「Windowsのプロセス間通信をどう選ぶか」の名前付きパイプの節にまとめてあるので、そちらを踏襲してください。多重起動検出という文脈に特有の注意点は次の1点だけです。

  • 通知の送信に失敗しても、多重起動防止としては成功として扱ってよい。 既存インスタンスが終了処理中でパイプサーバーを閉じていたなど、タイミングの問題で通知が届かないことはあります。この場合でも「2つ目のプロセスを起動させない」という主目的は達成できているので、通知の失敗を理由にエラーダイアログを出す必要はありません。

8. コンソールアプリ・サービスとの違い

この記事の設計は、ウィンドウを持つデスクトップアプリを前提にしています。コンソールアプリやバッチツールでは「多重起動を防ぐ」より「多重実行されても安全に共存できるようにする」設計のほうが現実的なことが多く、これは実質的にファイル連携の排他制御の話に帰着します。

Windows サービスはさらに事情が異なります。サービスコントロールマネージャー(SCM)は、同じサービス名のサービスをそもそも同時に2つ起動しないため、この記事の Mutex による検出は基本的に不要です。「UI アプリ + 常駐サービス」のような構成では、UI 側だけこの記事の設計を使い、サービス側の多重起動・多重実行に関する考え方はまた別の話になります。サービスの作り方や設計の勘所は別記事に譲ります。

9. 実装例 ── Mutexによる検出とアクティブ化要求

9.1 必要なもの

コードに入る前に、前提を明示しておきます。

項目 内容
対象フレームワーク .NET 8 以降の Windows 向け TFM(net8.0-windows など)。WPF を使うのでプロジェクトファイルに <UseWPF>true</UseWPF> が必要です
NuGet パッケージ System.Threading.AccessControlMutexAcl.CreateMutexSecurity はこのパッケージのアセンブリ System.Threading.AccessControl.dll に含まれており、既定の参照には入っていません12
使う名前空間 System.IO.Pipes / System.Runtime.InteropServices / System.Security.AccessControl / System.Security.Principal / System.Text.Json
対象OS Windows のみ。Global\ 名前空間・AllowSetForegroundWindowCurrentUserOnly はいずれも Windows 固有の仕組みです
WinForms の場合 Application.Current.Dispatcher.InvokeControl.Invoke に、Application.Current.MainWindow を対象フォームの参照に置き換えれば、ほぼそのまま使えます

パッケージは dotnet add package System.Threading.AccessControl で追加します。なお、ACL を明示せず素の new Mutex(...) で済ませる場合(第5.2節の表の1行目・セッション単位)は、このパッケージは不要です。

9.2 骨格 ── 検出・通知・サーバー起動

コードが長いので、先に全体の流れだけを示します。実質的には次の4ステップしかありません。NotifyRunningInstanceAsync ShowMainWindow StartActivationServer の中身は第9.3節にあります。

// args は Main のコマンドライン引数
string mutexName = $@"Global\KomuraSoft.MyApp.SingleInstance.{WindowsIdentity.GetCurrent().User}";
var security = new MutexSecurity();
security.AddAccessRule(new MutexAccessRule(
    WindowsIdentity.GetCurrent().User!, MutexRights.FullControl, AccessControlType.Allow));

// 1. 検出 ── 自分が最初のインスタンスかどうかを createdNew で判定する
var mutex = MutexAcl.Create(
    initiallyOwned: true, name: mutexName, createdNew: out bool createdNew, mutexSecurity: security);

if (!createdNew)
{
    // 2. 通知 ── 2つ目なら、既存インスタンスへ引数を送って自分は終了する
    NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
    return;
}

// 3. 起動 ── 1つ目なら、ウィンドウを表示してからパイプサーバーを立てる
ShowMainWindow();
StartActivationServer();

// 4. 後始末 ── 取得したのと同じスレッドから解放してから終了する
mutex.ReleaseMutex();

順序で1つだけ譲れないのが 3. で、ウィンドウを表示してからパイプサーバーを起動する点です。逆にすると、サーバーが受け取ったアクティブ化要求の宛先がまだ存在せず、通知が静かに消えます。

9.3 完全版

上の骨格に、第2〜7章で挙げた注意点をすべて反映したものが次のコードです。WPF を想定していますが、WinForms でも第9.1節の表のとおり置き換えるだけでほぼそのまま使えます。

using System.IO.Pipes;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;

public static class Program
{
    // 製品固有のGUIDを埋め込み、他アプリと名前が衝突しないようにする。
    // 「ユーザー単位・セッション横断」で1インスタンスに絞りたいので、Global\に加えて
    // ユーザーのSIDを名前へ埋め込む。SIDを含めずGlobal\だけにすると、全ユーザーが
    // 同じMutexを共有してしまい「マシン全体で1インスタンス」に変わる(第3章・第5.1節)
    private static readonly string MutexName =
        $@"Global\KomuraSoft.MyApp.SingleInstance.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";
    // 通知用パイプもMutexと同じスコープ(ユーザー単位)に揃える。SIDを含めず
    // 固定名のままだと、別ユーザーが同じ名前でサーバーを立てたときに
    // 衝突・混線し得る(第5章、CurrentUserOnlyはACLを絞るだけで名前は分けない)
    private static readonly string PipeName =
        $"KomuraSoft.MyApp.Activate.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";

    [STAThread]
    private static void Main(string[] args)
    {
        // 現在のユーザーにのみフルコントロールを許可するACLを明示しておくと、
        // 自分が先に作成できた場合に他ユーザーからの乗っ取り・妨害を防げる
        // (NuGetパッケージ System.Threading.AccessControl が必要)。
        // ただしこれは「先に占有される」こと自体への対策にはならない(第5.4節)
        var mutexSecurity = new MutexSecurity();
        mutexSecurity.AddAccessRule(new MutexAccessRule(
            WindowsIdentity.GetCurrent().User!, MutexRights.FullControl, AccessControlType.Allow));

        bool createdNew;
        Mutex mutex;
        try
        {
            // createdNew が true のときだけ、この呼び出しでMutexを取得できている
            mutex = MutexAcl.Create(
                initiallyOwned: true, name: MutexName, createdNew: out createdNew, mutexSecurity: mutexSecurity);
        }
        catch (UnauthorizedAccessException)
        {
            // 同じユーザーでも昇格レベルが異なると、既存Mutexのオープン自体が
            // 拒否されることがある(第5.5節)。「昇格レベル違いの別インスタンスが
            // 既にいる」とみなし、通知は諦めて安全側(起動しない)に倒す
            return;
        }
        using var _ = mutex;

        if (!createdNew)
        {
            // 既に起動している。既存インスタンスへ通知して自分は終了する
            NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
            return;
        }

        try
        {
            // App.xamlのStartupUriは削除しておくこと。残したままだと、
            // ここで手動生成したMainWindowに加えてStartupUri側のウィンドウも
            // 自動生成・表示されてしまい(app.MainWindowもそちらに上書きされる)、
            // ウィンドウが2つ開いた挙句アクティブ化要求が届かないほうを向く事故になる
            var app = new App();
            app.InitializeComponent();
            var mainWindow = new MainWindow();
            app.MainWindow = mainWindow;
            mainWindow.Show();

            // MainWindowの生成・表示が終わってからパイプサーバーを起動する。
            // 逆順だと、ウィンドウがまだ無い状態でアクティブ化要求が届き、
            // ActivateMainWindow呼び出しが失敗し得る(例外は下のcatch-allで
            // 握りつぶされ、通知が単に消えるだけになる)。この間に届いた要求は
            // 「サーバー未起動→接続失敗」としてNotifyRunningInstanceAsync側の
            // ベストエフォート設計(第7章)にそのまま乗せる
            StartActivationServer();

            app.Run();
        }
        finally
        {
            // 所有スレッド(このスレッド)から明示的に解放してから終了する。
            // 解放せずに終了すると、次回起動時にAbandonedMutexExceptionの
            // 原因になる(第4章)
            mutex.ReleaseMutex();
        }
    }

    [DllImport("user32.dll")]
    private static extern bool AllowSetForegroundWindow(uint dwProcessId);

    // 接続中のパイプから、サーバー側(=既存インスタンス)のプロセスIDを取る
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool GetNamedPipeServerProcessId(
        SafePipeHandle pipe, out uint serverProcessId);

    private static async Task NotifyRunningInstanceAsync(string[] args)
    {
        try
        {
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
            using var pipe = new NamedPipeClientStream(
                ".", PipeName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
            await pipe.ConnectAsync(cts.Token);

            // 自分(今起動した2つ目のプロセス)は直近の入力イベントを受けて
            // 起動したばかりで、フォアグラウンド設定権限を持っていることが多い。
            // その権限を、いま接続した相手 ── 既存インスタンス ── にだけ渡し、
            // 向こうのSetForegroundWindowを成功させる(第6章)。
            // ASFW_ANY(-1)を渡すと「すべてのプロセス」が対象になる
            if (GetNamedPipeServerProcessId(pipe.SafePipeHandle, out uint serverProcessId))
            {
                AllowSetForegroundWindow(serverProcessId);
            }

            byte[] payload = JsonSerializer.SerializeToUtf8Bytes(new ActivateRequest(1, args));
            await pipe.WriteAsync(payload, cts.Token);
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or OperationCanceledException)
        {
            // 既存インスタンスが終了処理中で応答しなかった(IOException)、
            // 昇格レベルが異なりCurrentUserOnlyの認可に失敗した(UnauthorizedAccessException。
            // 第5.5節)など、通知が届かない理由を区別せず、
            // 多重起動防止としての主目的(2つ目を起動させない)は達成できている(第7章)
        }
    }

    private static void StartActivationServer()
    {
        // 1メッセージの上限。同一ユーザーの古いヘルパーや壊れたクライアントが
        // 際限なく送り続けても、サーバー側のメモリを守る
        const int MaxPayloadBytes = 64 * 1024;

        _ = Task.Run(async () =>
        {
            while (true)
            {
                try
                {
                    // コンストラクター自体もtryの中に入れる。maxNumberOfServerInstancesが
                    // 1のため、直前の接続の後始末が終わっていないタイミングなどで
                    // ここがIOExceptionを送出することがあり、tryの外に置くとこの
                    // 1回の失敗でバックグラウンドタスクごと止まってしまう
                    using var pipe = new NamedPipeServerStream(
                        PipeName, PipeDirection.In, 1,
                        PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

                    await pipe.WaitForConnectionAsync();

                    // maxNumberOfServerInstancesが1なので、この接続が応答しない
                    // クライアントに固定されると、以降の正規の起動要求を一切
                    // 受け付けられなくなる。接続1回分に上限時間を設ける
                    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                    using var ms = new MemoryStream();
                    var buffer = new byte[4096];
                    int n;
                    while ((n = await pipe.ReadAsync(buffer, cts.Token)) > 0)
                    {
                        ms.Write(buffer, 0, n);
                        if (ms.Length > MaxPayloadBytes)
                            throw new IOException("ペイロードが上限サイズを超えました。");
                    }

                    var req = JsonSerializer.Deserialize<ActivateRequest>(ms.ToArray());
                    // Version だけでなく Args も検査する。旧バージョンの送信元や
                    // 手作りの不正なペイロードが {"Version":1} のようにArgsを
                    // 欠いたJSONを送ってくると、Argsはnullのままデシリアライズされる
                    if (req is { Version: 1, Args: not null })
                    {
                        // UIの操作はUIスレッドに戻して行う
                        Application.Current.Dispatcher.Invoke(() => ActivateMainWindow(req.Args));
                    }
                }
                catch (Exception)
                {
                    // 接続が切れた、上限時間・上限サイズを超えた、ペイロードが壊れている、
                    // ディスパッチ中に想定外の例外が起きた、など理由を問わず、
                    // この接続1回分の異常として握りつぶす。このタスク自体を死なせると
                    // 以降すべての通知を受け付けられなくなるため、ループは必ず継続する。
                    // ただしパイプの構築自体がawait前に即座に失敗し続けるケース
                    // (単一インスタンス枠を他プロセスが握っている等)でホットスピン
                    // しないよう、次のループ前に必ず一呼吸置く
                    await Task.Delay(TimeSpan.FromSeconds(1));
                }
            }
        });
    }

    [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    private static void ActivateMainWindow(string[] args)
    {
        var window = Application.Current.MainWindow;
        if (window is null) return;

        if (window.WindowState == System.Windows.WindowState.Minimized)
            window.WindowState = System.Windows.WindowState.Normal;
        window.Show();
        window.Activate();

        // WPFのActivate()は内部でSetForegroundWindowを呼ぶが、制限(第6章)で
        // 失敗することがあるため、AllowSetForegroundWindow済みの状態で明示的にも呼ぶ
        var hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle;
        SetForegroundWindow(hwnd);

        if (args.Length > 0)
        {
            // args[0] を開くべきファイルパスとして扱う、などアプリ固有の処理
        }
    }

    private sealed record ActivateRequest(int Version, string[] Args);
}

設計判断を3つ補足します。

  • Global\ に付け足すユーザー識別子は、名前が変わらないものを選ぶ。 WindowsIdentity.GetCurrent().User が返す SecurityIdentifier はユーザー名と違って改名の影響を受けず、ToString()S-1-5-21-... 形式の文字列になります。13 ユーザー名を直接埋め込むと、アカウント名の変更やドメイン移行で多重起動防止が効かなくなる事故につながります。
  • Mutexの解放とパイプサーバーの停止は、アプリの終了処理の一部として明示的に行う。 上の例では finallyReleaseMutex を呼んでいますが、実アプリではウィンドウを閉じる処理の中でキャンセルトークンを使ってパイプサーバーのループも止めるようにしてください。
  • version フィールドは最初から入れる。 起動引数の形式を将来変える可能性は十分あります。「知らないバージョンは無視する」判定を最初から入れておくと、旧バージョンの実行ファイルが残っている端末でも安全に振る舞えます。

10. 動作確認の手順

多重起動防止は「効いていないことに気づきにくい」機能です。実装したら、必ず次の順に手を動かして確認してください。1〜3は必須、4以降は第5.2節の表で選んだスコープに応じて実施します。

  1. 同一セッションでの二重起動 ── アプリを起動したまま、もう一度実行ファイルをダブルクリックします。2つ目のウィンドウが開かず、既存のウィンドウが前面に来れば成功です。タスクマネージャーの「詳細」タブで、対象の実行ファイルが1行しか残っていないことも確認します。
  2. 最小化からの復帰 ── 1つ目を最小化した状態で 1. を繰り返します。最小化されたまま復帰しない場合は、第9.3節の ActivateMainWindow にある WindowState を戻す処理が呼ばれていません。
  3. 起動引数の受け渡し ── コマンドプロンプトから MyApp.exe C:\temp\sample.txt のように引数付きで2つ目を起動し、既存インスタンス側でそのパスが処理されることを確認します。ここが動かない場合、パイプ名の不一致か、パイプサーバーの起動順序(第9.2節)を疑います。
  4. セッションをまたぐ確認(ユーザー単位・マシン単位を選んだ場合) ── 同じユーザーでコンソールログオンとリモートデスクトップ接続の2セッションを作り、両方から起動します。現在のセッション一覧とIDは qwinsta コマンドで確認できます。14 プレフィックスなしのままだと、ここで2つ起動できてしまいます(第3章)。これが多重起動防止の不具合として最も多く報告される形です。
  5. ユーザーをまたぐ確認(マシン単位を選んだ場合) ── 別ユーザーでもう1セッション作り、先のユーザーが起動中に起動できないことを確認します。Global\ に SID を含めているかどうかで結果が変わるので、第5.1節の図と突き合わせてください。
  6. 昇格レベル違いの確認 ── 1つ目を通常権限で起動したまま、2つ目を「管理者として実行」で起動します。第5.5節で書いた UnauthorizedAccessException の経路に落ちる場合があり、そのときに2つ目が静かに終了する(エラーダイアログを出して落ちない)ことを確認します。
  7. 放棄されたMutexの確認 ── 1つ目をタスクマネージャーの「タスクの終了」で強制終了させてから、もう一度起動します。普通に起動できれば正常です。ここで起動できないなら、Mutex を保持しているスレッドやプロセスが残っています。同じ Mutex を他の排他制御にも流用している場合は、AbandonedMutexException の扱い(第4章)もあわせて確認します。

作った Mutex が実際にどの名前で存在しているかを目で確かめたい場合は、Sysinternals の WinObj でオブジェクトマネージャーの名前空間を開き、名前で探すのが確実です。15Global\ を付けたつもりが付いていなかった」という凡ミスは、これで一発で分かります。

11. まとめ

Windowsアプリの多重起動防止は、new Mutex(true, name, out createdNew) という数行だけを見れば単純です。しかし実務で事故なく機能させるには、Global\/Local\ によるセッション可視性の違い、Mutex 特有のスレッド所有権制約と AbandonedMutexException の扱い、そして SetForegroundWindow の制限を踏まえたアクティブ化設計まで、周辺知識をひととおり押さえておく必要があります。

実装の順序としては、まず「どの単位(セッション・ユーザー・マシン)で多重起動を防ぎたいか」を決め(第5章)、それに合わせて Mutex の名前空間と通知用パイプのスコープを揃える。そのうえで、既存インスタンスの前面化は AllowSetForegroundWindow で権限を委譲する定石を使い、それでも失敗するケースは無理に前面化せず通知に留める、という割り切りを持っておくと実装が破綻しません。要件が「1ユーザー1インスタンスか、マシン全体で1インスタンスか」で迷う場合は、運用環境(RDP併用の有無、複数ユーザーの同時ログオンの有無)を先に確認することをおすすめします。

関連記事

関連する相談領域

合同会社小村ソフトでは、多重起動防止やウィンドウ制御を含む Windows デスクトップアプリの設計・実装、リモートデスクトップ環境特有の不具合の原因調査、既存アプリの設計レビューを扱っています。

参考リンク

  1. Microsoft Learn, Mutex Constructor. 名前付きMutexが既に存在する場合はcreatedNewがfalseになり例外は発生しないこと、initiallyOwnedによる初期所有権はcreatedNewがtrueのときだけ有効なことについて。  2 3

  2. Microsoft Learn, Mutex Class. 名前付きMutexにプレフィックスを指定しない場合は既定でLocal\になること、Global\/Local\によるターミナルサービスセッション間の可視性の違い、Mutexがスレッド単位で所有権を強制する(他の同期オブジェクトと異なる)ことについて。  2 3

  3. Microsoft Learn, Kernel Object Namespaces. セッションごとに独立した名前空間とグローバル名前空間の構造、Global\/Local\プレフィックスによる名前空間の指定方法について。  2

  4. Microsoft Learn, Mutex.ReleaseMutex Method. 所有していないスレッドがReleaseMutexを呼ぶとApplicationExceptionが送出されること、スレッドがMutexを解放せずに終了した場合はMutexが放棄された状態になることについて。  2

  5. Microsoft Learn, AbandonedMutexException Class. 放棄されたMutexを次に取得したスレッドにAbandonedMutexExceptionが送出されること、待機自体は成功しており呼び出し元がMutexの所有権を得ていることについて。  2

  6. Microsoft Learn, SetForegroundWindow function. フォアグラウンドウィンドウを設定できるプロセスの条件と、条件を満たさない場合はタスクバーボタンの点滅に留まることについて。  2

  7. Microsoft Learn, AllowSetForegroundWindow function. フォアグラウンドウィンドウを設定できるプロセスが、その権限をdwProcessIdで指定したプロセスへ譲渡できること、「このパラメーターがASFW_ANYの場合、すべてのプロセスがフォアグラウンドウィンドウを設定できるようになる」こと、譲渡した権限は「次にユーザーが入力を発生させたとき(その入力がそのプロセスへ向けられている場合を除く)、または次にどれかのプロセスがAllowSetForegroundWindowを呼んだとき(前回と同じプロセスが指定された場合を除く)」に失われることについて。  2 3 4

  8. Microsoft Learn, CreateMutexW function (synchapi.h). 名前付きMutexで単一インスタンスに制限する場合、悪意あるユーザーが先に同じ名前のMutexを作成してアプリの起動を妨害できること、対策としてランダムな名前や1ユーザー1インスタンスならユーザープロファイル配下のロックファイルを使う代替案について。 

  9. Microsoft Learn, Mutexes. 名前付きシステムMutexはOS全体から可視でありグローバルであるため、作成時点からアクセス制御セキュリティで保護することが推奨されること、MutexSecurityによるアクセス制御について。 

  10. Microsoft Learn, PipeOptions Enum. CurrentUserOnlyがWindows上ではユーザーアカウントに加えて昇格レベルまで検証することについて。 

  11. Microsoft Learn, GetNamedPipeServerProcessId function. 指定した名前付きパイプについて、サーバー側のプロセス識別子を取得できることについて(Windows Vista以降)。 

  12. Microsoft Learn, MutexAcl.Create Method. MutexAcl.Create が名前空間 System.Threading・アセンブリ System.Threading.AccessControl.dll・NuGet パッケージ System.Threading.AccessControl で提供されること、Global\/Local\ プレフィックスによる名前空間の指定と既定が Local\ であること、既定では名前付きMutexが作成者以外にも開けるためアクセス制限には MutexSecurity を渡す必要があることについて。 

  13. Microsoft Learn, WindowsIdentity.User Property. ユーザーのセキュリティ識別子(SID)を返すプロパティであり、SIDがすべてのWindows NT実装でユーザーまたはグループを一意に識別することについて。 

  14. Microsoft Learn, qwinsta. リモートデスクトップセッションホスト上のセッション一覧(セッション名・ID・状態)を表示するコマンドについて。 

  15. Microsoft Learn, WinObj - Sysinternals. WinObjがNTオブジェクトマネージャーの名前空間を表示するツールであり、名前付きカーネルオブジェクトを閲覧できることについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

C#でアプリの多重起動を防止するにはどうすればよいですか?
基本形は new Mutex(true, name, out bool createdNew) です。同名のMutexが既に存在していても例外は発生せず、createdNewがfalseになるだけなので、この値で分岐して2つ目のプロセスを終了させます。名前は他社アプリと衝突しないよう、製品固有のGUIDを埋め込んだ文字列にするのが定石です。createdNewがfalseの分岐ではMutexに一切触らず即座に処理を終えるのが安全です。
リモートデスクトップ環境で多重起動防止が効かないのはなぜですか?
Mutexの名前にプレフィックスを付けないと、既定でLocal\(セッション限定)の名前空間に作られるためです。同じユーザーがコンソールとRDPで複数セッションを持つ環境では、セッションごとに別のMutexとして扱われ、2つ目のセッションからは普通に起動できてしまいます。セッションをまたいで1つに絞るにはGlobal\プレフィックスが必須です。ただしGlobal\だけだと全ユーザー共有のマシン単位になるため、1ユーザー1インスタンスにしたい場合は名前にユーザーのSIDも埋め込みます。
既存インスタンスのウィンドウを前面に出すにはどうすればよいですか?
単純にSetForegroundWindowを呼ぶだけでは、OSの制限により多くの場合失敗し、タスクバーボタンの点滅に留まります。定石は、今ユーザーがダブルクリックして起動した2つ目のプロセスが持つフォアグラウンド設定権限を、AllowSetForegroundWindowで既存インスタンスへ明け渡してから、名前付きパイプでアクティブ化を要求する設計です。譲渡先はプロセスIDで指定し、ASFW_ANY(すべてのプロセスに許可)は使いません。相手のプロセスIDは、接続したパイプからGetNamedPipeServerProcessIdで取れます。それでも失敗するケース(タスクスケジューラ経由の起動など)は無理に前面化せず、通知に留めるのが妥当です。
AbandonedMutexExceptionはどう扱えばよいですか?
Mutexを所有していたスレッドがReleaseMutexを呼ばずに終了(プロセスのクラッシュなど)すると、次に取得したスレッドにAbandonedMutexExceptionが送出されます。これは待機自体は成功しており、呼び出し元が既に所有権を得ていることを示す例外です。握りつぶさず、保護対象の状態の整合性を確認してから使うのが正しい扱いです。正常終了する経路ではfinallyで明示的にReleaseMutexを呼んでおくと、次回起動時の無用な発生を防げます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る