多執行緒中的鎖系統(四)-談談自旋鎖

蘑菇先生發表於2015-01-24

閱讀目錄:

  1. 基礎
  2. 自旋鎖示例
  3. SpinLock
  4. 繼續SpinLock
  5. 總結

基礎

核心鎖:基於核心物件構造的鎖機制,就是通常說的核心構造模式。使用者模式構造和核心模式構造

           優點:cpu利用最大化。它發現資源被鎖住,請求就排隊等候。執行緒切換到別處幹活,直到接受到可用訊號,執行緒再切回來繼續處理請求。

           缺點:託管程式碼->使用者模式程式碼->核心程式碼損耗、執行緒上下文切換損耗。

                   在鎖的時間比較短時,系統頻繁忙於休眠、切換,是個很大的效能損耗。

自旋鎖:原子操作+自迴圈。通常說的使用者構造模式。  執行緒不休眠,一直迴圈嘗試對資源訪問,直到可用。

           優點:完美解決核心鎖的缺點。

           缺點:長時間一直迴圈會導致cpu的白白浪費,高併發競爭下、CPU的消耗特別嚴重。

混合鎖:核心鎖+自旋鎖。 混合鎖是先自旋鎖一段時間或自旋多少次,再轉成核心鎖。

           優點:核心鎖和自旋鎖的折中方案,利用前二者優點,避免出現極端情況(自旋時間過長,核心鎖時間過短)。

           缺點: 自旋多少時間、自旋多少次,這些策略很難把控。 

           在作業系統及net框架層,這塊演算法策略做的已經非常優了,有些API函式也提供了時間及次數可配置項,讓使用者根據需求自行判斷。

自旋鎖示例

來看下我們自己簡單實現的自旋鎖:

        int signal = 0;
            var li = new List<int>();
            Parallel.For(0, 1000 * 10000, r =>
            {
                while (Interlocked.Exchange(ref signal, 1) != 0)//加自旋鎖
                {
                    //黑魔法
                }
                li.Add(r);
                Interlocked.Exchange(ref signal, 0);  //釋放鎖
            });
            Console.WriteLine(li.Count);
            //輸出:10000000

上面就是自旋鎖:Interlocked.Exchange+while

1:定義signal  0可用,1不可用。

2:Parallel模擬併發競爭,原子更改signal狀態。 後續執行緒自旋訪問signal,是否可用。

3:A執行緒使用完後,更改signal為0。 剩餘執行緒競爭訪問資源,B執行緒勝利後,更改signal為1,失敗執行緒繼續自旋,直到可用。

SpinLock

SpinLock是net4.0後Net提供的自旋鎖類庫,內部做了優化。

簡單看下例項:

  var li = new List<int>();
            var sl = new SpinLock();
            Parallel.For(0, 1000 * 10000, r =>
            {
                bool gotLock = false;     //釋放成功
                sl.Enter(ref gotLock);    //進入鎖
                li.Add(r);
                if (gotLock) sl.Exit();  //釋放
            });
            Console.WriteLine(li.Count);
            //輸出:10000000

 繼續SpinLock

new SpinLock(false)   這個建構函式主要用來檢查死鎖用,true是開啟。

在開啟狀態下,一旦發生死鎖會直接拋異常的。

SpinLock實現的部分原始碼:

  public void Enter(ref bool lockTaken) 
        {
            if (lockTaken) 
            { 
                lockTaken = false;
                throw new System.ArgumentException(Environment.GetResourceString("SpinLock_TryReliableEnter_ArgumentException")); 
            }

            // Fast path to acquire the lock if the lock is released
            // If the thread tracking enabled set the new owner to the current thread id 
            // Id not, set the anonymous bit lock
            int observedOwner = m_owner; 
            int newOwner = 0; 
            bool threadTrackingEnabled = (m_owner & LOCK_ID_DISABLE_MASK) == 0;
            if (threadTrackingEnabled) 
            {
                if (observedOwner == LOCK_UNOWNED)
                    newOwner = Thread.CurrentThread.ManagedThreadId;
            } 
            else if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
            { 
                newOwner = observedOwner | LOCK_ANONYMOUS_OWNED; // set the lock bit 
            }
            if (newOwner != 0) 
            {
#if !FEATURE_CORECLR
                Thread.BeginCriticalRegion();
#endif 

#if PFX_LEGACY_3_5 
                if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner) == observedOwner) 
                {
                    lockTaken = true; 
                    return;
                }
