天天看點

線程同步手記

一、前言

        線程同步其實很簡單,但是往往被老師教的很複雜。這是之前上課受的傷。腦袋瓜當人人家的跑馬場,被蹂躏一番,最後老師留下的是先入為主的錯誤,以至于後面不停的幹擾我的了解,糾起錯來,真是不知道浪費了多少精力。

二、什麼是線程同步

        一直想要找一個良好的方式來表達什麼是線程同步。

        先看一個模拟線程同步的圖:

   

線程同步手記

        假如這個盒子一次隻能放一個東西,并且接力賽又要保持順暢,該是怎樣的情景?

        首先對于Reader來說,取貨的時候,箱子必須有貨,如果沒有貨,要在旁邊等候;

        其次對于Writer來說,存貨的時候,箱子必須為空,如果不為空,也要在旁邊等候;

        兩個人要步調一緻,并且配合默契,才能順利的搬運東西。反過來,如果Reader執行了好幾次,Writer才執行一次,或者Write執行了好幾次,Reader才執行一次,最後都不能很好的保持步調的一緻。

        把兩個人看成是兩個線程,這時候線程要同步,也必須要滿足上面的要求。兩個線程要協同一緻的工作,才能完成一項任務。

三、代碼示範:

  public class ThreadSyn
     {
        //緩存區,假設一次隻能緩存一個字元
        private static char buffer;
        //線程1:寫操作
        public Thread thread1 = new Thread(()=>
                                        {
                                            string str = "橫看成嶺側成峰,遠近高低各不同。不識廬山真面目,隻緣身在此山中。";
                                           for (int i = 0; i < 32; i++)
                                           {
                                               buffer = str[i];
                                               Thread.Sleep(26);
                                           }
 
                                       });
 
        //線程2:讀操作
        public Thread thread2 = new Thread(() =>
                                        { 
                                            for (int i = 0; i < 32; i++)
                                            {
                                                char ch = buffer;
                                                Console.WriteLine(ch);
                                                Thread.Sleep(36);
                                            }
                                        });
        }
 
         public class Program
       {
          static void Main(string[] args)
          {
              ThreadSyn threadSyn=new ThreadSyn();
              threadSyn.thread1.Start();
              threadSyn.thread2.Start();
 
              Console.Read();
          } 
    }      

        運作效果圖:

  

線程同步手記

四、原因和方案:

        此時線程還是沒有協同工作。因為如果寫一個,讀一個,再寫一個,再讀一個,那麼這首詩應該是一首完整的顯示。但是效果圖的詩句卻是紊亂的。

        如何才能解決真正的同步,.net為我們提供了一系列的同步類。包括:互鎖(Interlocked),管程(Monitor)和互斥體(Mutex).

  4.1下面用互鎖來解決上面的問題。

  public class ThreadSyn
     {
        //緩存區
        private static char _buffer;
        //标示盒子,即緩沖區使用的空間,盒子初始化為0
        private static long _box = 0;
        //線程1:寫操作
        public Thread thread1 = new Thread(()=>
                                        {
                                           string str = "橫看成嶺側成峰,遠近高低各不同。不識廬山真面目,隻緣身在此山中。";
                                           for (int i = 0; i < 32; i++)
                                           {
                                               //寫入之前檢查緩沖區
                                               //如果緩沖區已滿,就進行等待,直到緩沖區的資料被程序讀取為止
                                               while(Interlocked.Read(ref _box) == 1)
                                               { 
                                                   Thread.Sleep(10);
                                               }
                                             
                                               //向緩沖區寫資料
                                               _buffer = str[i];
                                               //寫完資料,标記緩沖區已滿
                                               Interlocked.Increment(ref _box);
                                           }
                                       });
 
        //線程2:讀操作
        public Thread thread2 = new Thread(() =>
                                        {
                                            for (int j = 0; j < 32; j++)
                                            {
                                                //寫入之前檢查緩沖區
                                                //如果緩沖區為空,就進行等待,直到緩沖區的資料被程序填充為止
                                                while(Interlocked.Read(ref _box) == 0)
                                                {
                                                    Thread.Sleep(10);
                                                }
                                               
                                                //向緩沖區讀資料
                                                char ch = _buffer;
                                                Console.Write(ch);
                                                //讀完資料,标記緩沖區已空
                                                Interlocked.Decrement(ref _box);
                                            }
                                        });
    }      

    運作效果圖:

    

