C# Mutexの使い方を基礎から解説|二重起動防止・排他制御・よくあるエラー対策

はじめに

C#でアプリケーションを開発していると、「同じアプリを二重起動させたくない」「複数プロセスから同じファイルへ同時に書き込みたくない」「ある処理を同時に1つだけ実行したい」といった場面があります。こうした排他制御でよく使われる仕組みがMutexです。

Mutexは、C#のSystem.Threading名前空間に用意されている同期オブジェクトです。特に、プロセスをまたいだ排他制御や、Windows Forms・WPF・Consoleアプリの二重起動防止でよく利用されます。

一方で、Mutexlockより扱いが重く、ReleaseMutexの呼び忘れや、所有していないスレッドからの解放、名前付きMutexの設計ミスなどでエラーが起きやすい点にも注意が必要です。

この記事では、C# Mutexの基本概念から、WaitOneReleaseMutexの使い方、二重起動防止の実装例、名前付きMutexの注意点、よくあるエラーと対策までを基礎から解説します。

1. C#のMutexとは?基本概念と使いどころ

1-1. Mutexの役割は「プロセスをまたいだ排他制御」

Mutexは「Mutual Exclusion」、つまり相互排他を実現するための同期プリミティブです。簡単に言うと、ある共有リソースを同時に1つのスレッド、または1つのプロセスだけが使えるようにする仕組みです。

C#のMutexは、同一プロセス内の複数スレッドだけでなく、名前付きMutexを使うことで別プロセス間の同期にも利用できます。Microsoftの公式ドキュメントでも、Mutexはプロセス間同期にも使用できる同期プリミティブと説明されています。

たとえば、次のようなケースでMutexが役立ちます。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
using var mutex = new Mutex(false, "SampleMutex");

mutex.WaitOne();

try
{
Console.WriteLine("この処理は同時に1つだけ実行されます。");
Thread.Sleep(3000);
}
finally
{
mutex.ReleaseMutex();
}
}
}

この例では、SampleMutexという名前のMutexを使っています。同じ名前のMutexを使う別プロセスが存在する場合、同時に保護された処理へ入ることを防げます。

1-2. lockとの違い:スレッド内排他かプロセス間排他か

C#で排他制御というと、まずlockを思い浮かべる人も多いでしょう。lockは、同一プロセス内の複数スレッドを制御する場合に便利です。

C#
private static readonly object _lockObject = new object();

lock (_lockObject)
{
// 同一プロセス内の複数スレッドからの同時実行を防ぐ
}

一方、Mutexはプロセス間で共有できる点が大きな違いです。

lockは軽量で高速ですが、別プロセスからは見えません。たとえば、同じアプリケーションを2つ起動した場合、片方のプロセス内で使っているlockオブジェクトは、もう片方のプロセスから参照できません。

そのため、同一プロセス内のスレッド排他ならlock、複数プロセス間の排他や二重起動防止ならMutex、という使い分けが基本です。

1-3. Semaphore・SemaphoreSlimとの違い

SemaphoreSemaphoreSlimも同期制御に使われますが、役割はMutexとは少し異なります。

Mutexは基本的に「同時に1つだけ」を許可する排他制御です。一方、Semaphoreは「同時に最大N個まで」を許可できます。

たとえば、最大3つの処理まで同時実行を許可したい場合はSemaphoreSemaphoreSlimが向いています。

C#
var semaphore = new SemaphoreSlim(3);

await semaphore.WaitAsync();

try
{
// 最大3つまで同時実行
}
finally
{
semaphore.Release();
}

また、SemaphoreSlimは軽量で非同期処理との相性がよく、await semaphore.WaitAsync()のように書けます。対して、MutexWaitOneは同期的にスレッドをブロックするため、async/await中心のコードでは慎重に使う必要があります。

1-4. C#でMutexが必要になる代表例

C#でMutexが必要になる代表例は次のとおりです。

1つ目は、アプリケーションの二重起動防止です。Windows FormsやWPFアプリで、同じアプリを複数起動させたくない場合に名前付きMutexがよく使われます。

2つ目は、複数プロセスから同じファイルへアクセスする場合です。ログファイル、設定ファイル、一時ファイルなどに複数プロセスから同時書き込みすると、内容が壊れる可能性があります。

3つ目は、外部リソースへのアクセス制御です。たとえば、同じデバイス、同じ共有メモリ、同じ一時ディレクトリなどを複数プロセスで扱う場合、Mutexによって同時アクセスを防げます。

4つ目は、バッチ処理や常駐アプリの多重実行防止です。タスクスケジューラから起動されるConsoleアプリで、前回の処理が終わる前に次回分が起動されるのを防ぎたい場合にも有効です。

2. C# Mutexの基本的な使い方

2-1. Mutexクラスの基本構文

