多線程總結之旅(6):線程同步之臨界區

   爲何使用線程同步?
  現在的應用程序愈來愈複雜,咱們經常須要多線程技術來提升咱們應用程序的響應速度,建立一個線程是不能提升程序的執行效率的,因此要建立多個線程。每一個線程都由本身的線程ID,當前指令指針(PC),寄存器集合和堆棧組成,但代碼區是共享的,即不一樣的線程能夠執行一樣的函數。因此在併發環境中,多個線程同時對同一個內存地址進行 寫入,因爲CPU時間調度上的問題,寫入數據會被屢次的覆蓋,會形成共享數據損壞,因此就要使線程同步。 (若是多個線程同時對共享數據只進行只讀訪問是不須要進行同步的)
 
  線程同步帶來的問題?
  

  在併發的環境裏,「線程同步鎖」能夠保護共享數據,可是也會存在一些問題:html

  1)   實現比較繁瑣,並且容易錯漏。你必須標識出可能由多個線程訪問的全部共享數據。而後,必須爲其獲取和釋放一個線程同步瑣,而且保證已經正確爲全部共享資源添加了鎖定代碼。數據庫

  2)   因爲臨界區沒法併發運行,進入臨界區就須要等待,加鎖帶來效率的下降。express

  3)   在複雜的狀況下,很容易形成死鎖,併發實體之間無止境的互相等待。多線程

  4)   優先級倒置形成實時系統不能正常工做。優先級低的進程拿到高優先級進程須要的鎖,結果是高/低優先級的進程都沒法運行,中等優先級的進程可能在狂跑。併發

  5)   當線程池中一個線程被阻塞時,可能形成線程池根據CPU使用狀況誤判建立更多的線程以便執行其餘任務,然而新建立的線程也可能因請求的共享資源而被阻塞,惡性循環,徒增線程上下文切換的次數,而且下降了程序的伸縮性。(這一點很重要)dom

 

  .NET下線程同步的方法?
 
  線程同步:即當有一個線程在對內存進行操做時,其餘線程都不能夠對這個內存地址進行操做,直到該線程完成操做, 其餘線程才能對該內存地址進行操做,而其餘線程又處於等待狀態,目前實現線程同步的方法有不少,其中包括 臨界區、互斥量、事件、信號量四種方式。
 
  我先介紹一下這四種方法的概念,主要是品味他們實現線程同步的不一樣之處在哪裏?
  

  1.臨界區:經過對多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問。(臨界區能夠認爲是操做共享資源的一段代碼函數

  2.互斥量:爲協調共同對一個共享資源的單獨訪問而設計的。ui

  3.信號量:爲控制一個具備有限數量用戶資源而設計。this

  4.事 件:用來通知線程有一些事件已發生,從而啓動後繼任務的開始spa

   
 
  若是沒有品味出來東東,咱們逐一詳細介紹四種方式。。。。。。。
 
 
1、臨界區:
  適用範圍:它只能同步一個進程中的線程,不能跨進程同步。通常用它來作單個進程內的代碼快同步,效率比較高。
  經過對多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問。在任意時刻只容許一個線程對共享資源進行訪問,若是有多個線程試圖訪問公共資源,那麼在有一個線程進入後,其餘試圖訪問公共資源的線程將被掛起,並一直等到進入臨界區的線程離開,臨界區在被釋放後,其餘線程才能夠搶佔。
  實現臨界區的方式包括 Lock、Monitor類、ReaderWriterLock類以及標誌量
 
  (1)Lock
    (1.1)lock的使用形式:lock(expression) embedded-statement,   即lock   (   表達式   )   嵌入語句
        注意:lock 語句的表達式必須表示一個引用類型的值。永遠不會爲 lock 語句中的表達式執行隱式裝箱轉換,所以,若是該表達式表示的是一個值類型的值,則會致使一個編譯時錯誤。
     (1.2)lock的實質。
        lock (x) 等價於如下代碼(其中x是引用類型)
        
        system.threading.monitor.enter(x);
        try {
             ...
          }
        finally 
          {           system.threading.monitor.exit(x);           }

 

     (1.3)lock什麼對象。
 
       lock什麼對象呢?
      (a)lock引用類型
      (b) 某些系統類提供專門用於鎖定的成員。例如,array 類型提供 syncroot。許多集合類型也提供 syncroot。

      (c)自定義類推薦用私有的只讀靜態對象,好比:private static readonly object obj = new object();爲何要設置成只讀的呢?這時由於若是在lock代碼段中改變obj的值,其它線程就暢通無阻了,由於互斥鎖的對象變了,object.referenceequals必然返回false。(推薦的方式

 
       爲何只能lock引用類型?

      (a)爲何不能lock值類型,好比lock(1)呢?lock本質上monitor.enter,monitor.enter會使值類型裝箱,每次lock的是裝箱後的對象。lock實際上是相似編譯器的語法糖,所以編譯器直接限制住不能lock值類型。

      (b)退一萬步說,就算能編譯器容許你lock(1),可是object.referenceequals(1,1)始終返回false(由於每次裝箱後都是不一樣對象),也就是說每次都會判斷成未申請互斥鎖,這樣在同一時間,別的線程照樣可以訪問裏面的代碼,達不到同步的效果。同理lock((object)1)也不行。

      (c)那麼lock("xxx")字符串呢?msdn上的原話是:鎖定字符串尤爲危險,由於字符串被公共語言運行庫 (clr)「暫留」。 這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了全部運行的應用程序域的全部線程中的該文本。所以,只要在應用程序進程中的任何位置處具備相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的全部實例。

      (d)一般,最好避免鎖定 public 類型或鎖定不受應用程序控制的對象實例。例如,若是該實例能夠被公開訪問,則 lock(this) 可能會有問題,由於不受控制的代碼也可能會鎖定該對象。這可能致使死鎖,即兩個或更多個線程等待釋放同一對象。出於一樣的緣由,鎖定公共數據類型(相比於對象)也可能致使問題。並且lock(this)只對當前對象有效,若是多個對象之間就達不到同步的效果。

      (e)lock(typeof(class))與鎖定字符串同樣,範圍太廣了。

    (1.4)示例

/*
該實例是一個線程中lock用法的經典實例,使獲得的balance不會爲負數
同時初始化十個線程,啓動十個,但因爲加鎖,可以啓動調用WithDraw方法的可能只能是其中幾個
*/
using System;

namespace ThreadTest29
{
    class Account
    {
        private Object thisLock = new object();//設置鎖對象
        int balance;
        Random r = new Random();

        public Account(int initial)
        {
            balance = initial;
        }

        int WithDraw(int amount)
        {
            if (balance < 0)
            {
                throw new Exception("負的Balance.");
            }
            //確保只有一個線程使用資源,一個進入臨界狀態,使用對象互斥鎖,10個啓動了的線程不能所有執行該方法
            lock (thisLock)
            {
                if (balance >= amount)
                {
                    Console.WriteLine("----------------------------:" + System.Threading.Thread.CurrentThread.Name + "---------------");

                    Console.WriteLine("調用Withdrawal以前的Balance:" + balance);
                    Console.WriteLine("把Amount輸入 Withdrawal     :-" + amount);
                    //若是沒有加對象互斥鎖,則可能10個線程都執行下面的減法,加減法所耗時間片斷很是小,可能多個線程同時執行,出現負數。
                    balance = balance - amount;
                    Console.WriteLine("調用Withdrawal以後的Balance :" + balance);
                    return amount;
                }
                else
                {
                    //最終結果
                    return 0;
                }
            }
        }
        public void DoTransactions()
        {
            for (int i = 0; i < 100; i++)
            {
                //生成balance的被減數amount的隨機數
                WithDraw(r.Next(1, 100));
            }
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            //初始化10個線程
            System.Threading.Thread[] threads = new System.Threading.Thread[10];
            //把balance初始化設定爲1000
            Account acc = new Account(1000);
            for (int i = 0; i < 10; i++)
            {
                System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(acc.DoTransactions));
                threads[i] = t;
                threads[i].Name = "Thread" + i.ToString();
            }
            for (int i = 0; i < 10; i++)
            {
                threads[i].Start();
            }
            Console.ReadKey();
        }
    }
}

   (2)Monitor類

    這個算是實現鎖機制的純正類,在鎖定的臨界區中只容許讓一個線程訪問,其餘線程排隊等待。主要整理爲2組方法。

    (2.1)Monitor.Enter和Monitor.Exit      

      微軟很照護咱們,給了咱們語法糖Lock,對的,語言糖確實減小了咱們沒必要要的勞動而且讓代碼更可觀,可是若是咱們要精細的     控制,則必須使用原生類,這裏要注意一個問題就是「鎖住什麼」的問題,通常狀況下咱們鎖住的都是靜態對象,咱們知道靜態對象屬於類級別,當有不少線程共同訪問的時候,那個靜態對象對多個線程來講是一個,不像實例字段會被認爲是多個。Monitor 鎖定對象是引用類型,而非值類型,該對象用來定義鎖的範圍,與lock同樣,畢竟lock是monitor的語法糖。

class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Thread t = new Thread(Run);

                t.Start();
            }
        }

        //資源
        static object obj = new object();

        static int count = 0;

        static void Run()
        {
            Thread.Sleep(10);

            //進入臨界區
            Monitor.Enter(obj);

            Console.WriteLine("當前數字:{0}", ++count);

            //退出臨界區
            Monitor.Exit(obj);
        }
    }

 

    (2.2)Monitor.Wait和Monitor.Pulse  

  首先這兩個方法是成對出現,一般使用在Enter,Exit之間。 Wait: 暫時的釋放資源鎖,而後該線程進入」等待隊列「中,那麼天然別的線程就能獲取到資源鎖。 Pulse:  喚醒「等待隊列」中的線程,那麼當時被Wait的線程就從新獲取到了鎖。 

