【WPF】Mutexを使用して多重起動を防止する

【WPF】Mutexを使用して多重起動を防止する

本記事は、WPF で作成したアプリケーションが二重起動しないための実装をまとめます。

動作確認

二重コードを防止しないと、どの様な動きになるのか確認してみます。

どんどん増えるアプリケーション

exe ファイルを実行するたびにアプリケーションが起動しています。

通常のデスクトップアプリケーションで多重起動するものはほとんど無いと思いますが、何も設定していないとこのような動作になってしまいます。

多重起動を防止するためには、Mutexクラスを使用します。

Mutex クラスとは?

Mutex クラスは排他制御の機能を提供するクラスです。排他制御とは簡単に言うと、あるリソースが使用中の場合はアクセス出来ないようにしてデータの整合性を保つ処理のことをいいます。

リソースがアクセス可能になるまで別のプロセスやプログラムからアクセスすることは出来ません。Mutex を利用して一度アプリケーションが起動したら再度起動しない制御をしていきます。

また、Mutex にはシステムミューテックス(名前付きミューテックス)とローカルミューテックス(名前なし)があり、作成した Mutex のスコープが異なります。

名前付き Mutex

名前付き Mutex は、オペレーティングシステム全体から参照でき、プロセス間で共有できる Mutex となります。インスタンスのアクセス修飾子がprivateであってもプロセス間で所有権が共有されます。

多重起動の防止にはこちらを使用します。

実装方法として、App.xaml のコードビハインド以下のようなコードを記述します。

"App.xaml.cs"
using System;
using System.Threading;
using System.Windows;

namespace WpfApp1
{
  /// <summary>
  /// Interaction logic for App.xaml
  /// </summary>
  public partial class App : Application
  {

    // Mutex名(アプリケーション固有の名前. 他のアプリケーションと被らない用に注意)
    private const string _mutexName = "MutexTest";

    // Mutexオブジェクトを作成
    private Mutex _mutex = new Mutex(false, _mutexName);

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public App()
    {
      // 起動時のイベントにメソッドを登録
      Startup += Application_Startup;

      // アプリケーション終了時のイベントにメソッドを登録
      Exit += Application_Exit;
    }

    /// <summary>
    /// スタートアップ時のイベントに読み込ませるメソッド
    /// </summary>
    /// <param name="sender">イベントを送信したオブジェクト</param>
    /// <param name="e">イベント引数</param>
    private void Application_Startup(object sender, StartupEventArgs e)
    {
      // ミューテックスの所有権を要求
      if (!_mutex.WaitOne(0, false))
      {
        // 既に起動しているため終了させる
        MessageBox.Show("ソフトウェアは既に起動しています。",
                        "二重起動防止",
                        MessageBoxButton.OK,
                        MessageBoxImage.Exclamation);

        // インスタンスが所有しているリソースを開放する
        _mutex.Close();

        // null を代入
        _mutex = null;

        // アプリケーションを終了させる
        Environment.Exit(0);
      }
    }

    /// <summary>
    /// アプリケーション終了時のイベントに登録するメソッド
    /// </summary>
    /// <param name="sender">イベントを送信したオブジェクト</param>
    /// <param name="e">イベント引数</param>
    private void Application_Exit(object sender, ExitEventArgs e)
    {
      // mutexを開放しておかないと、mutexの所有権を得られずに二度と起動できなくなる
      if (_mutex != null)
      {
          // 共有リソースである Mutex を開放
          _mutex.ReleaseMutex();

          // インスタンスが所有しているリソースを開放する
          _mutex.Close();
      }
    }
  }
}

それではコードを見ていきましょう。

"App.xaml.cs"
// Mutex名(アプリケーション固有の名前. 他のアプリケーションと被らない用に注意)
private const string _mutexName = "MutexTest";

// Mutexオブジェクトを作成
private Mutex _mutex = new Mutex(false, _mutexName);

Mutex オブジェクトを生成しているコードですが、エディタから Mutax のアセンブリを確認すると複数のコンストラクタが用意されています。

"Mutex[メタデータから]"
// パラメーター:
//   initiallyOwned:
//   呼び出したスレッドにミューテックスの初期所有権を与える場合は true、そうでない場合は false を指定します。
public Mutex(bool initiallyOwned);

// 概要:
// System.Threading.Mutex クラスの新しいインスタンスを、呼び出したスレッドがミューテックスの
// 初期所有権を持つべきかどうかを示すブール値、およびミューテックスの名前である文字列で初期化します。
//
// パラメーター:
// initiallyOwned:
//   System.Threading.Mutex クラスの新しいインスタンスを、呼び出したスレッドがミューテックスの
//   初期所有権を持つべきかどうかを示すブール値、およびミューテックスの名前である文字列で初期化します。
//
// name:
//   同期オブジェクトが他のプロセスと共有される場合は、その名前。
//   それ以外の場合は、NULLまたは空文字列。名前は大文字と小文字を区別する。
public Mutex(bool initiallyOwned, string? name);