C#でMutexを使うには、System.Threading名前空間を使用します。

C#
using System.Threading;

最もシンプルな作成方法は次のとおりです。

C#
var mutex = new Mutex();

これは名前のないローカルMutexです。同一プロセス内で共有リソースを排他制御する場合に使えます。

名前付きMutexを作る場合は、次のように名前を指定します。

C#
var mutex = new Mutex(false, "MyApplicationMutex");

第1引数のfalseは、作成したスレッドが最初からMutexの所有権を取得するかどうかを表します。falseの場合、作成時点では所有権を要求しません。

二重起動防止では、次のようにcreatedNewを使う構文がよく使われます。

C#
bool createdNew;
var mutex = new Mutex(true, "MyApplicationMutex", out createdNew);

createdNewtrueなら新しくMutexを作成できた、つまり最初の起動です。falseなら同じ名前のMutexがすでに存在しており、アプリが起動済みと判断できます。

2-2. WaitOneでロックを取得する

Mutexの所有権を取得するには、WaitOneを使います。

C#
mutex.WaitOne();

WaitOneは、Mutexを取得できるまで現在のスレッドを待機させます。取得できると、以降の処理に進みます。公式ドキュメントでは、WaitOneは現在のWaitHandleがシグナルを受信するまで現在のスレッドをブロックすると説明されています。

基本的な流れは次のとおりです。

C#
mutex.WaitOne();

try
{
// 排他制御したい処理
}
finally
{
mutex.ReleaseMutex();
}

WaitOneを呼んだスレッドはMutexの所有者になります。所有者になったスレッドだけが、後でReleaseMutexを呼び出して解放できます。

2-3. ReleaseMutexでロックを解放する

Mutexを取得したら、処理が終わったタイミングで必ずReleaseMutexを呼び出します。

C#
mutex.ReleaseMutex();

ReleaseMutexはMutexを1回解放します。Microsoftの公式ドキュメントでは、呼び出し元のスレッドがMutexを所有していない場合、ApplicationExceptionが発生すると説明されています。

つまり、次のようなコードは危険です。

C#
var mutex = new Mutex(false, "SampleMutex");

// WaitOneしていないのにReleaseMutexを呼ぶ
mutex.ReleaseMutex();

この場合、現在のスレッドはMutexを所有していないため、例外が発生します。

2-4. using・try-finallyで安全に解放する

Mutexを安全に扱うには、try-finallyReleaseMutexを必ず実行することが重要です。

C#
using var mutex = new Mutex(false, "SampleMutex");

mutex.WaitOne();

try
{
// 排他制御したい処理
}
finally
{
mutex.ReleaseMutex();
}

usingは、Mutexオブジェクト自体をDisposeするために使います。ReleaseMutexは所有権の解放、Disposeはオブジェクトが保持しているリソースの解放です。この2つは役割が違います。

よくある誤解として、「usingしているからReleaseMutexは不要」と考えてしまうケースがあります。しかし、Mutexを取得した場合は、基本的にReleaseMutexで所有権を解放する必要があります。

安全な書き方は次の形です。

C#
using var mutex = new Mutex(false, "SampleMutex");

bool hasHandle = false;

try
{
hasHandle = mutex.WaitOne(5000);

if (!hasHandle)
{
Console.WriteLine("Mutexを取得できませんでした。");
return;
}

Console.WriteLine("排他制御中の処理を実行します。");
}
finally
{
if (hasHandle)
{
mutex.ReleaseMutex();
}
}

hasHandleを使って、Mutexを取得できた場合だけReleaseMutexを呼ぶようにしています。

2-5. 基本サンプルコードで処理の流れを確認する

次のサンプルでは、同じConsoleアプリを複数起動すると、先に起動したプロセスが処理中の間、後から起動したプロセスはMutexの取得を待ちます。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
const string mutexName = "Sample.Basic.Mutex";

using var mutex = new Mutex(false, mutexName);

Console.WriteLine("Mutexの取得を待っています...");

mutex.WaitOne();

try
{
Console.WriteLine("Mutexを取得しました。処理を開始します。");
Thread.Sleep(10000);
Console.WriteLine("処理が完了しました。");
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("Mutexを解放しました。");
}
}
}

このコードの流れは次のようになります。

最初のプロセスがWaitOneでMutexを取得します。その間、後から起動したプロセスはWaitOneで待機します。最初のプロセスがReleaseMutexを呼ぶと、次のプロセスがMutexを取得して処理を続行します。

3. MutexでC#アプリの二重起動を防止する方法

3-1. 二重起動防止にMutexが使われる理由

C#アプリの二重起動防止では、名前付きMutexがよく使われます。理由は、同じ名前のMutexをOS全体、またはセッション内で共有できるためです。

