为什么大家都说自旋锁更快?

10

我已经阅读了很多网络文档、文章和帖子。几乎每个人和每个地方都声称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;
    }
}

在一台拥有24个2.5GHz核心的服务器上,使用x64编译后,该应用程序产生了以下结果:

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

欢迎来到StackOverflow!有几件事情需要注意,请先阅读FAQ。其次,您是否有问题需要提出?最后,这种类型的帖子似乎更适合发布在codeReview.stackexchange上。 - Sam Axe
2
@Dan-o 我不明白为什么这会适合CR。Rauf似乎并没有要求改进他的代码。 - svick
Rauf 似乎正在请求代码审查,这不是 StackOverflow 的服务范围。我可能有所误解。请随意回答。 - Sam Axe
各位,我想问的问题是,在哪种情况下,如果即使在一个简单的情况下(如操作是一个简单的变量增量),使用自旋锁可能比使用监视器更好,会导致自旋锁出现延迟? - Rauf
2
你没有使用正确的SpinLock构造函数。你使用了默认构造函数,启用了enableThreadOwnerTracking = true。这使得SpinLock的性能比Monitor.Enter差。相反,应该将其改为new SpinLock(false)。此外,你应该使用_spinLock.Exit(false)。 - Rolf Kristensen
1个回答

32

您只是没有测试SpinLock可以改善线程的情况。旋转锁背后的核心思想是,线程上下文切换是一种非常昂贵的操作,成本介于2000到10000个CPU周期之间。如果有可能通过等待一段时间(旋转)来获取锁,则额外的等待循环可以通过避免线程上下文切换而得到回报。

因此,基本要求是锁定持续时间非常短,在您的情况下是正确的。并且有合理的机会可以获得锁。而在您的情况下不是真的,锁由不少于24个在线程争夺,所有线程都在旋转和烧核心而没有机会获得锁。

在这个测试中,Monitor将最好地工作,因为它排队等待获取锁的线程。它们被暂停,直到其中一个有机会获取锁,并在释放锁时从等待队列中释放。让他们有公平的机会轮流,从而最大限度地提高他们同时完成的概率。Interlocked.Increment也不错,但不能提供公平性保证。

事先很难判断旋转锁是否是正确的方法,您必须进行测量。并发分析器是正确类型的工具。


1
那么,你的意思是在特定的环境配置和特定的代码片段中很难预测更好的方法?唯一的方法就是在执行缓慢的情况下测试代码。对吗? - Rauf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接