且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

为什么每个人都指出,自旋锁更快?

更新时间:2023-02-05 12:06:06

您只是没有测试场景,其中自旋锁可以提高线程。后面的自旋锁的核心思想是,一个线程上下文切换是非常昂贵的操作,2000和10000 CPU周期之间成本核算。而且,如果它很可能是一个线程可以等待一个位(纺)获取锁定,则多余的周期烧毁等待可以通过避免线程上下文切换还清。



所以,基本要求是,锁持有很短的时间,这是真的你的情况。并有该锁能够获得合理的可能性。这是不是在你的情况属实,锁的按不低于24线程的兵家必争之地。所有纺纱而不必机会获得锁燃烧核心。



在此测试显示器将工作***的,因为它会将线程正在等待获取锁。他们被暂停,直到其中一人有一个的机会的锁被释放时获取锁,从等待队列释放。给他们所有的公平的机会来转一转,从而最大限度地赔率,他们将完成所有在同一时间。 Interlocked.Increment也不错,但不能提供一个公平的保障。



它可以是非常难以判断是否自旋锁是正确的做法前面,你必须测量。并发分析是正确的一种工具。


I have read a lot of docs and articles and posts all over the internet. Almost everyone and everywhere commits that SpinLock is faster for a short running pieces of code, but I made a test, and it appears to me that simple Monitor.Enter works faster than SpinLock.Enter (Test is compiled against .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;
    }
}

On a server with 24 cores of 2.5 GHz this application compiled with x64 produced the following results:

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

You are just not testing a scenario where SpinLock can improve the threading. The core idea behind a spin-lock is that a thread-context switch is very expensive operation, costing between 2000 and 10,000 cpu cycles. And that if it is likely that a thread can acquire a lock by waiting for a bit (spinning) then the extra cycles burned waiting can pay off by avoiding the thread context switch.

So basic requirements is that the lock is held for a very short time, which is true in your case. And that there are reasonable odds that the lock can be acquired. Which is not true in your case, the lock is heavily contested by no less than 24 threads. All spinning and burning core without having a chance to acquire the lock.

In this test Monitor will work best since it queues threads waiting to acquire the lock. They are suspended until one of them has a chance to acquire the lock, released from the wait queue when the lock is released. Giving them all a fair chance to take a turn, thus maximizing the odds that they'll all finish at the same time. Interlocked.Increment is not bad either but can't provide a fairness guarantee.

It can be pretty hard to judge whether Spinlock is the right approach up front, you have to measure. A concurrency analyzer is the right kind of tool.