C# SpinLock实现

关于SpinLock自旋锁网上已经有很多说明,这里也copy了一部分,我这里主要关注微软的实现,学习人家的实现方式。

如果由于垃圾回收,基于对象的锁对象开销太高,可以使用SpinLock结构。.NET 4以后版本可使用,如果你有很多个锁(如,一个列表里面的每一个节点)并且锁时间通常非常的短,使用SpinLock将很有用。你需要避免使用超过一个的SpinLock,并且不要调用任何可能阻塞的。除了架构不同,SpinLock的使用同Monitor类非常相似。通过Enter或者TryEnter请求锁,并通过Exit释放锁。SpinLock同样也通过两个属性来提供关于它当前是否已锁的信息:IsHeld和IsHeldByCurrentThread.

不要将SpinLock声明为只读字段,如果声明为只读字段,会导致每次调用都会返回一个SpinLock新副本,在多线程下,每个方法都会成功获得锁,而受到保护的临界区不会按照预期进行串行化。

SpinLock 仅当您确定这样做可以改进应用程序的性能之后才能使用另外,务必请注意 SpinLock 是一个值类型(出于性能原因)。因此,您必须非常小心,不要意外复制了 SpinLock 实例,因为两个实例(原件和副本)之间完全独立,这可能会导致应用程序出现错误行为。如果必须传递 SpinLock 实例,则应该通过引用而不是通过值传递

    [HostProtection(Synchronization = true, ExternalThreading = true)]
    public struct SpinLock
    {
       private volatile int m_owner;
       private const int SPINNING_FACTOR = 100;

        // After how many yields, call Sleep(1)
        private const int SLEEP_ONE_FREQUENCY = 40;

        // After how many yields, call Sleep(0)
        private const int SLEEP_ZERO_FREQUENCY = 10;

        // After how many yields, check the timeout
        private const int TIMEOUT_CHECK_FREQUENCY = 10;

        // Thr thread tracking disabled mask
        private const int LOCK_ID_DISABLE_MASK = unchecked((int)0x80000000);  
       
           public SpinLock(bool enableThreadOwnerTracking)
        {
            m_owner = LOCK_UNOWNED;
            if (!enableThreadOwnerTracking)
            {
                m_owner |= LOCK_ID_DISABLE_MASK;
                Contract.Assert(!IsThreadOwnerTrackingEnabled, "property should be false by now");
            }
        }
        
        public void TryEnter(ref bool lockTaken)
        {
            TryEnter(0, ref lockTaken);
        }
        public void TryEnter(int millisecondsTimeout, ref bool lockTaken)
        {
            int observedOwner = m_owner;
            if (millisecondsTimeout < -1 || //invalid parameter
                lockTaken || //invalid parameter
                (observedOwner & ID_DISABLED_AND_ANONYMOUS_OWNED) != LOCK_ID_DISABLE_MASK ||  //thread tracking is enabled or the lock is already acquired
                Interlocked.CompareExchange(ref m_owner, observedOwner | LOCK_ANONYMOUS_OWNED, observedOwner, ref lockTaken) != observedOwner) // acquiring the lock failed
                ContinueTryEnter(millisecondsTimeout, ref lockTaken); // The call the slow pth
        }
    
        private void ContinueTryEnter(int millisecondsTimeout, ref bool lockTaken)
        {         
            uint startTime = 0;
            if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout != 0)
            {
                startTime = TimeoutHelper.GetTime();
            }
            if (IsThreadOwnerTrackingEnabled)
            {
                // Slow path for enabled thread tracking mode
                ContinueTryEnterWithThreadTracking(millisecondsTimeout, startTime, ref lockTaken);
                return;
            }

            // then thread tracking is disabled
            // In this case there are three ways to acquire the lock
            // 1- the first way the thread either tries to get the lock if it's free or updates the waiters, if the turn >= the processors count then go to 3 else go to 2
            // 2- In this step the waiter threads spins and tries to acquire the lock, the number of spin iterations and spin count is dependent on the thread turn
            // the late the thread arrives the more it spins and less frequent it check the lock avilability
            // Also the spins count is increases each iteration
            // If the spins iterations finished and failed to acquire the lock, go to step 3
            // 3- This is the yielding step, there are two ways of yielding Thread.Yield and Sleep(1)
            // If the timeout is expired in after step 1, we need to decrement the waiters count before returning

            int observedOwner;
            int turn = int.MaxValue;
            //***Step 1, take the lock or update the waiters

            // try to acquire the lock directly if possible or update the waiters count
            observedOwner = m_owner;
            if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
            {
                if (Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner, ref lockTaken) == observedOwner)
                {
                    return;
                }
            }
            else //failed to acquire the lock,then try to update the waiters. If the waiters count reached the maximum, jsut break the loop to avoid overflow
            {
                if ((observedOwner & WAITERS_MASK) != MAXIMUM_WAITERS)
                    turn = (Interlocked.Add(ref m_owner, 2) & WAITERS_MASK) >> 1 ;
            }
            // Check the timeout.
            if (millisecondsTimeout == 0 ||
                (millisecondsTimeout != Timeout.Infinite &&
                TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0))
            {
                DecrementWaiters();
                return;
            }

            //***Step 2. Spinning
            //lock acquired failed and waiters updated
            int processorCount = PlatformHelper.ProcessorCount;
            if (turn < processorCount)
            {
                int processFactor = 1;
                for (int i = 1; i <= turn * SPINNING_FACTOR; i++)
                {
                    Thread.SpinWait((turn + i) * SPINNING_FACTOR * processFactor);
                    if (processFactor < processorCount)
                        processFactor++;
                    observedOwner = m_owner;
                    if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
                    {
                        int newOwner = (observedOwner & WAITERS_MASK) == 0 ? // Gets the number of waiters, if zero
                            observedOwner | 1 // don't decrement it. just set the lock bit, it is zzero because a previous call of Exit(false) ehich corrupted the waiters
                            : (observedOwner - 2) | 1; // otherwise decrement the waiters and set the lock bit
                        Contract.Assert((newOwner & WAITERS_MASK) >= 0);

                        if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner)
                        {
                            return;
                        }
                    }
                }
            }

            // Check the timeout.
            if (millisecondsTimeout != Timeout.Infinite && TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0)
            {
                DecrementWaiters();
                return;
            }

            //*** Step 3, Yielding
            //Sleep(1) every 50 yields
            int yieldsoFar = 0;
            while (true)
            {
                observedOwner = m_owner;
                if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
                {
                    int newOwner = (observedOwner & WAITERS_MASK) == 0 ? // Gets the number of waiters, if zero
                           observedOwner | 1 // don't decrement it. just set the lock bit, it is zzero because a previous call of Exit(false) ehich corrupted the waiters
                           : (observedOwner - 2) | 1; // otherwise decrement the waiters and set the lock bit
                    Contract.Assert((newOwner & WAITERS_MASK) >= 0);

                    if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner)
                    {
                        return;
                    }
                }
                if (yieldsoFar % SLEEP_ONE_FREQUENCY == 0)
                {
                    Thread.Sleep(1);
                }
                else if (yieldsoFar % SLEEP_ZERO_FREQUENCY == 0)
                {
                    Thread.Sleep(0);
                }
                else
                {
                    Thread.Yield();
                }
                if (yieldsoFar % TIMEOUT_CHECK_FREQUENCY == 0)
                {
                    //Check the timeout.
                    if (millisecondsTimeout != Timeout.Infinite && TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0)
                    {
                        DecrementWaiters();
                        return;
                    }
                }
                yieldsoFar++;
            }
        }

        private void ContinueTryEnterWithThreadTracking(int millisecondsTimeout, uint startTime, ref bool lockTaken)
        {
            Contract.Assert(IsThreadOwnerTrackingEnabled);
            int lockUnowned = 0;
            // We are using thread IDs to mark ownership. Snap the thread ID and check for recursion.
            // We also must or the ID enablement bit, to ensure we propagate when we CAS it in.
            int m_newOwner = Thread.CurrentThread.ManagedThreadId;
            if (m_owner == m_newOwner)
            {
                // We don't allow lock recursion.
                throw new LockRecursionException(Environment.GetResourceString("SpinLock_TryEnter_LockRecursionException"));
            }
            SpinWait spinner = new SpinWait();
            // Loop until the lock has been successfully acquired or, if specified, the timeout expires.
            do
            {
                // We failed to get the lock, either from the fast route or the last iteration
                // and the timeout hasn't expired; spin once and try again.
                spinner.SpinOnce();
                // Test before trying to CAS, to avoid acquiring the line exclusively unnecessarily.
                if (m_owner == lockUnowned)
                {
                    if (Interlocked.CompareExchange(ref m_owner, m_newOwner, lockUnowned, ref lockTaken) == lockUnowned)
                    {
                        return;
                    }
                }
                // Check the timeout.  We only RDTSC if the next spin will yield, to amortize the cost.
                if (millisecondsTimeout == 0 ||
                    (millisecondsTimeout != Timeout.Infinite && spinner.NextSpinWillYield &&
                    TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0))
                {
                    return;
                }
            } while (true);
        }
        public void Exit()
        {
            //This is the fast path for the thread tracking is disabled, otherwise go to the slow path
            if ((m_owner & LOCK_ID_DISABLE_MASK) == 0)
                ExitSlowPath(true);
            else
                Interlocked.Decrement(ref m_owner);
        }
        
        private void ExitSlowPath(bool useMemoryBarrier)
        {
            bool threadTrackingEnabled = (m_owner & LOCK_ID_DISABLE_MASK) == 0;
            if (threadTrackingEnabled && !IsHeldByCurrentThread)
            {
                throw new System.Threading.SynchronizationLockException(
                    Environment.GetResourceString("SpinLock_Exit_SynchronizationLockException"));
            }

            if (useMemoryBarrier)
            {
                if (threadTrackingEnabled)
                    Interlocked.Exchange(ref m_owner, LOCK_UNOWNED);
                else
                    Interlocked.Decrement(ref m_owner);

            }
            else
            {
                if (threadTrackingEnabled)
                    m_owner = LOCK_UNOWNED;
                else
                {
                    int tmpOwner = m_owner;
                    m_owner = tmpOwner & (~LOCK_ANONYMOUS_OWNED);
                }

            }

        }
        public bool IsHeld
        {           
            get
            {
                if (IsThreadOwnerTrackingEnabled)
                    return m_owner != LOCK_UNOWNED;
                return (m_owner & LOCK_ANONYMOUS_OWNED) != LOCK_UNOWNED;
            }
        }
        public bool IsHeldByCurrentThread
        {
            get
            {
                if (!IsThreadOwnerTrackingEnabled)
                {
                    throw new InvalidOperationException(Environment.GetResourceString("SpinLock_IsHeldByCurrentThread"));
                }
                return ((m_owner & (~LOCK_ID_DISABLE_MASK)) == Thread.CurrentThread.ManagedThreadId);
            }
        }
    }