//
// 概要:
// System.Threading.Mutex クラスの新しいインスタンスを、呼び出したスレッドがミューテックスの初期所有権を持つべきかどうかを示すブール値、
// ミューテックスの名前を示す文字列、およびメソッドが戻ったときに呼び出したスレッドがミューテックスの初期所有権を付与されたかどうかを示すブール値で初期化します。
//
// パラメーター:
// initiallyOwned:
//   System.Threading.Mutex クラスの新しいインスタンスを、呼び出したスレッドがミューテックスの
//   初期所有権を持つべきかどうかを示すブール値、およびミューテックスの名前である文字列で初期化します。
//
// name:
//   同期オブジェクトが他のプロセスと共有される場合は、その名前。
//   それ以外の場合は、NULLまたは空文字列。名前は大文字と小文字を区別する。
//
// createdNew:
//   このメソッドが返されるとき、ローカル・ミューテックスが作成された場合(つまり、name が NULL または空文字列の場合)、
//   または指定された名前のシステムミューテックスが作成された場合は true、指定された名前のシステムミューテックスが既に存在していた場合は false となるブール値が格納されます。
//   このパラメータは初期化されていない状態で渡される。
public Mutex(bool initiallyOwned, string? name, out bool createdNew);

第一引数のinitiallyOwnedで Mutex の所有権をアプリケーションに与えるか設定します。truefalseでコードの書き方が変わってきます。今回のサンプルではfalseを指定しています。

第二引数で Mutex に設定する識別名を文字列で指定します。

第三引数を指定すると、ローカルミューテックス(名前なし)だった場合はtrueが返却され、システムミューテックス(名前あり)だった場合はfalseが返却されます。out修飾子に関しては詳細は割愛しますが、out修飾子に指定した値は参照渡しされ return 文で受け取らなくとも値の変更を受け取ることができます。

実際に名前部分をnullにして(ローカルミューテックス)コードを実行してみると第三引数でtrueが返ってきてることが分かります。

local-mutex

コンストラクタ内ではイベントに登録するメソッドを指定しています。引数にイベントを送信したオブジェクトとイベント引数を指定して発生したイベントを受け取れるようにします。

"App.xaml.cs"
/// <summary>
/// コンストラクタ
/// </summary>
public App()
{
  // 起動時のイベントにメソッドを登録
  Startup += Application_Startup;

  // アプリケーション終了時のイベントにメソッドを登録
  Exit += Application_Exit;
}


// ~ 中略 ~

/// <summary>
/// スタートアップ時のイベントに読み込ませるメソッド
/// </summary>
/// <param name="sender">イベントを送信したオブジェクト</param>
/// <param name="e">イベント引数</param>
private void Application_Startup(object sender, StartupEventArgs e)
{
  // 内容は省略
}

/// <summary>
/// アプリケーション終了時のイベントに登録するメソッド
/// </summary>
/// <param name="sender">イベントを送信したオブジェクト</param>
/// <param name="e">イベント引数</param>
private void Application_Exit(object sender, ExitEventArgs e)
{
  // 内容は省略
}

イベントハンドラに登録したメソッドの内容を詳しく見ていきます。

"App.xaml.cs"
private void Application_Startup(object sender, StartupEventArgs e)
{
  // ミューテックスの所有権を要求
  if (!_mutex.WaitOne(0, false))
  {
    // 既に起動しているため終了させる
    MessageBox.Show("ソフトウェアは既に起動しています。",
                    "二重起動防止",
                    MessageBoxButton.OK,
                    MessageBoxImage.Exclamation);

    // インスタンスが所有しているリソースを開放する
    _mutex.Close();

    // null を代入
    _mutex = null;

    // アプリケーションを終了させる
    Environment.Exit(0);
  }
}

private void Application_Exit(object sender, ExitEventArgs e)
{
  // mutexを開放しておかないと、mutexの所有権を得られずに二度と起動できなくなる
  if (_mutex != null)
  {
      // 共有リソースである Mutex を開放
      _mutex.ReleaseMutex();

      // インスタンスが所有しているリソースを開放する
      _mutex.Close();
  }
}

Application_Startup メソッドの実装でmutex.WaitOne(0, false)では第一引数に指定した間、スレッドをロックします。今回は待機する必要が無いので0秒を指定しロックせずに処理を行うことになります。単位はミリ秒です。

第二引数では実行されているコンテキストの再取得を待機時間の前に実行するかどうかを指定します(同期コンテキストの場合)。今回はfalseを指定しているので実行しません。

