1. はじめに
前の講義の例を思い出してみましょう:まだ簡単なアプリケーションで2つのスレッドが共有カウンタをインクリメントしていますが、最終値が期待通りにならないことがあります。このカウンタを保護するために既にキーワードの lock(厳密には Monitor)を使いましたが、これは同一プロセス内での同期に適しています。しかし、もしあなたのプログラム以外にもそのリソースを使いたいプロセスがあるとしたら?例えば、同じサービスが2つのインスタンスで動いていて両方が同じファイルに書き込みたい…あるいはプリンタポートのようなハードウェアを共有したい場合です。そんなときに頼りになるのが昔ながらのミューテックス(mutual exclusion — 「相互排他」)です。
コンセプト
ミューテックスは、単に同一プロセス内のスレッド間でのアクセスを制限するだけでなく、同じマシン上の異なるプロセス間でのアクセスを調整できる同期プリミティブです。会議室の前に置かれた大きな「使用中」札のように考えてください。社員も訪問者もそれを見ます。
.NET ではこれに System.Threading.Mutex クラスが使われます。
Mutex が本当に必要な場面:
- 異なるプロセス間でアクセスを同期する必要があるとき(例えば、別々のアプリケーションが同じファイルを扱う場合)。
- リソースが非常に貴重かつ不可分で、プロセスのコンテキストを超えてアクセス権を分けられないとき。
同一プロセス内のスレッド間のみを同期するなら通常は lock(Monitor)を使います。Mutex はより重く遅いので、主にプロセス間同期に使うのが適切です。
Mutex の動作
flowchart TD
A(プロセス1) --|要求する|--> M(Mutex)
B(プロセス2) --|要求する|--> M
M --|1つだけ許可|--> R(共有リソース)
A --|解放する|--> M
B --|解放後|--> M
2. Mutex の基本構文
作成
Mutex は他の多くの同期クラスと同じように簡単に作成できます:
using System.Threading;
Mutex mutex = new Mutex();
主なメソッド
- WaitOne() — ミューテックスの取得を試みます。取得できない場合は、他の誰かが解放するまでスレッドはブロックされます。
- ReleaseMutex() — ミューテックスを解放し、他のスレッドやプロセスがクリティカルセクションに入れるようにします。
簡単な例:同一プロセス内のスレッド間の同期
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread t1 = new Thread(PrintNumbers);
Thread t2 = new Thread(PrintNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
mutex.WaitOne(); // クリティカルセクションに入る
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // クリティカルセクションを出る
Thread.Sleep(100); // 見やすくするため
}
}
}
この例では、両方のスレッドが交互にコンソールへの出力を行います。
3. 名前付きミューテックスでのプロセス間同期
プロセス間同期のための「本格的な」手段として名前付きミューテックスを使います。名前を付けると、同じマシン上の全プロセスがそれにアクセスできます。
Mutex mutex = new Mutex(false, "MyApp_Mutex");
コンストラクタのパラメータ:
- 最初のパラメータ(bool initiallyOwned)— 作成直後にスレッドがミューテックスを取得するかどうか。通常は false。
- 2番目のパラメータ — ミューテックスの名前。同じ名前を使うプロセスは同じオブジェクトにアクセスします。例: "MyApp_Mutex"。
例:同じ Mutex を使う2つのアプリケーション
同じプログラムを別々のウィンドウから2回起動して効果を確認してみてください。
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("クリティカルセクションへの試行...");
mutex.WaitOne(); // 他のプロセスがミューテックスを解放するのを待つ
try
{
Console.WriteLine("このプロセスがクリティカルセクションを占有しています。");
Console.WriteLine("クリティカルセクションを終了するには Enter を押してください。");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("クリティカルセクションは解放されました。");
}
}
}
}
試してみよう:
- このアプリケーションを2つのウィンドウで開く。
- 両方を実行すると、2つ目は最初のウィンドウで Enter を押すまで待機します。
4. 同時起動の制限
Mutex は同時に起動できるインスタンス数を制限するのによく使われます。例えば:「ねぇ、ユーザーさん、次から電卓のコピーを2つ開かないでね!」といった具合です。
using System;
using System.Threading;
class Program
{
static void Main()
{
bool createdNew;
using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
{
if (!createdNew)
{
Console.WriteLine("アプリケーションは既に起動しています!");
return;
}
Console.WriteLine("アプリケーションは正常に起動しました。終了するには Enter を押してください。");
Console.ReadLine();
}
}
}
このパターンはデスクトップアプリでよく見られます:最初のインスタンスが起動し、2つ目はそれを検知して終了します。Microsoft の公式例は ドキュメント にあります。
5. Mutex を使う際の典型的なミス
ミスその1: ReleaseMutex() を呼び忘れる。
スレッドがミューテックスを正常に取得した(WaitOne())が、ReleaseMutex() を呼ばなかった(例外で落ちた、あるいは単純に忘れた)場合、他のどのスレッドやプロセスも中に入れなくなります。これはデッドロックを引き起こす可能性があります。良いスタイルは常に try-finally を使うことです:
mutex.WaitOne();
try
{
// クリティカルセクション
}
finally
{
mutex.ReleaseMutex();
}
ミスその2: ReleaseMutex() の呼び過ぎ。
WaitOne() を呼んだ回数より多く ReleaseMutex() を呼ぶと、ApplicationException がスローされます。
ミスその3: 自分のものでない Mutex を解放しようとする。
ミューテックスはそれを取得したスレッドに紐づいています。そのスレッドだけが ReleaseMutex() を呼べます。別のスレッドが解放しようとすると .NET は例外を投げます。
ミスその4: Mutex を使うべきでない場所で使っている。
Mutex はプロセス間で動作できるのでシステムコールが必要になり、lock より遅くなります。プロセス間同期が不要なら lock を使うべきです。
ミスその5: ミューテックスの名前がまずい。
単純すぎる名前(例: "MyMutex" )を選ぶと、他のアプリと偶然同じ名前になってしまうことがあります。会社名やアプリ名などを含めてユニークな名前を使うのが安全です。
6. 便利なポイント
WaitOne(timeout)
ミューテックスの待機にタイムアウトを設定できます:
if (mutex.WaitOne(5000)) // 最大5秒待つ
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("5秒以内にリソースを取得できませんでした!");
}
Mutex.TryOpenExisting
既に存在するミューテックスに接続する必要がある場合は、静的メソッドを使います:
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// これで existingMutex は既存のミューテックスへの参照になる
}
ユーザー間での分離
名前付きミューテックスはデフォルトでシステムオブジェクト作成権限を持つすべてのユーザーがアクセスできます。より厳密な制御が必要なら MutexSecurity を使うコンストラクタを使ってください。
比較表: lock/Monitor, Mutex, Semaphore
| プリミティブ | プロセス間同期? | 速度 | 使う場所 |
|---|---|---|---|
|
いいえ | 非常に高速 | 同一プロセス内のスレッド間 |
|
はい | 低め(コスト高め) | 異なるプロセス間のスレッド同士 |
|
はい(NamedSemaphore がある) | Mutex と同程度 | 同時に許可するスレッド数を制限したいとき |
セマフォについては次の講義で詳しく扱います。
Mutex の状態
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrant)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : スレッドが終了し ReleaseMutex() を呼ばなかった
Abandoned --> Unowned
- Unowned — ミューテックスを所有しているものはいない。
- Owned — スレッドがミューテックスを取得している状態。
- Abandoned — スレッドが突然終了して ReleaseMutex() を呼ばなかった状態。次にミューテックスを取得した者は AbandonedMutexException を受け取る(これはエラーであり、リソースの不整合が起きている可能性を示すサインです)。
GO TO FULL VERSION