9

私はインターネット上で多くのドキュメントや記事、投稿を読みました。ほとんどすべての人が、短い実行コードでは SpinLock の方が高速であると主張していますが、私がテストを行ったところ、単純な Monitor.Enter は SpinLock.Enter よりも高速に動作するように見えます (テストは .NET 4.5 に対してコンパイルされています)。

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Linq;
using System.Globalization;
using System.ComponentModel;
using System.Threading;
using System.Net.Sockets;
using System.Net;

class Program
{
    static int _loopsCount = 1000000;
    static int _threadsCount = -1;

    static ProcessPriorityClass _processPriority = ProcessPriorityClass.RealTime;
    static ThreadPriority _threadPriority = ThreadPriority.Highest;

    static long _testingVar = 0;


    static void Main(string[] args)
    {
        _threadsCount = Environment.ProcessorCount;

        Console.WriteLine("Cores/processors count: {0}", Environment.ProcessorCount);

        Process.GetCurrentProcess().PriorityClass = _processPriority;

        TimeSpan tsInterlocked = ExecuteInterlocked();
        TimeSpan tsSpinLock = ExecuteSpinLock();
        TimeSpan tsMonitor = ExecuteMonitor();

        Console.WriteLine("Test with interlocked: {0} ms\r\nTest with SpinLock: {1} ms\r\nTest with Monitor: {2} ms",
            tsInterlocked.TotalMilliseconds,
            tsSpinLock.TotalMilliseconds,
            tsMonitor.TotalMilliseconds);

        Console.ReadLine();
    }

    static TimeSpan ExecuteInterlocked()
    {
        _testingVar = 0;

        ManualResetEvent _startEvent = new ManualResetEvent(false);
        CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);

        Thread[] threads = new Thread[_threadsCount];

        for (int i = 0; i < threads.Length; i++)
        {
            threads[i] = new Thread(() =>
                {
                    _startEvent.WaitOne();

                    for (int j = 0; j < _loopsCount; j++)
                    {
                        Interlocked.Increment(ref _testingVar);
                    }

                    _endCountdown.Signal();
                });

            threads[i].Priority = _threadPriority;
            threads[i].Start();
        }

        Stopwatch sw = Stopwatch.StartNew();

        _startEvent.Set();
        _endCountdown.Wait();

        return sw.Elapsed;
    }

    static SpinLock _spinLock = new SpinLock();

    static TimeSpan ExecuteSpinLock()
    {
        _testingVar = 0;

        ManualResetEvent _startEvent = new ManualResetEvent(false);
        CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);

        Thread[] threads = new Thread[_threadsCount];

        for (int i = 0; i < threads.Length; i++)
        {
            threads[i] = new Thread(() =>
            {
                _startEvent.WaitOne();

                bool lockTaken;

                for (int j = 0; j < _loopsCount; j++)
                {
                    lockTaken = false;

                    try
                    {
                        _spinLock.Enter(ref lockTaken);

                        _testingVar++;
                    }
                    finally
                    {
                        if (lockTaken)
                        {
                            _spinLock.Exit();
                        }
                    }
                }

                _endCountdown.Signal();
            });

            threads[i].Priority = _threadPriority;
            threads[i].Start();
        }

        Stopwatch sw = Stopwatch.StartNew();

        _startEvent.Set();
        _endCountdown.Wait();

        return sw.Elapsed;
    }

    static object _locker = new object();

    static TimeSpan ExecuteMonitor()
    {
        _testingVar = 0;

        ManualResetEvent _startEvent = new ManualResetEvent(false);
        CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);

        Thread[] threads = new Thread[_threadsCount];

        for (int i = 0; i < threads.Length; i++)
        {
            threads[i] = new Thread(() =>
            {
                _startEvent.WaitOne();

                bool lockTaken;

                for (int j = 0; j < _loopsCount; j++)
                {
                    lockTaken = false;

                    try
                    {
                        Monitor.Enter(_locker, ref lockTaken);

                        _testingVar++;
                    }
                    finally
                    {
                        if (lockTaken)
                        {
                            Monitor.Exit(_locker);
                        }
                    }
                }

                _endCountdown.Signal();
            });

            threads[i].Priority = _threadPriority;
            threads[i].Start();
        }

        Stopwatch sw = Stopwatch.StartNew();

        _startEvent.Set();
        _endCountdown.Wait();

        return sw.Elapsed;
    }
}

2.5 GHz の 24 コアを搭載したサーバーで、このアプリケーションを x64 でコンパイルすると、次の結果が得られました。

Cores/processors count: 24
Test with interlocked: 1373.0829 ms
Test with SpinLock: 10894.6283 ms
Test with Monitor: 1171.1591 ms
4

1 に答える 1

31

SpinLock がスレッド化を改善できるシナリオをテストしていないだけです。スピンロックの背後にある中心的な考え方は、スレッドコンテキストの切り替えは非常にコストのかかる操作であり、2000 ~ 10,000 CPU サイクルのコストがかかるということです。また、スレッドがビットを待機する (スピンする) ことでロックを取得できる可能性が高い場合は、スレッド コンテキストの切り替えを回避することで、待機によって消費される余分なサイクルが報われる可能性があります。

したがって、基本的な要件は、ロックが非常に短時間保持されることです。これは、あなたの場合に当てはまります。そして、ロックを取得できる合理的な可能性があること。あなたの場合はそうではありませんが、ロックは24以上のスレッドによって激しく争われています。ロックを取得する機会がなくても、すべての回転と燃焼のコア。

このテストでは、ロックの取得を待機しているスレッドをキューに入れるため、Monitor が最適に機能します。それらのいずれかがロックを取得する機会が得られるまで中断され、ロックが解放されると待機キューから解放されます。ターンを取る公平な機会を全員に与えることで、全員が同時に終了する可能性を最大化します。Interlocked.Increment も悪くはありませんが、公平性を保証することはできません。

スピンロックが適切なアプローチであるかどうかを判断するのは非常に難しい場合があります。測定する必要があります。コンカレンシー アナライザーは適切な種類のツールです。

于 2013-01-30T20:33:32.337 に答える