SpinLock 构造函数有一个bool enableThreadOwnerTracking参数用来表示是否跟踪线程,如果为true,那么在获取锁以后变量m_owner就是线程ManagedThreadId属性,否者为1,因为获取锁的修改 observedOwner | 1 ,就相当于m_owner设为1,在释放锁的时候m_owner减1Interlocked.Decrement(ref m_owner) 或者设置为 Interlocked.Exchange(ref m_owner, LOCK_UNOWNED)。

SpinLock的核心方法Enter和TryEnter最终都是调用ContinueTryEnter方法,该方法首先检查IsThreadOwnerTrackingEnabled是否启用线程跟踪,如果启用就调用ContinueTryEnterWithThreadTracking方法,ContinueTryEnterWithThreadTracking方法里面实例化了一个SpinWait,然后自旋获取锁,这里也是借用原子操作【Interlocked.CompareExchange(ref m_owner, m_newOwner, lockUnowned, ref lockTaken)】,如果没有启用跟踪,那么ContinueTryEnter将分3不走,就像里面的注释描述的那样;case 1通过原子操作【Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner, ref lockTaken) == observedOwner)】直接获取锁,如果失败进入到case2【turn < processorCount】,然后在循环尝试获取锁,每次循环都会调用 Thread.SpinWait方法等待;获取锁还是通过原子操作,如果失败,则进入case3,该case也是循环等待,在循环体里面不在是 Thread.SpinWait而是  Thread.Yield();和    Thread.Sleep(0);

if (yieldsoFar % 40 == 0) 
                    Thread.Sleep(1);
                else if (yieldsoFar % 10 == 0)
                    Thread.Sleep(0);
                else
                    Thread.Yield();

看到这个代码 是不是和 SpinWait相似啊。可以总结以下,case1 直接尝试获取锁,case2 循环中通过调用Thread.SpinWait 尝试获取锁【当前线程不会让出CPU】, case3循环中通过Thread.Yield()和Thread.Sleep来尝试获取锁

 Exit的方法实现就非常简单了,主要是调用ExitSlowPath,说白了就是把变量m_owner还原为初始值

 

 

posted on 2017-11-24 18:41  dz45693  阅读(1599)  评论(0编辑  收藏  举报

导航