アプリ起動時に特定の名前のMutexを作成し、すでに存在している場合は「起動済み」と判断します。これにより、Windows Forms、WPF、Consoleアプリなどで二重起動を防止できます。

二重起動防止の基本方針は次のとおりです。

C#
bool createdNew;
using var mutex = new Mutex(true, "MyCompany.MyApp", out createdNew);

if (!createdNew)
{
Console.WriteLine("アプリはすでに起動しています。");
return;
}

// 通常起動

createdNewtrueなら最初の起動、falseならすでに同名のMutexが存在します。

3-2. 名前付きMutexとは

名前付きMutexとは、文字列の名前を持つMutexです。

C#
new Mutex(true, "MyCompany.MyProduct.MyApp", out bool createdNew);

名前のないMutexは、基本的に同一プロセス内で使います。一方、名前付きMutexはOS上で同じ名前を使うことで、別プロセス間でも同じ同期オブジェクトを参照できます。

公式ドキュメントでも、名前付きシステムMutexはオペレーティングシステム全体で表示され、プロセスのアクティビティ同期に使用できると説明されています。

二重起動防止では、この性質を利用します。

3-3. createdNewを使って起動済みか判定する

createdNewは、Mutexが新しく作成されたかどうかを示す値です。

C#
bool createdNew;
using var mutex = new Mutex(true, "MyCompany.MyApp", out createdNew);

if (!createdNew)
{
Console.WriteLine("すでに起動しています。");
return;
}

try
{
Console.WriteLine("初回起動です。");
}
finally
{
mutex.ReleaseMutex();
}

ここで重要なのは、createdNewtrueの場合、現在のスレッドがMutexを所有しているという点です。そのため、アプリ終了時にReleaseMutexを呼び出します。

一方、createdNewfalseの場合、すでに同名のMutexが存在します。この場合は二重起動と判断し、通常はアプリを終了します。

3-4. Windows Formsアプリでの実装例

.NET 6以降のWindows Formsアプリでは、Program.csに次のように実装できます。

C#
using System;
using System.Threading;
using System.Windows.Forms;

namespace SampleWinFormsApp
{
internal static class Program
{
[STAThread]
static void Main()
{
const string mutexName = @"Local\MyCompany.SampleWinFormsApp";

using var mutex = new Mutex(true, mutexName, out bool createdNew);

if (!createdNew)
{
MessageBox.Show(
"アプリケーションはすでに起動しています。",
"二重起動防止",
MessageBoxButtons.OK,
MessageBoxIcon.Information);

return;
}

try
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
finally
{
mutex.ReleaseMutex();
}
}
}
}

.NET FrameworkのWindows Formsアプリでは、ApplicationConfiguration.Initialize()ではなく、次のように書くことが多いです。

C#
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());

その場合は、次のようになります。

C#
using System;
using System.Threading;
using System.Windows.Forms;

namespace SampleWinFormsApp
{
internal static class Program
{
[STAThread]
static void Main()
{
const string mutexName = @"Local\MyCompany.SampleWinFormsApp";

using var mutex = new Mutex(true, mutexName, out bool createdNew);

if (!createdNew)
{
MessageBox.Show("アプリケーションはすでに起動しています。");
return;
}

try
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
finally
{
mutex.ReleaseMutex();
}
}
}
}

3-5. WPFアプリでの実装例

WPFアプリでは、App.xaml.csOnStartupでMutexを作成する方法が分かりやすいです。

C#
using System.Threading;
using System.Windows;

namespace SampleWpfApp
{
public partial class App : Application
{
private Mutex? _mutex;
private bool _createdNew;

protected override void OnStartup(StartupEventArgs e)
{
const string mutexName = @"Local\MyCompany.SampleWpfApp";

_mutex = new Mutex(true, mutexName, out _createdNew);

if (!_createdNew)
{
MessageBox.Show(
"アプリケーションはすでに起動しています。",
"二重起動防止",
MessageBoxButton.OK,
MessageBoxImage.Information);

Shutdown();
return;
}

base.OnStartup(e);
}

protected override void OnExit(ExitEventArgs e)
{
if (_createdNew)
{
_mutex?.ReleaseMutex();
}

_mutex?.Dispose();

base.OnExit(e);
}
}
}

ポイントは、Mutexをローカル変数ではなくフィールドとして保持することです。アプリの起動中にMutexオブジェクトが破棄されると、二重起動防止の意味がなくなる可能性があります。

3-6. Consoleアプリでの実装例

Consoleアプリで二重起動を防止する例です。

C#
using System;
using System.Threading;