線程同步手記

        InterLocked提供了單個指令的操作,是以他提供了性能非常高的同步。

    

線程同步手記

4.2用Monitor來解決問題

  Monitor的原理是這樣的:先執行的線程,獨占鎖,進入臨界區,執行臨界區資源代碼。其他線程,隻能在集中在臨界資源上等待被叫喚。當獨占鎖推出資源區,也可以繼續讓自己等待,等待下一次被叫喚。

    //緩存區
        private static char _buffer;
        //用于同步的對象
        private static object _objForLock = new object();

        //線程1:寫操作
        public Thread thread1 = new Thread(() =>
        {
            string str = "橫看成嶺側成峰,遠近高低各不同。不識廬山真面目,隻緣身在此山中。";
            for (int i = 0; i < 32; i++)
            {
                try
                {
                    //進入臨界區,擷取獨占鎖
                    Monitor.Enter(_objForLock);

                    //向緩沖區寫資料
                    _buffer = str[i];
               
                    //寫完後,喚醒在臨界資源上睡眠的線程
                    Monitor.Pulse(_objForLock);

                    //讓目前線程睡眠在臨界資源上
                    Monitor.Wait(_objForLock);

                    //整個流程有點像輪班吃飯,
                    //第一個人先去吃飯,第二個在值班等待,第一個吃完了,喚醒第二個吃飯,自己則在等待下一次吃飯。
                }
                catch (ThreadInterruptedException ex)
                {   
                   Console.WriteLine("線程被中斷……");
                }
                finally
                {
                    //退出臨界區
                    Monitor.Exit(_objForLock);
                }
            }
        });

        //線程2:讀操作
        public Thread thread2 = new Thread(() =>
        {
            for (int j = 0; j < 32; j++)
            {
                try
                {
                    //進入臨界區,擷取獨占鎖
                    Monitor.Enter(_objForLock); 

                    //向緩沖區讀資料
                    char ch = _buffer;
                    Console.Write(ch);

                    //寫完後,喚醒在臨界資源上睡眠的線程
                    Monitor.Pulse(_objForLock);

                    //讓目前線程睡眠在臨界資源上
                    Monitor.Wait(_objForLock);

                }
                catch (ThreadInterruptedException ex)
                {   
                   Console.WriteLine("線程被中斷……");
                }
                finally
                {
                    //退出臨界區
                    Monitor.Exit(_objForLock);
                }
            }
        });      

  不同的是Monitor隻能鎖定引用類型的對象,值類型會被裝箱,等于生成另外一個對象,不能達到同步。為了保證推出臨界區資源得到釋放,使用了finally。為了友善使用,C#專門使用了lock語句。

  是以我們可以完全更簡潔的重寫上面的try{}finally{}中的關鍵代碼,如下所示:

          lock (_objForLock)
                {
                    //進入臨界區,擷取獨占鎖
                    Monitor.Enter(_objForLock);

                    //向緩沖區寫資料
                    _buffer = str[i];

                    //寫完後,喚醒在臨界資源上睡眠的線程
                    Monitor.Pulse(_objForLock);

                    //讓目前線程睡眠在臨界資源上
                    Monitor.Wait(_objForLock);  
                }      
線程同步手記

獨占鎖注意:

  因為獨占鎖,其他線程就不能再通路,隻有Lock結束後,其他線程才可以通路,這保證了通路的正确性。但是,如果有多個線程對同一個資源進行寫操作,在獨占鎖解開前,其他線程隻能被臨時暫停,這使得程式的效率大打折扣。是以應該慎用鎖,隻有必要時才使用。