System.Threading.Semaphoreの使い方

C#のセマフォの使い方です。

C#のセマフォは主に以下の2つのシーンで利用します。

  • 同時に度に実行できるスレッドの数を制限したい
  • 複数のスレッドの進行を同期したい

同時にXXXする数を制限したいという仕様があったときに効果を発揮します。大抵は、共有資源へのアクセス数はN個までのような使い方になると思います。1度に実行できるスレッド数を1つではなく、複数個指定できるため、6本スレッドが待機している状態から3スレッドだけを2回に分けて流す動作が実現できます。

また、特に重要な機能として、名前付きでセマフォを宣言した場合、異なるプロセスからも同じ資源を使うことができます。このためマルチプロセスかつマルチスレッドな環境下でもプロセスの違いを意識せずにセマフォを共有できます。

セマフォの説明と使い方

セマフォはざっくりと以下の仕様を持ちます。

  • 現在実行数をカウントするためのカウンタがある
    • カウンタが0なら実行できない
  • コンストラクタで
    • 同時に実行できる数を指定できる
    • 名前をつけると他プロセスと資源を共有できる
  • WaitOne()メソッドで
    • 実行できるようになるまでブロックで待機する
    • WaitOne()から抜けるとカウンタが1つ減る
  • Release()メソッドで
    • カウンタが1つ増える
    • Relase(N)でカウンタがN個増える

文字ばかりでしたので図にしてみました。スレッドがセマフォに4つ入った時の状態です。

f:id:Takachan:20170911225413p:plain

スレッドを2つ進めると以下のようになります。

f:id:Takachan:20170911225517p:plain

使い方

マルチプロセスで説明します。

まず、メインプロセスです。

using System;
using System.Threading;

namespace ConsoleApp_1
{
    /// <summary>
    /// メインプロセス
    /// </summary>
    internal class AppMain_1
    {
        public static void Main(string[] args)
        {
            // セマフォを作成
            var sem = new Semaphore(0, 2, "sem1");
            try
            {
                while (true)
                {
                    Console.WriteLine("エンターキーを押すとRelease(2)を実行します。"
                     + "\"end\"を入力すると終了します。");

                    string input = Console.ReadLine();
                    if (input == "end")
                    {
                        break;
                    }

                    // セマフォカウンタを2へ変更
                    Console.WriteLine(sem.Release(2));
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

            Console.WriteLine("--- End ---");
            Console.ReadLine();

            using (sem) { } // 資源を解放する
        }
    }
}

サブプロセスは以下の通りです。

using System;
using System.Threading;

namespace ConsoleApp_2
{
    /// <summary>
    /// サブプロセスを表します。
    /// </summary>
    internal class Program
    {
        public static void Main(string[] args)
        {
            // 別のプロセスで生成されたセマフォは以下の構文で取得できるか試行できる
            if (Semaphore.TryOpenExisting("sem1", out Semaphore sem))
            {
                // 取得できた
                Console.WriteLine("sem1が見つかりました。資源を確保できるまで待機します。");

                // 資源が使用可能になるまで待機する。
                sem.WaitOne();

                // WaitOneを抜けた場合セマフォカウンタが-1される
                Console.WriteLine("処理を実行しました。");
            }
            else
            {
                // OS上に資源が見つからない場合取得できない。
                // コンストラクタで見つからない場合新規にセマフォを作成することもできる
                Console.WriteLine("sem1は見つかりません。");
            }

            Console.ReadLine();
        }
    }
}

動かしてみる

上記コードを動かしてみます。メインプロセス1つに対してサブプロセスを5つ起動します。

Relaseに2を指定し、最初に2つスレッドが進む様子を確認します。

初期状態では以下のようになっていて、一番上のアクティブなメインプロセスでエンターキーを入力します。

f:id:Takachan:20170911230616p:plain

一番上と3番目が実行されました。順序がおかしいですがWaitOneした順にFIFOです。

f:id:Takachan:20170911230728p:plain

もう一度メインプロセスでエンターを入力すると2番目と4番目が実行されます。

f:id:Takachan:20170911230836p:plain

さいごに

コード例ですが、実際は結構例外が起きます。 例え、セマフォのカウンタが0の時にRelease(2)を実行すると以下のような例外が起きます。

> System.Threading.SemaphoreFullException: 指定されたカウントをセマフォに追加すると、カウントの最大値を超えます。

ここらへんの例外とハンドリングはリファレンスを参照しながら実際に動かしながら色々試してください。

「C# セマフォ」でググればMSDNが出てくると思います。

セマフォはかなり高度なシナリオでしか使う機会がないため、かなかお目にかかる機会もないかと思います。(そもそもほかプロセスのスレッドをシグナル状態にするなんて普通はあり得ないですし…)

が、使えると面白いのでぜひ試してみてください。