class Program
{
static int Main()
{
const string mutexName = @"Local\MyCompany.SampleConsoleApp";

using var mutex = new Mutex(true, mutexName, out bool createdNew);

if (!createdNew)
{
Console.WriteLine("このアプリケーションはすでに起動しています。");
return 1;
}

try
{
Console.WriteLine("アプリケーションを開始しました。");
Console.WriteLine("Enterキーを押すと終了します。");
Console.ReadLine();

return 0;
}
finally
{
mutex.ReleaseMutex();
}
}
}

タスクスケジューラで起動されるバッチ処理の場合は、二重起動時にエラーコードを返す設計にしておくと、運用時に状態を把握しやすくなります。

4. 名前付きMutexの注意点と設計ポイント

4-1. Mutex名は一意になるように決める

名前付きMutexを使う場合、Mutex名はアプリケーションごとに一意になるように決める必要があります。

悪い例は次のような名前です。

C#
"AppMutex"
"Mutex"
"SingleInstance"

このような一般的すぎる名前は、他のアプリケーションと衝突する可能性があります。

おすすめは、会社名、製品名、アプリ名、GUIDなどを含める方法です。

C#
@"Local\MyCompany.MyProduct.MyApp"
@"Local\MyCompany.MyProduct.MyApp.{D9B8C4A1-1234-4A9B-ABCD-1234567890AB}"

特に配布アプリや社内で複数ツールを運用している環境では、名前の衝突を避けるためにGUIDを含めると安全です。

4-2. Global\とLocal\の違い

Windows環境の名前付きMutexでは、名前の先頭にGlobal\またはLocal\を付けられます。

C#
@"Global\MyCompany.MyApp"
@"Local\MyCompany.MyApp"

Global\は、すべてのターミナルサーバーセッションで見える名前空間です。Local\は、作成されたセッション内で見える名前空間です。Microsoftの公式ドキュメントでも、Global\はすべてのターミナルサーバーセッション、Local\は作成されたセッション内で表示されると説明されています。

通常のデスクトップアプリで「同じユーザーセッション内の二重起動を防止したい」場合は、Local\で十分なケースが多いです。

一方、リモートデスクトップや複数ユーザー環境を含めてPC全体で1つだけにしたい場合は、Global\を検討します。ただし、Global\を使う場合は権限やセキュリティの問題に注意が必要です。

4-3. ユーザー単位・PC全体で二重起動を制御する考え方

二重起動防止では、「何を単位に1つだけ許可するか」を最初に決める必要があります。

同じユーザーの同じログオンセッション内で1つだけにしたい場合は、Local\を使う設計が分かりやすいです。

C#
const string mutexName = @"Local\MyCompany.MyApp";

PC全体で1つだけにしたい場合は、Global\を使います。

C#
const string mutexName = @"Global\MyCompany.MyApp";

ただし、PC全体で1つだけにすると、別ユーザーが同じPCにログオンしている場合にも起動できない可能性があります。業務アプリではこれが望ましい場合もありますが、一般的なデスクトップアプリではユーザーごとに起動できた方が自然なケースもあります。

設計時には、次の観点を確認しましょう。

同じユーザーだけで二重起動を防ぎたいのか。別ユーザーも含めてPC全体で1つにしたいのか。リモートデスクトップ環境でどう扱うのか。サービスやタスクスケジューラからの起動も対象にするのか。

4-4. MutexのライフサイクルとDispose

MutexIDisposableを実装しているため、使い終わったらDisposeする必要があります。Microsoftの公式ドキュメントでも、Mutexの使用が完了したら直接またはusingなどで破棄する必要があると説明されています。

短い処理で使う場合は、usingを使うのが簡単です。

C#
using var mutex = new Mutex(false, "MyCompany.MyApp");

WPFアプリのようにアプリケーションの起動中ずっと保持したい場合は、フィールドに保持してOnExitDisposeします。

C#
private Mutex? _mutex;

protected override void OnExit(ExitEventArgs e)
{
_mutex?.Dispose();
base.OnExit(e);
}

ただし、DisposeはMutexオブジェクトのリソース解放であり、所有権の解放とは別です。WaitOnenew Mutex(true, ...)で所有権を取得している場合は、適切なタイミングでReleaseMutexを呼ぶことが基本です。

4-5. アプリ終了時にMutexを確実に解放する

アプリ終了時にMutexを確実に解放するには、try-finallyを使います。

C#
using var mutex = new Mutex(true, "MyCompany.MyApp", out bool createdNew);

if (!createdNew)
{
return;
}

try
{
// アプリ本体の処理
}
finally
{
mutex.ReleaseMutex();
}

例外が発生してもfinallyは実行されるため、通常終了だけでなく異常終了時にもMutexを解放しやすくなります。

ただし、プロセスが強制終了された場合や、タスクマネージャーから終了された場合は、通常のfinallyが実行されないことがあります。その場合、次にMutexを取得する側でAbandonedMutexExceptionが発生する可能性があります。