這裏咱們是否注意到了兩點:①   可能A線程進入到臨界區後,須要B線程作一些初始化操做,而後A線程繼續幹剩下的事情。②   用上面的兩個方法,咱們能夠實現線程間的彼此通訊。

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            LockObj obj = new LockObj();

            //注意,這裏使用的是同一個資源對象obj
            Jack jack = new Jack(obj);
            John john = new John(obj);

            Thread t1 = new Thread(new ThreadStart(jack.Run));
            Thread t2 = new Thread(new ThreadStart(john.Run));

            t1.Start();
            t1.Name = "Jack";

            t2.Start();
            t2.Name = "John";

            Console.ReadLine();
        }
    }

    //鎖定對象
    public class LockObj { }

    public class Jack
    {
        private LockObj obj;

        public Jack(LockObj obj)
        {
            this.obj = obj;
        }

        public void Run()
        {
            Monitor.Enter(this.obj);

            Console.WriteLine("{0}:我已進入茅廁。", Thread.CurrentThread.Name);

            Console.WriteLine("{0}:擦,太臭了,我仍是撤!", Thread.CurrentThread.Name);

            //暫時的釋放鎖資源
            Monitor.Wait(this.obj);

            Console.WriteLine("{0}:兄弟說的對,我仍是進去吧。", Thread.CurrentThread.Name);

            //喚醒等待隊列中的線程
            Monitor.Pulse(this.obj);

            Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name);

            Monitor.Exit(this.obj);
        }
    }

    public class John
    {
        private LockObj obj;

        public John(LockObj obj)
        {
            this.obj = obj;
        }

        public void Run()
        {
            Monitor.Enter(this.obj);

            Console.WriteLine("{0}:直奔茅廁,兄弟,你仍是進來吧,當心憋壞了!",
                               Thread.CurrentThread.Name);

            //喚醒等待隊列中的線程
            Monitor.Pulse(this.obj);

            Console.WriteLine("{0}:嘩啦啦....", Thread.CurrentThread.Name);

            //暫時的釋放鎖資源
            Monitor.Wait(this.obj);

            Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name);

            Monitor.Exit(this.obj);
        }
    }
}

   (3)ReaderWriteLock類

   先前也知道,Monitor實現的是在讀寫兩種狀況的臨界區中只可讓一個線程訪問,那麼若是業務中存在」讀取密集型「操做,就比如數據庫同樣,讀取的操做永遠比寫入的操做多。針對這種狀況,咱們使用Monitor的話很吃虧,不過不要緊,ReadWriterLock就很牛X,由於實現了」寫入串行「,」讀取並行「。