#else
                if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner) 
                {
                    // Fast path succeeded 
                    return; 
                }
#endif 
#if !FEATURE_CORECLR
                Thread.EndCriticalRegion();
#endif
            } 
            //Fast path failed, try slow path
            ContinueTryEnter(Timeout.Infinite, ref lockTaken); 
        } 
private void ContinueTryEnter(int millisecondsTimeout, ref bool lockTaken)
        { 
            long startTicks = 0; 
            if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout != 0)
            { 
                startTicks = DateTime.UtcNow.Ticks;
            }

#if !FEATURE_PAL && !FEATURE_CORECLR   // PAL doesn't support  eventing, and we don't compile CDS providers for Coreclr 
            if (CdsSyncEtwBCLProvider.Log.IsEnabled())
            { 
                CdsSyncEtwBCLProvider.Log.SpinLock_FastPathFailed(m_owner); 
            }
#endif 

            if (IsThreadOwnerTrackingEnabled)
            {
                // Slow path for enabled thread tracking mode 
                ContinueTryEnterWithThreadTracking(millisecondsTimeout, startTicks, 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 increaes 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;

            //***Step 1, take the lock or update the waiters
 
            // try to acquire the lock directly if possoble or update the waiters count
            SpinWait spinner = new SpinWait(); 
            while (true) 
            {
                observedOwner = m_owner; 
                if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
                {
#if !FEATURE_CORECLR
                    Thread.BeginCriticalRegion(); 
#endif
 
#if PFX_LEGACY_3_5 
                    if (Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner) == observedOwner)
                    { 
                        lockTaken = true;
                        return;
                    }
#else 
                    if (Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner, ref lockTaken) == observedOwner)
                    { 
                        return; 
                    }
#endif 

#if !FEATURE_CORECLR
                    Thread.EndCriticalRegion();
#endif 
                }
                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 || Interlocked.CompareExchange(ref m_owner, observedOwner + 2, observedOwner) == observedOwner) 
                        break;
 
                spinner.SpinOnce();
            }

            // Check the timeout. 
            if (millisecondsTimeout == 0 ||
                (millisecondsTimeout != Timeout.Infinite && 
                TimeoutExpired(startTicks, millisecondsTimeout))) 
            {
                DecrementWaiters(); 
                return;
            }

            //***Step 2. Spinning 
            //lock acquired failed and waiters updated
            int turn = ((observedOwner + 2) & WAITERS_MASK) / 2; 
            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)
                    { 
#if !FEATURE_CORECLR
                        Thread.BeginCriticalRegion();
#endif
 
                        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 PFX_LEGACY_3_5 
                        if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner) == observedOwner)
                        {
                            lockTaken = true;
                            return; 
                        }
#else 
                        if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner) 
                        {
                            return; 
                        }
#endif

#if !FEATURE_CORECLR 
                        Thread.EndCriticalRegion();
#endif 
                    } 
                }
            } 

            // Check the timeout.
            if (millisecondsTimeout != Timeout.Infinite && TimeoutExpired(startTicks, millisecondsTimeout))
            { 
                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) 
                { 
#if !FEATURE_CORECLR
                    Thread.BeginCriticalRegion(); 
#endif
                    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 PFX_LEGACY_3_5 
                    if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner) == observedOwner) 
                    {
                        lockTaken = true; 
                        return;
                    }