5. Mutexによる排他制御の実践例

5-1. ファイル書き込みをMutexで排他制御する

複数プロセスから同じファイルへ書き込む場合、Mutexで排他制御すると安全性を高められます。

C#
using System;
using System.IO;
using System.Threading;

class Program
{
static void Main()
{
const string mutexName = @"Local\MyCompany.FileWriteMutex";
const string filePath = @"C:\Temp\sample.log";

using var mutex = new Mutex(false, mutexName);

bool hasHandle = false;

try
{
hasHandle = mutex.WaitOne(TimeSpan.FromSeconds(5));

if (!hasHandle)
{
Console.WriteLine("ファイル書き込みのロックを取得できませんでした。");
return;
}

File.AppendAllText(
filePath,
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} 書き込みました{Environment.NewLine}");
}
finally
{
if (hasHandle)
{
mutex.ReleaseMutex();
}
}
}
}

このコードでは、同じ名前のMutexを使うプロセス間でファイル書き込みを1つずつ実行します。

ただし、すべての書き込みプロセスが同じMutex名を使う必要があります。Mutexを使わない別プロセスが同じファイルに書き込む場合、そのプロセスは排他制御の対象外です。

5-2. 複数プロセスから同じリソースへアクセスする場合

Mutexは、ファイル以外にも複数プロセスから同じリソースへアクセスする場面で使えます。

たとえば、次のようなリソースが対象になります。

共有設定ファイル、共通キャッシュディレクトリ、一時ファイル、外部ツールの実行、共有メモリ、USBデバイス、ライセンスファイルなどです。

実装例は次のようになります。

C#
using System;
using System.Threading;

public class SharedResourceService
{
private const string MutexName = @"Local\MyCompany.SharedResource";

public void Execute()
{
using var mutex = new Mutex(false, MutexName);

bool locked = false;

try
{
locked = mutex.WaitOne(TimeSpan.FromSeconds(10));

if (!locked)
{
throw new TimeoutException("共有リソースのロック取得がタイムアウトしました。");
}

AccessSharedResource();
}
finally
{
if (locked)
{
mutex.ReleaseMutex();
}
}
}

private void AccessSharedResource()
{
Console.WriteLine("共有リソースへアクセスしています。");
}
}

このように、Mutexの取得、タイムアウト判定、解放処理を1つのメソッドにまとめると、呼び出し側のコードがすっきりします。

5-3. 一定時間だけロック取得を待つ方法

WaitOneには、待機時間を指定できるオーバーロードがあります。

C#
bool acquired = mutex.WaitOne(3000);

この例では、最大3秒だけ待ちます。3秒以内にMutexを取得できた場合はtrue、タイムアウトした場合はfalseが返ります。公式ドキュメントでも、タイムアウト付きのWaitOneは、タイムアウト間隔が経過した場合にfalseを返すと説明されています。

TimeSpanを使うと、より読みやすく書けます。

C#
bool acquired = mutex.WaitOne(TimeSpan.FromSeconds(3));

待機し続ける設計にすると、他プロセスがMutexを解放しない場合にアプリが固まったように見えることがあります。そのため、実務ではタイムアウトを指定することをおすすめします。

5-4. タイムアウト時に処理を中断する実装例

タイムアウト時に処理を中断する例です。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
const string mutexName = @"Local\MyCompany.TimeoutSample";

using var mutex = new Mutex(false, mutexName);

bool hasHandle = false;

try
{
hasHandle = mutex.WaitOne(TimeSpan.FromSeconds(5));

if (!hasHandle)
{
Console.WriteLine("他の処理が実行中のため、今回は処理を中断します。");
return;
}

Console.WriteLine("ロックを取得しました。処理を実行します。");
Thread.Sleep(10000);
}
finally
{
if (hasHandle)
{
mutex.ReleaseMutex();
Console.WriteLine("ロックを解放しました。");
}
}
}
}

このコードでは、5秒待ってもMutexを取得できなければ処理を中断します。バッチ処理や定期実行ジョブでは、無限に待つよりも、タイムアウトしてログを残す設計の方が運用しやすくなります。

5-5. Mutexを使うべきケース・使わない方がよいケース

Mutexを使うべきケースは、プロセスをまたいだ排他制御が必要な場合です。具体的には、二重起動防止、複数プロセスからのファイル書き込み制御、外部リソースの同時利用防止などです。

一方、同一プロセス内のスレッドだけを制御したい場合は、lockSemaphoreSlimの方が適していることが多いです。MutexはOSの同期オブジェクトを使うため、lockより重くなりやすいからです。

また、非同期処理で待機したい場合も、Mutexは第一候補になりにくいです。WaitOneはスレッドをブロックするため、async/awaitの流れではSemaphoreSlim.WaitAsyncなどを検討した方が自然です。

