マルチスレッドで高速なC#を書くためのロック戦略

  • 52
    いいね
  • 2
    コメント

マルチスレッドで高速な実装を行うにはロックを避けては通れません。
この記事では色々なC#を使ってロック方法の紹介とベンチマーク結果をご紹介します。

ロック方法によるパフォーマンス差1

ロック方法 4スレッド 32スレッド
unsafe2 11,494,252 / 秒 6,578,947 / 秒
Interlocked 6,896,551 / 秒 3,215,434 / 秒
lock 4,000,000 / 秒 877,192 / 秒
SemaphoreSlim 1,104,972 / 秒 104,602 / 秒
Semaphore 319,284 / 秒 48,035 / 秒
Mutex 234,962 / 秒 37,199 / 秒

unsafeはロックを取得していないのでプログラマーの意図した動きにはなりませんが、ロックによるオーバーヘッドを見るために計測しました。

Interlocked > lock > SemaphoreSlim > Semaphore の順に早いようです。
とはいえロック方法によっては得手不得手があるので、使う状況をそれぞれ見ていきます。

Interlocked

int incrementedValue = Interlocked.Increment(ref intValue); // 安全にインクリメント

こんな感じで使います。
数値を単品で操作するケースはこれで十分でしょう。

lock

class X {
    object lockObject = new object();
    public void Work() {
        lock(lockObject) {
            // ロックの中
        }
    }
}

こんな感じです。
同じ引数のlock(){ ... }で囲った範囲同士であれば、シングルスレッドと同じ感覚で使えます。

SemaphoreSlim

class X {
    SemaphoreSlim sem = new SemaphoreSlim(1, 1);
    public async Task Work() {
        await sem.WaitAsync().ConfigureAwait(false);
        try {
            // ロックの中、awaitもok
        } finally {
            sem.Release();
        }
    }
}

lock方式だとロック中にawaitが使えないのですが3、こちらは使えます。
lock方式が使えるならそちらを、awaitを使うならこちらを選ぶと良さそうです。

Semaphore

class X {
    Semaphore sem = new Semaphore(1, 1);
    public void Work() {
        sem.WaitOne();
        try {
            // ロックの中、awaitもok
        } finally {
            sem.Release();
        }
    }
}

Semaphore方式はSemaphoreSlim方式と似てますが、awaitに対応していません。
速度ではlock方式に劣ります。
今回のベンチマークではこれを使うメリットは見い出せませんでした(^^;)

MSDNのSemaphoreSlimには以下の記載がありますので、Semaphoreは過去のものかもしれません(間違ってたらごめんなさい)。
名前付きセマフォとしてプロセス間でリソースをロックする場面でだけ使うと良いかもしれません。4

SemaphoreSlim クラス
リソースまたはリソースのプールに同時にアクセスできるスレッドの数を制限する Semaphore の軽量版を表します。

Mutex

class X {
    Mutex mut = new Mutex();
    public void Work() {
        mut.WaitOne();
        try {
            // ロックの中
        } finally {
            mut.ReleaseMutex();
        }
    }
}

使い方はSemaphoreとよく似ていて、こちらも名前付きにすると他プロセスからも見えるそうです。Semaphoreのように同時実行エントリ数を変更できない(1固定)のに、なぜかSemaphoreより性能が劣ります。

まとめと感想

Interlocked < lock < SemaphoreSlim < Semaphore < Mutex の順で高速。
特にスレッド数が増えると差も広がる傾向があります。
検証に使ったコード


  1. ベンチマーク環境
    CPU : Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
    実行回数: 1,000,000回 

  2. だめな例です 

  3. コンパイルエラーになります。wandboxでの実行結果
    awaitは呼び出す前後でスレッドが異なる可能性があります。lock()はスレッドアフィニティがある(ロック前後で同一スレッドであることを要求)し、SemaphoreSlimはスレッドアフィニティがない(ロック前後でスレッドは変わってもいい)からだそうです。 

  4. コメントで指摘を頂き変更しました
    名前付きセマフォはSemaphore(int initialCount, int maximumCount, string name)で使えるようです
    https://msdn.microsoft.com/ja-jp/library/54wk4yfd(v=vs.110).aspx
    laughterさん、教えて頂きありがとうございます!