読者です 読者をやめる 読者になる 読者になる

.NET FrameworkのMutexの納得いかない仕様

C#

変な仕様

.NET Frameworkの名前付きMutexは、どうも、Disposeというか、Closeしても、MutexをReleaseMutexしてくれないような感じ。試しに下のようなコードを書いた。

using System;
using System.Threading;

namespace MutexTest
{
  public class MutexTest
  {
    public static void Main()
    {
      using (Mutex m = new Mutex(false, "Hello, world"))
      {
        m.WaitOne();
        Console.WriteLine("Hey, I'm in the protected world!");
        Thread.Sleep(10000);
        Console.WriteLine("...");

        // 本当は次の行が必要
        // m.ReleaseMutex();
      }
    }
  }
}

このプログラムを、1秒ぐらいの時間差をおいて、2つのプロセスを起動(実行)すると、1つめのプロセスは正常に終了するのだが、2つめのプロセスは、

ハンドルされていない例外: System.Threading.AbandonedMutexException: 放棄されたミューテックスのため、待機は完了しました。
   場所 System.Threading.WaitHandle.WaitOne(Int64 timeout, Boolean exitContext)
   場所 MutexTest.MutexTest.Main()

といったエラーを出力して終了する。とはいえ、Mutexのカーネルオブジェクト自体は正常に破棄されるようで、この後にもう一度、このプログラムを実行しても、問題はない。

つまり、正常に動作するようにするには、ReleaseMutexを明示的に呼び出すしかないらしい。普通に考えて、ReleaseMutexせずにDisposeするなんて考えにくいのだけど、ふと思って、次みたいなコードを書いてみた。

using System;
using System.Threading;

namespace MutexTest
{
  public class MutexTest
  {
    public static void Main()
    {
      using (Mutex m = new Mutex(false, "Hello, world"))
      {
        m.WaitOne();
        Console.WriteLine("Hey, I'm in the protected world!");
        Thread.Sleep(10000);
        Console.WriteLine("...");
      }

      using (Mutex m = new Mutex(false, "Hello, world"))
      {
        m.ReleaseMutex();
      }
    }
  }
}

全くナンセンスなコードなんだけど、このコードでは、上記のAbandonedMutexExceptionは出なくなる。つまり、正しくReleaseMutexすることには成功しているっぽい。

ただし、この場合、2つ目のプロセスでは、

ハンドルされていない例外: System.ApplicationException: オブジェクト同期メソッドは、コードの非同期ブロックから呼び出されました。
   場所 System.Threading.Mutex.ReleaseMutex()
   場所 MutexTest.MutexTest.Main()

というよくわからない例外が出る。なんだろう、これは。

安全なMutex

ということで、次のようなラッパークラスを作った。

class SessionLock : IDisposable
{
  public SessionLock(string mutexName)
  {
    m_mutex = new Mutex(false, mutexName);
    m_mutex.WaitOne();
  }

  public void Dispose()
  {
    try
    {
      m_mutex.ReleaseMutex();
      m_mutex.Close();
    }
    catch { }
  }

  Mutex m_mutex;
}

これならば、

using System;
using System.Threading;

namespace MutexTest
{
  ...

  public class MutexTest
  {
    public static void Main()
    {
      using (new SessionLock("Hello, world"))
      {
        Console.WriteLine("Hey, I'm in the protected world!");
        Thread.Sleep(10000);
        Console.WriteLine("...");
      }
    }
  }
}

というRAIIっぽいコンパクトなコードが書ける。というか、C#のusingは、newの結果を変数に代入しなくてもいいんですねぇ。