6. C# Mutexでよくあるエラーと対策

6-1. ReleaseMutexでApplicationExceptionが発生する原因

ReleaseMutexApplicationExceptionが発生する主な原因は、現在のスレッドがMutexを所有していないことです。

たとえば、次のコードは問題があります。

C#
using var mutex = new Mutex(false, "SampleMutex");

if (!mutex.WaitOne(1000))
{
// 取得できていないのにfinallyでReleaseMutexしてしまう
}

mutex.ReleaseMutex();

WaitOnefalseを返した場合、Mutexを取得できていません。その状態でReleaseMutexを呼ぶと例外になります。

対策は、Mutexを取得できたかどうかをフラグで管理することです。

C#
bool hasHandle = false;

try
{
hasHandle = mutex.WaitOne(1000);

if (!hasHandle)
{
return;
}

// 排他処理
}
finally
{
if (hasHandle)
{
mutex.ReleaseMutex();
}
}

また、WaitOneしたスレッドと別のスレッドでReleaseMutexを呼ぶのも避ける必要があります。Mutexはスレッドの所有権を管理するため、取得したスレッドが解放するのが基本です。

6-2. AbandonedMutexExceptionが発生する原因と対応

AbandonedMutexExceptionは、Mutexを所有していたスレッドやプロセスが、ReleaseMutexを呼ばずに終了した場合に発生します。

たとえば、Mutexを取得したプロセスが強制終了された場合、次にMutexを取得しようとしたプロセスでこの例外が発生することがあります。

C#
try
{
mutex.WaitOne();
}
catch (AbandonedMutexException)
{
// 前回の所有者がMutexを解放せず終了した
// 必要に応じて共有データの整合性を確認する
}

公式ドキュメントでも、破棄されたMutexはコード上の重大なエラーを示すことが多く、保護されていたデータ構造が一貫した状態ではない可能性があると説明されています。

対応としては、例外を単純に握りつぶすのではなく、保護対象のリソースが壊れていないか確認することが重要です。ファイル書き込み中に強制終了された場合は、ファイルの内容や一時ファイルの状態を確認する処理を入れると安全です。

6-3. UnauthorizedAccessExceptionが発生するケース

名前付きMutexでは、権限の問題でUnauthorizedAccessExceptionが発生することがあります。

特に、Global\を使う場合や、別ユーザー・サービス・管理者権限プロセスと通常ユーザープロセスの間で同じMutexを使う場合に注意が必要です。

たとえば、あるユーザーが作成した名前付きMutexに、別のユーザーのプロセスがアクセスしようとして権限不足になるケースがあります。

対策としては、次の点を検討します。

まず、Global\が本当に必要かを見直します。ユーザーセッション内だけでよいならLocal\にする方が単純です。

次に、複数ユーザー間で共有する必要がある場合は、アクセス制御を明示的に設定する設計を検討します。ただし、セキュリティ設定は環境や.NETのバージョンによって書き方が変わるため、慎重に設計する必要があります。

6-4. Mutexが解放されずアプリが固まる原因

Mutexが解放されず、アプリが固まったように見える原因は主に次の3つです。

1つ目は、ReleaseMutexの呼び忘れです。例外発生時にReleaseMutexまで到達しないコードになっていると、他のスレッドやプロセスが待ち続けます。

悪い例です。

C#
mutex.WaitOne();

// ここで例外が発生するとReleaseMutexされない
DoSomething();

mutex.ReleaseMutex();

良い例です。

C#
mutex.WaitOne();

try
{
DoSomething();
}
finally
{
mutex.ReleaseMutex();
}

2つ目は、無限待機です。

C#
mutex.WaitOne();

この書き方は、Mutexを取得できるまで待ち続けます。業務アプリやバッチ処理では、タイムアウトを指定する方が安全です。

C#
if (!mutex.WaitOne(TimeSpan.FromSeconds(10)))
{
Console.WriteLine("ロック取得がタイムアウトしました。");
return;
}

3つ目は、同じスレッドで複数回WaitOneしたのに、同じ回数だけReleaseMutexしていないケースです。Mutexは同じスレッドから再入可能ですが、取得した回数分だけ解放する必要があります。

C#
mutex.WaitOne();
mutex.WaitOne();

mutex.ReleaseMutex();
// まだ1回分所有している

公式ドキュメントでも、Mutexを所有するスレッドは同じMutexに対してWaitOneを繰り返し呼べる一方、所有権を解放するには同じ回数だけReleaseMutexを呼ぶ必要があると説明されています。

6-5. デッドロックを防ぐための実装ポイント

Mutexを使うときは、デッドロックにも注意が必要です。

デッドロックとは、複数のスレッドやプロセスがお互いの解放待ちになり、処理が進まなくなる状態です。