#else
                    if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner) 
                    {
                        return; 
                    } 
#endif
 
#if !FEATURE_CORECLR
                    Thread.EndCriticalRegion();
#endif
                } 

                if (yieldsoFar % SLEEP_ONE_FREQUENCY == 0) 
                { 
                    Thread.Sleep(1);
                } 
                else if (yieldsoFar % SLEEP_ZERO_FREQUENCY == 0)
                {
                    Thread.Sleep(0);
                } 
                else
                { 
#if PFX_LEGACY_3_5 
                    Platform.Yield();
#else 
                    Thread.Yield();
#endif
                }
 
                if (yieldsoFar % TIMEOUT_CHECK_FREQUENCY == 0)
                { 
                    //Check the timeout. 
                    if (millisecondsTimeout != Timeout.Infinite && TimeoutExpired(startTicks, millisecondsTimeout))
                    { 
                        DecrementWaiters();
                        return;
                    }
                } 

                yieldsoFar++; 
            } 
        }
 
        /// <summary>
        /// decrements the waiters, in case of the timeout is expired
        /// </summary>
        private void DecrementWaiters() 
        {
            SpinWait spinner = new SpinWait(); 
            while (true) 
            {
                int observedOwner = m_owner; 
                if ((observedOwner & WAITERS_MASK) == 0) return; // don't decrement the waiters if it's corrupted by previous call of Exit(false)
                if (Interlocked.CompareExchange(ref m_owner, observedOwner - 2, observedOwner) == observedOwner)
                {
                    Contract.Assert(!IsThreadOwnerTrackingEnabled); // Make sure the waiters never be negative which will cause the thread tracking bit to be flipped 
                    break;
                } 
                spinner.SpinOnce(); 
            }
 
        }
View Code

從程式碼中發現SpinLock並不是簡單的實現那樣一直自旋,其內部做了很多優化。  

1:內部使用了Interlocked.CompareExchange保持原子操作, m_owner 0可用,1不可用。

2:第一次獲得鎖失敗後,繼續呼叫ContinueTryEnter,ContinueTryEnter有三種獲得鎖的情況。 

3:ContinueTryEnter函式第一種獲得鎖的方式,使用了while+SpinWait。

4:第一種方式達到最大等待者數量後,命中走第二種。 繼續自旋 turn * 100次。100這個值是處理器核數(4, 8 ,16)下最好的。

5:第二種如果還不能獲得鎖,走第三種。這種就帶有混合構造的意思了,如下:

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

 Thread.Sleep(1) : 終止當前執行緒,放棄剩下時間片 休眠1毫秒, 退出跟其他執行緒搶佔cpu。當然這個一般會更多,系統無法保證這麼細的時間粒度。

 Thread.Sleep(0):  終止當前執行緒,放棄剩下時間片。  但立馬還會跟其他執行緒搶cpu,能不能搶到跟執行緒優先順序有關。

 Thread.Yeild():       結束當前執行緒,讓出CPU給其他準備好的執行緒。其他執行緒ok後或沒有還沒有準備好,繼續執行當前,Thread.Yeild()會返回個bool值,表示CPU是否讓出成功。

從原始碼中可以學到不少程式設計技巧,比如可以借鑑自旋+Thread.Yeild() 或 while+Thread.Yeild()等組合使用方式。

 總結

本章介紹了自旋鎖的基礎及樓主的經驗。 關於SpinLock類原始碼這塊,只簡單理解了下並沒有深究。

測試了下SpinLock和自己實現的自旋鎖效能對比(並行新增1000w List<int>()),SpinLock是單純的自旋鎖效能2倍以上。

另外測試了lock的效能,是系統SpinLock效能的3倍以上,可見lock內部自旋的效率更高,CLR暫沒開源,看不到CLR具體實現的程式碼。

參考http://www.projky.com/dotnet/4.0/System/Threading/SpinLock.cs.html

相關文章