ReaderWriteLock中主要用3組方法:

<1>  AcquireWriterLock: 獲取寫入鎖。

          ReleaseWriterLock:釋放寫入鎖。

<2>  AcquireReaderLock: 獲取讀鎖。

          ReleaseReaderLock:釋放讀鎖。

<3>  UpgradeToWriterLock:將讀鎖轉爲寫鎖。

         DowngradeFromWriterLock:將寫鎖還原爲讀鎖。

 下面就實現一個寫操做,三個讀操做,要知道這三個讀操做是併發的。

namespace Test
{
    class Program
    {
        static List<int> list = new List<int>();

        static ReaderWriterLock rw = new System.Threading.ReaderWriterLock();

        static void Main(string[] args)
        {
            Thread t1 = new Thread(AutoAddFunc);

            Thread t2 = new Thread(AutoReadFunc);

            t1.Start();

            t2.Start();

            Console.Read();
        }

        /// <summary>
/// 模擬3s插入一次
/// </summary>
/// <param name="num"></param>
        public static void AutoAddFunc()
        {
            //3000ms插入一次
            Timer timer1 = new Timer(new TimerCallback(Add), null, 0, 3000);
        }

        public static void AutoReadFunc()
        {
            //1000ms自動讀取一次
            Timer timer1 = new Timer(new TimerCallback(Read), null, 0, 1000);
            Timer timer2 = new Timer(new TimerCallback(Read), null, 0, 1000);
            Timer timer3 = new Timer(new TimerCallback(Read), null, 0, 1000);
        }

        public static void Add(object obj)
        {
            var num = new Random().Next(0, 1000);

            //寫鎖
            rw.AcquireWriterLock(TimeSpan.FromSeconds(30));

            list.Add(num);

            Console.WriteLine("我是線程{0},我插入的數據是{1}。", Thread.CurrentThread.ManagedThreadId, num);

            //釋放鎖
            rw.ReleaseWriterLock();
        }

        public static void Read(object obj)
        {
            //讀鎖
            rw.AcquireReaderLock(TimeSpan.FromSeconds(30));

            Console.WriteLine("我是線程{0},我讀取的集合爲:{1}",
                              Thread.CurrentThread.ManagedThreadId, string.Join(",", list));
            //釋放鎖
            rw.ReleaseReaderLock();
        }
    }
}

  (4)標誌量: 顧名思義,標誌量就是聲明一個布爾型變量,用來標示某些方法的執行狀態。

 

 

 

 

下一篇博文中來介紹互斥量實現線程同步。。。。

相關文章
相關標籤/搜索