たとえば、2つのMutexを異なる順序で取得すると危険です。

C#
// プロセスA
mutexA.WaitOne();
mutexB.WaitOne();

// プロセスB
mutexB.WaitOne();
mutexA.WaitOne();

プロセスAがmutexAを持ち、プロセスBがmutexBを持ったまま、お互いに相手のMutexを待つとデッドロックになります。

対策は、複数のMutexを取得する順序を必ず統一することです。

C#
// どの処理でも必ずA → Bの順で取得する
mutexA.WaitOne();
mutexB.WaitOne();

さらに、タイムアウトを設定する、ロック範囲を短くする、Mutexを保持したまま時間のかかる処理や外部通信をしない、といった対策も重要です。

7. Mutex実装時のベストプラクティス

7-1. try-finallyでReleaseMutexを必ず呼ぶ

Mutexを取得したら、try-finallyで確実に解放するのが基本です。

C#
bool hasHandle = false;

try
{
hasHandle = mutex.WaitOne(TimeSpan.FromSeconds(5));

if (!hasHandle)
{
return;
}

// 排他制御したい処理
}
finally
{
if (hasHandle)
{
mutex.ReleaseMutex();
}
}

try-finallyを使うことで、例外が発生しても解放処理を実行できます。

7-2. WaitOneの戻り値を確認する

タイムアウト付きのWaitOneを使う場合は、戻り値を必ず確認します。

悪い例です。

C#
mutex.WaitOne(1000);

// 取得できた前提で処理してしまう
DoSomething();

mutex.ReleaseMutex();

良い例です。

C#
bool acquired = mutex.WaitOne(1000);

if (!acquired)
{
Console.WriteLine("Mutexを取得できませんでした。");
return;
}

try
{
DoSomething();
}
finally
{
mutex.ReleaseMutex();
}

WaitOnefalseを返した場合、Mutexを取得できていません。その状態で保護対象の処理を実行したり、ReleaseMutexを呼んだりしないようにします。

7-3. タイムアウトを設定して待ち続けを防ぐ

実務では、WaitOne()で無限に待つよりも、タイムアウトを設定する方が安全です。

C#
if (!mutex.WaitOne(TimeSpan.FromSeconds(30)))
{
throw new TimeoutException("Mutexの取得がタイムアウトしました。");
}

無限待機は、ユーザーから見ると「アプリが固まった」ように見えることがあります。バッチ処理では、ジョブが終了しないまま残り続ける原因にもなります。

タイムアウト時には、ログを出す、処理をスキップする、リトライする、ユーザーにメッセージを表示するなど、アプリの性質に合わせた対応を決めておきましょう。

7-4. Mutex名にアプリ固有の識別子を入れる

名前付きMutexでは、名前の衝突を避けることが非常に重要です。

おすすめは、次のような形式です。

C#
@"Local\CompanyName.ProductName.ApplicationName"

より確実に衝突を避けたい場合はGUIDを含めます。

C#
@"Local\CompanyName.ProductName.ApplicationName.{0F8F4A2B-1111-2222-3333-123456789ABC}"

Mutex名が他のアプリと重複すると、関係のないアプリ同士で二重起動と判断されたり、不要な待機が発生したりします。

7-5. usingまたはDisposeでリソースを解放する

Mutexオブジェクトは、使い終わったらDisposeします。

短いスコープならusing varが便利です。

C#
using var mutex = new Mutex(false, "SampleMutex");

従来の書き方では、次のようになります。

C#
using (var mutex = new Mutex(false, "SampleMutex"))
{
// Mutexを使う処理
}

アプリ全体で保持する場合は、フィールドに保存して終了時にDisposeします。

C#
private Mutex? _mutex;

private void Cleanup()
{
_mutex?.Dispose();
}

ただし、繰り返しになりますが、DisposeReleaseMutexは別の役割です。Mutexの所有権を取得した場合は、ReleaseMutexで解放し、オブジェクト自体はDisposeで破棄します。

8. C# Mutexに関するよくある質問

8-1. Mutexとlockはどちらを使うべき?

同一プロセス内の複数スレッドだけを排他制御したいなら、基本的にはlockを使う方がシンプルです。

C#
lock (_lockObject)
{
// 同一プロセス内の排他制御
}

一方、複数プロセス間で排他制御したい場合や、アプリの二重起動を防止したい場合はMutexを使います。

目安としては、次のように考えると分かりやすいです。

同じアプリ内のスレッドだけならlock。別プロセスも含めたいならMutex。同時実行数をN個までにしたいならSemaphoreまたはSemaphoreSlim。非同期処理中心ならSemaphoreSlimを検討します。

8-2. Mutexで完全に二重起動を防止できる?

