分析.Net裡執行緒同步機制

風靈使發表於2019-02-20

我們知道並行程式設計模型兩種:一種是基於訊息式的,第二種是基於共享記憶體式的。 前段時間專案中遇到了第二種 使用多執行緒開發並行程式共享資源的問題 ,今天以實際案例出發對.net裡的共享記憶體式的執行緒同步機制做個總結,由於某些類庫的應用屬於基礎,所以本次不對基本使用做出講解,基本使用 MSDN是最好的教程。

一、volatile關鍵字

基本介紹: 封裝了 Thread.VolatileWrite()Thread.VolatileRead()的實現 ,主要作用是強制重新整理快取記憶體。

使用場景: 適用於在多核多CPU的機器上 解決變數在記憶體和快取記憶體同步不及時的問題。

案例:參考下文 二、原子操作的 案例 或者 System.Collections.Concurrent名稱空間下的 ConcurrentQueue ,ConcurrentDictionary等併發集合的實現方式。

二、原子操作(Interlock

基本介紹: 原子操作是 實現SpinlockMonitorReadWriterLock鎖的基礎,其實現原理是在計算機匯流排上標誌一個訊號來表示資源已經被佔用 如果其他指令進行修改則等待本次操作完成後才能進行,因為原子操作是在硬體上實現的 所以速度非常快,大約在50個時鐘週期。其實原子操作也可以看做一種鎖。

使用場景:效能要求較高的場合,需要對欄位進行快速的同步或者對變數進行原子形式的跟新操作(例如:int b=0; b=b+1 實際分解為多條彙編指令,在多執行緒情況下 多條彙編指令並行的執行可能導致錯誤的結果,所以要保證執行 b=b+1 生成的彙編指令是一個原子形式執行 ),例如實現一個並行佇列,非同步佇列等。

案例:一個基於事件觸發機制佇列的實現


/// <summary>
    /// 表示一個實時處理佇列
    /// </summary>
    public class ProcessQueue<T>
    {
         #region [成員]

        private ConcurrentQueue<IEnumerable<T>> queue;

        private Action<IEnumerable<T>> PublishHandler;

        //指定處理的執行緒數
        private int core = Environment.ProcessorCount;

        //正在執行的執行緒數
        private int runingCore = 0;

        public event Action<Exception> OnException;

        //佇列是否正在處理資料
        private int isProcessing=0; 

        //佇列是否可用
        private bool enabled = true;

        #endregion

        #region 建構函式

        public ProcessQueue(Action<IEnumerable<T>> handler)
        {

            queue = new ConcurrentQueue<IEnumerable<T>>();
        
            PublishHandler = handler;
            this.OnException += ProcessException.OnProcessException;
        }

        #endregion

        #region [方法]

        /// <summary>
        /// 入隊
        /// </summary>
        /// <param name="items">資料集合</param>
        public void Enqueue(IEnumerable<T> items)
        {
            if (items != null)
            {
                queue.Enqueue(items);
            }

            //判斷是否佇列有執行緒正在處理 
            if (enabled && Interlocked.CompareExchange(ref isProcessing, 1, 0) == 0)
            {
                if (!queue.IsEmpty)
                {
                    ThreadPool.QueueUserWorkItem(ProcessItemLoop);
                }
                else
                {
                    Interlocked.Exchange(ref isProcessing, 0);
                }
            }
        }

        /// <summary>
        /// 開啟佇列資料處理
        /// </summary>
        public void Start()
        {
            Thread process_Thread = new Thread(PorcessItem);
            process_Thread.IsBackground = true;
            process_Thread.Start();
        }

        /// <summary>
        /// 迴圈處理資料項
        /// </summary>
        /// <param name="state"></param>
        private void ProcessItemLoop(object state)
        {
            //表示一個執行緒遞迴 當處理完當前資料時 則開起執行緒處理佇列中下一條資料 遞迴終止條件是佇列為空時
            //但是可能會出現 佇列有資料但是沒有執行緒去處理的情況 所有一個監視執行緒監視佇列中的資料是否為空,如果為空
            //並且沒有執行緒去處理則開啟遞迴執行緒

            if (!enabled && queue.IsEmpty)
            {
                Interlocked.Exchange(ref isProcessing, 0);
                return;
            }

            //處理的執行緒數 是否小於當前CPU核數
            if (Thread.VolatileRead(ref runingCore) <= core * 2*)
            {
                IEnumerable<T> publishFrame;
                //出隊以後交給執行緒池處理
                if (queue.TryDequeue(out publishFrame))
                {
                    Interlocked.Increment(ref runingCore);
                    try
                    {
                        PublishHandler(publishFrame);
                        
                        if (enabled && !queue.IsEmpty)
                        {    
                            ThreadPool.QueueUserWorkItem(ProcessItemLoop);
                        }
                        else
                        {
                            Interlocked.Exchange(ref isProcessing, 0);
                        }
                        
                    }
                    catch (Exception ex)
                    {
                        OnProcessException(ex);
                    }

                    finally
                    {
                        Interlocked.Decrement(ref runingCore);
                    }
                }
            }

        }

        /// <summary>
        ///定時處理幀 執行緒呼叫函式  
        ///主要是監視入隊的時候執行緒 沒有來的及處理的情況
        /// </summary>
        private void PorcessItem(object state)
        {
            int sleepCount=0;
            int sleepTime = 1000;
            while (enabled)
            {
                //如果佇列為空則根據迴圈的次數確定睡眠的時間
                if (queue.IsEmpty)
                {
                    if (sleepCount == 0)
                    {
                        sleepTime = 1000;
                    }
                    else if (sleepCount == 3)
                    {
                        sleepTime = 1000 * 3;
                    }
                    else if (sleepCount == 5)
                    {
                        sleepTime = 1000 * 5;
                    }
                    else if (sleepCount == 8)
                    {
                        sleepTime = 1000 * 8;
                    }
                    else if (sleepCount == 10)
                    {
                        sleepTime = 1000 * 10;
                    }
                    else
                    {
                        sleepTime = 1000 * 50;
                    }
                    sleepCount++;
                    Thread.Sleep(sleepTime);
                }
                else
                {
                    //判斷是否佇列有執行緒正在處理 
                    if (enabled && Interlocked.CompareExchange(ref isProcessing, 1, 0) == 0)
                    {
                        if (!queue.IsEmpty)
                        {
                            ThreadPool.QueueUserWorkItem(ProcessItemLoop);
                        }
                        else
                        {
                            Interlocked.Exchange(ref isProcessing, 0);
                        }
                        sleepCount = 0;
                        sleepTime = 1000;
                    }
                }
            }
        }

        /// <summary>
        /// 停止佇列
        /// </summary>
        public void Stop()
        {
            this.enabled = false;

        }

        /// <summary>
        /// 觸發異常處理事件
        /// </summary>
        /// <param name="ex">異常</param>
        private void OnProcessException(Exception ex)
        {
            var tempException = OnException;
            Interlocked.CompareExchange(ref tempException, null, null);

            if (tempException != null)
            {
                OnException(ex);
            }
        }

        #endregion

    }

三、自旋鎖(Spinlock)

基本介紹: 在原子操作基礎上實現的鎖,使用者態的鎖,缺點是執行緒一直不釋放CPU時間片。作業系統進行一次執行緒使用者態到核心態的切換大約需要500個時鐘週期,可以根據這個進行參考我們的執行緒是進行使用者等待還是轉到核心的等待.。

使用場景:執行緒等待資源時間較短的情況下使用。

案例: 和最常用的Monitor 使用方法一樣 這裡就不舉例了,在實際場景中應該優先選擇使用Monitor,除非是執行緒等待資源的時間特別的短。

四、監視器(Monitor

基本介紹: 原子操作基礎上實現的鎖,開始處於使用者態,自旋一段時間進入核心態的等待釋放CPU時間片,缺點使用不當容易造成死鎖 c#實現的關鍵字是Lock

使用場景: 所有需要加鎖的場景都可以使用。

案例: 案例太多了,這裡就不列出了。

五、讀寫鎖(ReadWriterLock)

原理分析: 原子操作基礎上實現的鎖,

使用場景:適用於寫的次數少,讀的頻率高的情況。

案例:一個執行緒安全的快取實現(.net 4.0 可以使用基礎類庫中的 ConcurrentDictionary<K,V>) 注意:老版本ReaderWriterLock已經被淘汰,新版的是ReaderWriterLockSlim

class CacheManager<K, V>
    {
        #region [成員]

        private ReaderWriterLockSlim readerWriterLockSlim;

        private Dictionary<K, V> containter;

        #endregion

        #region [建構函式]

        public CacheManager()
        {
            this.readerWriterLockSlim = new ReaderWriterLockSlim();
            this.containter = new Dictionary<K, V>();
        }

        #endregion

        #region [方法]

        public void Add(K key, V value)
        {
            readerWriterLockSlim.EnterWriteLock();

            try
            {
                containter.Add(key, value);
            }

            finally
            {
                readerWriterLockSlim.ExitWriteLock();
            }
        }

        public V Get(K key)
        {

            bool result = false;
            V value;

            do
            {
                readerWriterLockSlim.EnterReadLock();

                try
                {
                    result = containter.TryGetValue(key, out value);
                }

                finally
                {
                    readerWriterLockSlim.ExitWriteLock();
                }

            } while (!result);

            return value;
        }

        #endregion
    }

.net中還有其他的執行緒同步機制:ManualResetEventSlimAutoResetEventSemaphoreSlim 這裡就逐個進行不介紹 具體在《CLR Via C# 》中解釋的非常詳細,但在具體的實際開發中我還沒有使用到。

最好的執行緒同步機制是沒有同步,這取決於良好的設計,當然有些情況下無法避免使用鎖。 在效能要求不高的場合基本的lock就能滿足要求,但效能要求比較苛刻的情就需求更具實際場景進行選擇哪種執行緒同步機制。

相關文章