マルチスレッドの排他制御に関するある記事で、数値を操作するだけであってもInterlocked
クラスよりlock
ステートメントの方がよい、と書かれていたのが感覚と異なったので、自分でも測ってみる。
単純なインクリメントを、Parallel.For
により並列実行したベンチマークを示す。(BenchmarkDotNet)
ベンチマークコード折りたたみ
[ShortRunJob] [HideColumns("Error", "StdDev", "Median", "RatioSD")] [ReturnValueValidator(failOnError: true)] public class ConcurrentCounterBenchmark { private abstract class ConcurrentCounterBase() { protected abstract void Increment(); protected abstract long GetCount(); // メソッド名:ベンチマーク用に挙動を変えたのに名前を変え忘れて、挙動と名前が合っていない命名になっています(整合性を欠いたりするとよくないのでそのままに) public long GetCountPerSecond() { var options = new ParallelOptions() { MaxDegreeOfParallelism = 8 }; const int N = 1 << 16; Parallel.For(0, N, options, _ => { Increment(); }); return GetCount(); } } private sealed class LockClass() : ConcurrentCounterBase { private long counter; private static readonly object obj = new(); protected override void Increment() { lock (obj) ++counter; } protected override long GetCount() => counter; } private sealed class InterlockedClass : ConcurrentCounterBase { private long counter; protected override void Increment() { Interlocked.Increment(ref counter); } protected override long GetCount() => counter; } private sealed class SpinLockClass : ConcurrentCounterBase { private long counter; private static SpinLock spinLock = new(); protected override void Increment() { bool lockTaken = false; try { spinLock.Enter(ref lockTaken); ++counter; } finally { if (lockTaken) spinLock.Exit(false); } } protected override long GetCount() => counter; } private sealed class SemaphoreClass : ConcurrentCounterBase { private long counter; private static readonly SemaphoreSlim semaphore = new(1, 1); protected override void Increment() { semaphore.Wait(); try { ++counter; } finally { semaphore.Release(); } } protected override long GetCount() => counter; } private sealed class MutexClass : ConcurrentCounterBase { private long counter; private static readonly Mutex mutex = new(); protected override void Increment() { mutex.WaitOne(); try { ++counter; } finally { mutex.ReleaseMutex(); } } protected override long GetCount() => counter; } private sealed class ThreadLocalClass : ConcurrentCounterBase { private readonly ThreadLocal<long> counter = new(true); protected override void Increment() { counter.Value++; } protected override long GetCount() => counter.Values.Sum(); } [Benchmark(Baseline = true)] public long Lock() => new LockClass().GetCountPerSecond(); [Benchmark] public long InterlockedIncrement() => new InterlockedClass().GetCountPerSecond(); [Benchmark] public long SpinLock() => new SpinLockClass().GetCountPerSecond(); [Benchmark] public long Semaphore() => new SemaphoreClass().GetCountPerSecond(); [Benchmark] public long Mutex() => new MutexClass().GetCountPerSecond(); [Benchmark] public long ThreadLocal() => new ThreadLocalClass().GetCountPerSecond(); }
| Method | Mean | Ratio | |--------------------- |-------------:|-------:|- | Lock | 2,656.4 μs | 1.00 | | InterlockedIncrement | 1,771.7 μs | 0.67 | | SpinLock | 28,084.6 μs | 10.58 | | Semaphore | 7,555.0 μs | 2.84 | | Mutex | 392,752.6 μs | 147.87 | | ThreadLocal | 349.4 μs | 0.13 |
このケースではInterlocked
利用の方が良くなった。
もちろん冒頭で挙げた記事と条件は異なるが、Interlocked
が単純にlock
に後れるわけでないことは確かめられた。
ThreadLocal<T>
ところで、カウントした値を出せればいいみたいな話ならThreadLocal<T>
を使うこともできる。
private sealed class ThreadLocalClass : ConcurrentCounterBase { private readonly ThreadLocal<long> counter = new(true); protected override void Increment() { counter.Value++; } protected override long GetCount() => counter.Values.Sum(); }
そもそも排他制御を行わず、スレッドごとの値を合計すればそれはカウント、という解法。
ベンチマークとしては条件が揃っていないけれど、実際は適した方法を選びたい。