多くの通常ケースでは、名前付きMutexで二重起動を防止できます。

ただし、「完全に」と言い切るには注意が必要です。Mutex名の設計が不適切だと他アプリと衝突する可能性があります。また、Local\Global\の選び方によって、ユーザーセッション単位なのかPC全体なのかが変わります。さらに、権限の違うプロセス間ではアクセス制御の問題が起きることもあります。

つまり、Mutexは二重起動防止の代表的な方法ですが、要件に応じて名前、スコープ、権限、終了時の解放処理を正しく設計する必要があります。

8-3. Mutexは非同期処理でも使える?

使うこと自体はできますが、MutexWaitOneは同期的にスレッドをブロックします。そのため、async/awaitを多用する非同期処理では注意が必要です。

たとえば、次のように書くと、WaitOne中はスレッドがブロックされます。

C#
mutex.WaitOne();

try
{
await DoSomethingAsync();
}
finally
{
mutex.ReleaseMutex();
}

このようなコードは、設計によってはスレッドの無駄遣いやデッドロックの原因になります。

同一プロセス内の非同期排他であれば、SemaphoreSlimWaitAsyncを使う方が自然です。

C#
await semaphore.WaitAsync();

try
{
await DoSomethingAsync();
}
finally
{
semaphore.Release();
}

ただし、複数プロセス間の排他制御が必要な場合は、Mutexの利用を検討します。その場合でも、ロック範囲を短くし、長時間のawaitをMutex保持中に行わないようにするのが安全です。

8-4. .NET Frameworkと.NETで使い方は違う?

基本的なMutexの使い方は、.NET Frameworkでも、.NET 6、.NET 8、.NET 10などの新しい.NETでも大きくは変わりません。

C#
using var mutex = new Mutex(true, "MyCompany.MyApp", out bool createdNew);

ただし、Windows Formsの起動コードなど、アプリケーションテンプレート側の書き方は異なります。

.NET Frameworkでは次のように書くことが多いです。

C#
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());

.NET 6以降では、テンプレートによって次のように書かれます。

C#
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());

また、名前付きMutexのセキュリティやOS依存の挙動は、環境によって注意が必要です。特にGlobal\を使う場合や、Windows以外の環境で名前付きMutexを使う場合は、公式ドキュメントを確認しながら設計するのが安全です。

8-5. Mutex名が重複するとどうなる?

Mutex名が重複すると、同じMutexとして扱われます。

たとえば、まったく関係のない2つのアプリが次の同じ名前を使っていたとします。

C#
"AppMutex"

この場合、一方のアプリがMutexを作成していると、もう一方のアプリは「すでに起動している」「ロックされている」と判断してしまう可能性があります。

そのため、Mutex名にはアプリ固有の情報を含めることが重要です。

C#
@"Local\MyCompany.SalesTool.MainApp"

さらに確実にするなら、GUIDを含めるとよいでしょう。

C#
@"Local\MyCompany.SalesTool.MainApp.{E2B7B7D0-ABCD-4F00-9B1A-123456789ABC}"

名前付きMutexは便利ですが、名前の設計を誤ると予期しないアプリ間干渉が起きます。特にライブラリや共通部品でMutexを使う場合は、呼び出し元アプリごとに名前を変えられるようにしておくと安全です。

まとめ

C#のMutexは、排他制御や二重起動防止を実現するための重要な同期オブジェクトです。特に、名前付きMutexを使うことで、プロセスをまたいだ排他制御ができる点が大きな特徴です。

同一プロセス内のスレッド排他ならlock、複数プロセス間の排他やアプリの二重起動防止ならMutexを使う、という使い分けが基本です。

Mutexを使う際は、WaitOneで取得し、ReleaseMutexで解放します。タイムアウト付きのWaitOneを使う場合は戻り値を確認し、取得できた場合だけReleaseMutexを呼び出します。

また、try-finallyで確実に解放すること、usingDisposeでリソースを破棄すること、Mutex名にアプリ固有の識別子を入れることも重要です。

二重起動防止では、new Mutex(true, mutexName, out createdNew)を使い、createdNewfalseなら起動済みと判断します。Windows Forms、WPF、Consoleアプリのいずれでも同じ考え方で実装できます。

一方で、ReleaseMutexの呼び方を誤るとApplicationExceptionが発生し、強制終了などでMutexが放棄されるとAbandonedMutexExceptionが発生することがあります。こうしたエラーを避けるには、Mutexを取得できたかどうかをフラグで管理し、ロック範囲を短くし、タイムアウトを設定することが大切です。

C# Mutexは強力ですが、必要以上に使うものではありません。プロセス間排他が必要な場面に絞って使い、同一プロセス内の軽い排他にはlockSemaphoreSlimを使い分けることで、安全で分かりやすい実装になります。