コンテキストとは、プログラムが実行されている環境の状態のことを言います。フラグメントのtrue or falseのような状態とは違い、少し抽象的な表現で理解が難しいですが参考として以下の記事を掲載しておきます。

mutex.WaitOne(0, false)で Mutex の所有権が取得できた場合はtrue。そうでない場合はfalseが返却されます。

多重起動していた場合は Mutex の所有権は取得できないのでfalseが返却され if 文の処理に入り、その後インスタンスを開放してアプリケーションを終了する流れになっています。

Application_Exit メソッドでは、_mutex.ReleaseMutex();によって Mutex を開放しています。

アプリケーション終了時に Mutex を開放していないと、次回起動時にアプリケーションが起動しなくなると言う記事を見かけたので、試しに開放するコードをコメントアウトして実行したところ普通に起動しました。気になったのでミューテックスに関するドキュメントを調べてみたところ以下のような記述がありました。

Mutex を解放せずにスレッドが終了すると、ミューテックスは破棄されたと見なされます。 ミューテックスが保護しているリソースが矛盾した状態で残る可能性があるため、多くの場合、これは重大なプログラミング エラーを示します。 ミューテックスを取得する次のスレッドで AbandonedMutexException がスローされます。 システム全体でミューテックスが有効な場合にミューテックスが破棄されたときは、アプリケーションが強制終了されたことを示している可能性があります (たとえば、Windows タスク マネージャを使用した終了)。

実行時のスレッドが終了した場合に Mutex が開放されていないと破棄されたとみなされるようです。次のスレッドで Mutex を取得しようとした場合に例外が発生するようですが、システムミューテックスが有効な場合はアプリケーション終了時に Mutex も破棄(開放)されるようなので、次回起動時に新しく Mutex が取得できるようです。

しかし、サンプルコードにもあるように Mutex を開放する処理は記述しておきます。

ここで再びスタートアップイベントに登録したメソッドの内容を見てみましょう。

"App.xaml.cs"
private void Application_Startup(object sender, StartupEventArgs e)
{
  // ミューテックスの所有権を要求
  if (!_mutex.WaitOne(0, false))
  {
    // 既に起動しているため終了させる
    MessageBox.Show("ソフトウェアは既に起動しています。",
                    "二重起動防止",
                    MessageBoxButton.OK,
                    MessageBoxImage.Exclamation);

    // インスタンスが所有しているリソースを開放する
    _mutex.Close();

    // null を代入
    _mutex = null;

    // アプリケーションを終了させる
    Environment.Exit(0);
  }
}

Exit イベントで登録したメソッドとは違い、mutex.ReleaseMutex()という Mutex を開放する処理は実行されていません。ここでは Mutex の所有権が得られなかった時の処理なので、ここでmutex.ReleaseMutex()を実行してしまうと例外が発生してしまします。

そのため、mutex.Close()は生成されたインスタンスが所有しているリソースを開放しているのであって、Mutex を開放している訳ではありません。

アプリケーション起動時に、もしシステムミューテックスが見つかれば生成されたインスタンスを開放してアプリケーションを終了しています。

Exit イベントで登録したメソッドにはmutex.ReleaseMutex()が実装されています。

"App.xaml.cs"
private void Application_Exit(object sender, ExitEventArgs e)
{

  if (_mutex != null)
  {
    // 共有リソースである mutex を開放
    _mutex.ReleaseMutex();

    // インスタンスが所有しているリソースを開放する
    _mutex.Close();
  }
}

最後にビルドしたアプリケーションの動作を確認します。

dual-boot-injunction

多重起動が禁止されていることが分かります。

今回確認用に作成したアプリケーションの GitHub リポジトです。

余談:イベント発火時のメソッドの登録

余談ですが、イベント発火時のメソッドの登録はApp.xaml側で指定する事もできます。

"App.xaml"
<Application x:Class="DotNET6_ReceiveMessageOpencvSharpCapture.App"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:DotNET6_ReceiveMessageOpencvSharpCapture"
            StartupUri="MainWindow.xaml"
            Startup="Application_Startup"
            Exit="Application_Exit"
             >
    <!-- ↑Startupイベント と Exitイベント にハンドラを登録する -->
    <Application.Resources>

    </Application.Resources>
</Application>

まとめ

  • Mutex クラスは排他制御を提供するクラス。
  • アプリケーションの多重起動を防止する際の Mutex の使い方は
    • アプリケーション固有の名前付き Mutex を作成する
    • WaitOneメソッドで Mutex の所有権を確認する
    • 所有権がなければ既にアプリケーションは起動しているので、後から起動したアプリケーションは終了する
    • Mutex の所有権を持っているアプリケーションが終了した場合は、Mutex を開放する。

参考