進階系列(11)—— C#多線程

1、多線程的相關概念

1.進程:是操做系統結構的基礎;是一個正在執行的程序;計算機中正在運行的程序實例;能夠分配給處理器並由處理器執行的一個實體;由單一順序的執行顯示,一個當前狀態和一組相關的系統資源所描述的活動單元。程序員

2.線程:線程是程序中一個單一的順序控制流程。是程序執行流的最小單元。另外,線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程本身不擁有系統資源,只擁有一點在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的所有資源。一個線程能夠建立和撤消另外一個線程,同一進程中的多個線程之間能夠併發執行。因爲線程之間的相互制約,導致線程在運行中呈現出間斷性。線程也有就緒、阻塞和運行三種基本狀態。每個程序都至少有一個線程,若程序只有一個線程,那就是程序自己。數據庫

3.多線程:在單個程序中同時運行多個線程完成不一樣的工做,稱爲多線程。緩存

理解:其實更容易理解一點進程與線程的話,能夠舉這樣一個例子:把進程理解成爲一個運營着的公司,然而每個公司員工就能夠叫作一個線程。每一個公司至少要有一個員工,員工越多,若是你的管理合理的話,公司的運營速度就會越好。這裏官味一點話就是說。cpu大部分時間處於空閒時間,浪費了cpu資源,多線程可讓一個程序「同時」處理多個事情,提升效率。安全

(一)單線程問題演示多線程

建立一個WinForm應用程序,這裏出現的問題是,點擊按鈕後若是在彈出提示框以前,窗體是不能被拖動的。併發

 private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < 10000000000; i++)  
            {
                i += 1;
            }
            MessageBox.Show("出現後能拖動,提示沒出現以前窗體不能被拖動");
        }

緣由:運行這個應用程序的時候,窗體應用程序自帶一個叫作UI的線程,這個線程負責窗體界面的移動大小等。若是點擊按鈕則這個線程就去處理這個循環計算,而放棄了其它操做,故而窗體拖動無響應。這就是單線程帶來的問題。ide

解決辦法:使用多線程,咱們本身建立線程。把計算代碼放入咱們本身寫的線程中,UI線程就能繼續作他的界面響應了。函數

(二)線程的建立性能

 線程的實現:線程必定是要執行一段代碼的,因此要產生一個線程,必須先爲該線程寫一個方法,這個方法中的代碼,就是該線程中要執行的代碼,然而啓動線程時,是經過委託調用該方法的。線程啓動是,調用傳過來的委託,委託就會執行相應的方法,從而實現線程執行方法。ui

 //建立線程  
        private void button1_Click(object sender, EventArgs e)
        {
            //ThreadStart是一個無參無返回值的委託。
            ThreadStart ts = new ThreadStart(js);
            //初始化Thread的新實例,並經過構造方法將委託ts作爲參數賦初始值。
            Thread td = new Thread(ts);   //須要引入System.Threading命名空間
            //運行委託
            td.Start();
        }
        //建立的線程要執行的函數。
        void js()
        {
            for (int i = 0; i < 1000000000; i++)
            {
                i += 1;
            }
            MessageBox.Show("提示出現先後窗體都能被拖動");
        }

把這個計算寫入本身寫的線程中,就解決了單線程中的界面無反應缺陷。

小結:建立線程的4個步驟:

1.編寫線程索要執行的方法。

2.引用System.Threading命名空。

3.實例化Thread類,並傳入一個指向線程所要運行方法的委託。

4.調用Start()方法,將該線程標記爲能夠運行的狀態,但具體執行時間由cpu決定。

 (三)方法重入(多個線程執行一個方法)

 因爲線程可與同屬一個進程的其它線程共享進程所擁有的所有資源。

因此多個線程同時執行一個方法的狀況是存在的,然而這裏不通過處理的話會出現一點問題,線程之間前後爭搶資源,導致數據計算結果錯亂。

public partial class 方法重入 : Form
    {
        public 方法重入()
        {
            InitializeComponent();

            //設置TextBox類的這個屬性是由於,開啓ui線程,
            //微軟設置檢測不容許其它線程對ui線程的數據進行訪問,這裏咱們把檢測關閉,也就容許了其它線程對ui線程數據的訪問。
            //若是檢測不設置爲False,則報錯。
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            textBox1.Text = "0";
            //開啓第一個線程,對js方法進行計算
            ThreadStart ts = new ThreadStart(js);
            Thread td = new Thread(ts);
            td.Start();

            //開啓第二個線程,對js方法進行計算
            ThreadStart ts1 = new ThreadStart(js);
            Thread td1 = new Thread(ts1);
            td1.Start();
        }
        //多線程要重入的方法。
        void js()
        {
            int a = Convert.ToInt32(textBox1.Text);
            for (int i = 0; i < 2000; i++)
            {
                a++;
                textBox1.Text = a.ToString();
            }
        }
    }

 

出錯現象:點擊按鈕後TextBox1中數據爲2000+或2000,若是你看到的數據一直是2000說明你的計算機cpu比較牛X,這樣的話你想看到不是2000的話,你能夠多點擊幾回試試,真不行的話,代碼中給TextBox1賦值爲0,換作在界面中給textBox1數值默認值爲0試試看。

出錯緣由:兩個進程同時計算這個方法,不相干擾應該每一個線程計算的結果都是2000的,可是這裏的結果輸出卻讓人之外,緣由是第一個兩個線程同時計算,並非同時開始計算,而是根據cpu決定的哪一個先開始,哪一個後開始,雖然相差時間很少,但後開始的就會取用先開始計算過的數據計算,這樣就會致使計算錯亂。

解決辦法:解決這個的一個簡單辦法解釋給方法加鎖,加鎖的意思就是第一個線程取用過這個資源完畢後,第二個線程再來取用此資源。造成排隊效果。

下面給方法加鎖。

//多線程要重入的方法,這裏加鎖。
        void js()
        {
            lock (this)
            {
                int a = Convert.ToInt32(textBox1.Text);
                for (int i = 0; i < 2000; i++)
                {
                    a++;
                    textBox1.Text = a.ToString();
                }
            }
        }

給方法加過鎖後,線程一前一後取用資源,就能避免不可預計的錯亂結果,第一個線程計算爲2000,第二個線程計算就是從2000開始,這裏的結果就爲4000。

小結:多線程能夠同時運行,提升了cpu的效率,這裏的同時並非同時開始,同時結束,他們的開始是由cpu決定的,時間相差不大,但會有不可預計的計算錯亂,這裏要注意相似上面例子致使的方法重入問題。

(四)前臺線程後臺線程

 .Net的公用語言運行時能區分兩種不一樣類型的線程:前臺線程和後臺線程。這二者的區別就是:應用程序必須運行完全部的前臺線程才能夠退出;而對於後臺線程,應用程序則能夠不考慮其是否已經運行完畢而直接退出,全部的後臺線程在應用程序退出時都會自動結束。

問題:關閉了窗口,消息框還能彈出。

 private void button1_Click(object sender, EventArgs e)
        { 
            //開啓一個線程,對js方法進行計算
            ThreadStart ts2 = new ThreadStart(js);
            Thread td2 = new Thread(ts2);             
            td2.Start();

        }        
        void js()
        {
            for (int i = 0; i < 2000000000; i++)  //若是看不出效果這裏的2後面多加0
            {
                i++;
            }
            MessageBox.Show("關閉了窗口我仍是要出來的!");
        }

緣由:.Net環境使用Thread創建線程,線程默認爲前臺線程。即線程屬性IsBackground=false,而前臺線程只要有一個在運行則應用程序不關閉,因此知道彈出消息框後應用程序纔算關閉。

解決辦法:在代碼中設置td2.IsBackground=true;

 (五)線程執行帶參數的方法

 //建立一個執行帶參數方法的線程
        private void button1_Click(object sender, EventArgs e)
        {
            //ParameterizedThreadStart這是一個參數類型爲object的委託
            ParameterizedThreadStart pts=new ParameterizedThreadStart(SayHello);
            Thread td2 = new Thread(pts);
            td2.Start("張三");  //參數值先入這裏
        }
        void SayHello(object name)
        {
            MessageBox.Show("你好,"+name.ToString()+"!");
        } 

(六)線程執行帶多參數的方法

 其實仍是帶一參數的方法,只不過是利用參數類型爲object的好處,這裏將類型傳爲list類型,貌似多參。

 //建立一個執行帶多個參數的方法線程
        private void button1_Click(object sender, EventArgs e)
        {
            List<string> list = new List<string> { "張三", "李四", "王五" };
            //ParameterizedThreadStart這是一個參數類型爲object的委託
            ParameterizedThreadStart pts=new ParameterizedThreadStart(SayHello);
            Thread td2 = new Thread(pts);
            td2.Start(list);  //參數值先入這裏
        }
        void SayHello(object list)
        {
            List<string> lt = list as List<string>;
            for (int i = 0; i < lt.Count; i++)
            {
                MessageBox.Show("你好," + lt[i].ToString() + "!");
            }
        } 

 2、線程的重要屬性

(一)肯定多線程的結束時間,thread的IsAlive屬性

在多個線程運行的背景下,瞭解線程何時結束,何時中止是頗有必要的。

案例:老和尚唸經計時,2本經書,2個和尚念,一人一本,不能撕破,最短期唸完,問老和尚們唸完經書最短鬚要多長時間。

分析:首先在開始唸經的時候給計時,記爲A,最後在記下慢和尚唸完經書時的時間,記爲B。求B-A

代碼:IsAlive屬性:標識此線程已啓動而且還沒有正常終止或停止,則爲 true,再念,沒念完,努力中;不然爲 false,唸完啦,歇着。

 //和尚1,和尚2
        public Thread td1, td2;

        public void StarThread()
        {
            //開啓一個線程執行Hello方法,即和尚1念菠蘿菠蘿蜜
            ThreadStart ts = new ThreadStart(Hello);
            td1 = new Thread(ts);
            td1.Start();
        }
        public void StarThread1()
        {
            //開啓一個線程執行Welcome方法,即和尚2念大金剛經
            ThreadStart ts = new ThreadStart(Welcome);
            td2 = new Thread(ts);
            td2.Start();
        }
        public string sayh="", sayw="";

        //菠蘿菠蘿蜜
        public void Hello()
        {
            //
            sayh = "Hellow everyone ! ";
        }

        //大金剛經
        public  void Welcome()
        {   
            //
            sayw = "Welcome to ShangHai ! ";
            //偷懶10秒
            Thread.Sleep(10000);
        }

        protected void btn_StarThread_Click(object sender, EventArgs e)
        {
            //記時開始,預備念        
            Response.Write("開始唸的時間: "+DateTime.Now.ToString() + "</br>");
            //和尚1就位
            StarThread();
            //和尚2就位
            StarThread1();

            int i = 0;
            while (i == 0)
            {
                //判斷線程的IsAlive屬性
                //IsAlive標識此線程已啓動而且還沒有正常終止或停止,則爲 true;不然爲 false。
                //若是兩個都爲false說明,線程結束終止
                if (!td1.IsAlive && !td2.IsAlive)
                {
                    i++;
                    if (i == 1)
                    {
                        //念得內容,繞樑三尺。
                        Response.Write("咱們年的內容: "+(sayh + " + " + sayw) + "</br>");
                        Response.Write("唸完時的時間: "+DateTime.Now.ToString());
                        Response.End();
                    }
                }
            }
        }

(二)、線程優先級,thread的ThreadPriority屬性

線程優先級區別於線程佔有cpu時間的多少,固然優先級越高同等條件下佔有的cpu時間越多。級別高的執行效率要高於級別低的。

優先級有5個級別:Lowest<BelowNormal<Normal<AboveNormal<Highest;默認爲Normal。

案例:老和尚娶媳婦。佛祖說:大家3個和尚,清修刻苦,現特許大家娶媳婦啦,不過娶媳婦的只能是大家三個中間的一人。條件是我手中的經書誰能先念完,誰能夠娶。

分析:和尚平時都很刻苦,各有特色,平時和尚1在lowest環境下唸經,和尚2在normal環境下唸經,和尚3在Highest環境下唸經。

 protected void btn_StarThread_Click(object sender, EventArgs e)
        {
            Write();
        }

        //i爲和尚1唸的頁數
        //j爲和尚2唸的頁數
        //k爲和尚3唸的頁數
        //c爲經書總頁數
        int i=0,j=0,k=0,c=10000000;

        //和尚1唸經
        public void Jsi()
        {
            while (i <= c)
            {
                i+=1;
            }
        }
        //和尚2唸經
        public void Jsj()
        {
            while (j <= c)
            {
                j+=1;
            }
        }
        //和尚3唸經
        public void Jsk()
        {
            while (k <= c)
            {
                k+=1;
            }
        }
        public void Write()
        {
            //開啓線程計算i
            ThreadStart sti = new ThreadStart(Jsi);
            Thread tdi = new Thread(sti);
            //設置線程優先級爲Lowest。和尚1在Lowest環境下唸經
            tdi.Priority = ThreadPriority.Lowest;

            //開啓線程計算j
            ThreadStart stj = new ThreadStart(Jsj);
            Thread tdj = new Thread(stj);
            //設置線程優先級爲Normal。和尚2在Normal環境下唸經
            tdj.Priority = ThreadPriority.Normal;            

            //開啓線程計算k
            ThreadStart stk = new ThreadStart(Jsk);
            Thread tdk = new Thread(stk);
            //設置線程優先級爲Highest。和尚3在Highest環境下唸經
            tdk.Priority = ThreadPriority.Highest;         
            
            //開始
            tdj.Start();
            tdk.Start();
            tdi.Start();
            int s = 0;
            while (s==0)
            {                
                if (k > c)
                {
                    s++;
                    Response.Write("比賽結束,結果以下:</br></br>");
                    Response.Write("和尚1在Lowest環境下唸經:" + i + "頁</br>和尚2在Normal環境下唸經:" + j + "頁</br>和尚3在Highest環境下唸經:" + k + "頁</br></br>");
                    Response.Write("佛祖又說:你念或者不念,蒼老師,就在那裏!");
                    Response.End();
                }
            }
        }

複製代碼

爲啦方便期間,從這之後,我要用控制檯程序演示,操控線程。

(三)線程通訊之Monitor類

若是,你的線程A中運行鎖內方法時候,須要去訪問一個暫不可用資源B,可能在B上耗費很長的等待時間,那麼這時候你的線程A,將佔用鎖內資源,阻塞其它線程訪問鎖定內容,形成性能損失。你該怎麼解決這樣子的問題呢?這樣,讓A暫時放棄鎖,停留在鎖中的,容許其它線程訪問鎖,而等B資源可用時,通知A讓他繼續鎖內的操做。是否是解決啦問題,這樣就用到啦這段中的Monitor類,提供的幾個方法:Wait(),Pulse(),PulseAll(),這幾個方法只能在當前鎖定中使用。

Wait():暫時中斷運行鎖定中線程操做,釋放鎖,時刻等待着通知復活。

Pulse():通知等待該鎖線程隊列中的第一個線程,此鎖可用。

PulseAll():通知全部鎖,此鎖可用。

案例:嵩山少林和尚開會。主持人和尚主持會議會不停的上舞臺講話,方丈會出來宣佈大會開始,弟子們開始討論峨眉山怎麼走。

分析:主持人一個線程,方丈一個線程,弟子們一個線程,主持人貫徹全場。

 public class MutexSample
    {
        static void Main()
        {
            comm com = new comm();
            com.dhThreads();
            Console.ReadKey();
        }
    }
    public class comm
    {
        //狀態值:0時主持人和尚說,1時方丈說,2時弟子們說,3結束。
        int sayFla;
        //主持人上臺
        int i = 0;
        public void zcrSay()
        {
            lock (this)
            {
                string sayStr;
                if (i == 0)
                {
                    //讓方丈說話
                    sayFla = 1;
                    sayStr = Thread.CurrentThread.Name+"今晚,陽光明媚,多雲轉晴,方丈大師,程祥雲而來,傳揚峨眉一隅,狀況如何,還請方丈閃亮登場。";
                    Console.WriteLine(sayStr);
                    i++;
                    //此時sayFla=1通知等待的方丈線程運行
                    Monitor.Pulse(this);  
                    //暫時鎖定主持人,暫停到這裏,釋放this讓其它線程訪問
                    Monitor.Wait(this);
                    
                }
                //被通知後,從上一個鎖定開始運行到這裏
                if (i == 1)
                {
                    //讓弟子說話
                    sayFla = 2;
                    sayStr = Thread.CurrentThread.Name + "看方丈那幸福的表情,徜徉肆恣,願走的跟他去吧。下面請弟子們各抒己見";
                    Console.WriteLine(sayStr);
                    i++;
                    //此時sayFla=12通知等待的弟子線程運行
                    Monitor.Pulse(this);  
                     //暫時鎖定主持人,暫停到這裏,釋放this讓其它線程訪問
                    Monitor.Wait(this);                    
                }
                //被通知後,從上一個鎖定開始運行到這裏
                if (i == 2)
                {
                    sayFla = 3;
                    sayStr = Thread.CurrentThread.Name + "大會結束!方丈幸福!!蒼老師你在哪裏?!!放開那女孩 ...";
                    Console.WriteLine(sayStr);
                    i++;
                    Monitor.Wait(this); 
                }
            }
        }
        //方丈上臺
        public void fzSay()
        {
            lock (this)
            {
                while (true)
                {
                    if (sayFla != 1)
                    {
                        Monitor.Wait(this);
                    }
                    if (sayFla == 1)
                    {
                        Console.WriteLine(Thread.CurrentThread.Name + "藍藍的天空,綠綠的湖水,我看見,咿呀呀呀,看見一老尼,咿呀呀,在水一方。願意來的一塊兒來,不肯來的蒼老師給大家放寺裏。。咿呀呀,我走啦。。。");
                        //交給主持人
                        sayFla = 0;
                        //通知主持人線程,this可用
                        Monitor.Pulse(this);
                    }
                }                
            }
        }
        //弟子上臺
        public void dzSay()
        {
            lock (this)
            {
                while (true)
                {
                    if (sayFla != 2)
                    {
                        Monitor.Wait(this);  
                    }
                    if (sayFla == 2)
                    {
                        Console.WriteLine(Thread.CurrentThread.Name + "果然如此的話,仍是方丈大師本身去吧!! 祝福啊  .... ");
                        //交給主持人
                        sayFla = 0;
                        Monitor.Pulse(this);
                    }
                }
                
            }
        } 
        public void dhThreads()
        {            
                Thread zcrTd = new Thread(new ThreadStart(zcrSay));
                Thread fzTd = new Thread(new ThreadStart(fzSay));
                Thread dzTd = new Thread(new ThreadStart(dzSay));
                zcrTd.Name = "主持人:";
                fzTd.Name = "方丈:";
                dzTd.Name = "弟子:";                
                zcrTd.Start();
                fzTd.Start();
                dzTd.Start();
        }
    }

(四)線程排隊之Join

多線程,共享一個資源,前後操做資源。Join()方法,暫停當前線程,直到指定線程運行完畢,才喚醒當前線程。若是沒有Join,多線程隨機讀取公用資源,沒有前後次序。

案例:兩個和尚念一本經書,老和尚年前半本書,小和尚念後半本書,小和尚調皮,非要先念,就給老和尚用迷魂藥啦。。

分析:一本書6頁,小和尚4-6,老和尚1-3,兩個和尚,兩個線程。

 public class 鏈接線程Join
    {
        //小和尚
        public static Thread litThread;
        //老和尚
        public static Thread oldThread;

        //老和尚唸經
        static void oldRead()
        {
            //老和尚被小和尚下藥
            litThread.Join();  //暫停oldThread線程,開始litThread,直到litThread線程結束,oldThread才繼續運行,若是不適用Join將小和尚一句,老和尚一句,隨即沒有規則的。
            for (int i = 1; i <= 3; i++)
            {
                Console.WriteLine(i);
            }
        }
        //小和尚唸經
        static void litRead()
        { 
            for (int i = 4; i <= 6; i++)
            {
                Console.WriteLine(i);
            }
        }
        static void Main(string[] args)
        {
            oldThread = new Thread(new ThreadStart(oldRead));
            litThread = new Thread(new ThreadStart(litRead));

            oldThread.Start();
            // FristThread.Join();   //暫停oldThread線程,開始litThread,直到litThread線程結束,oldThread才繼續運行
            litThread.Start();
            Console.ReadKey();
        }
    }

(五)多線程互斥鎖Mutex

 互斥鎖是一個同步的互斥對象,適用於,一個共享資源,同時只能有一個線程可以使用。

共享資源加互斥鎖,須要兩部走:1.WaitOne(),他將處於等待狀態知道能夠獲取資源上的互斥鎖,獲取到後,阻塞主線程運行,直到釋放互斥鎖結束。2.ReleaseMutex(),釋放互斥鎖,是其它線程能夠獲取該互斥鎖。

案例:和尚寫日記。最近寺廟香火不旺,爲啦節約用水,方丈發話兩個和尚用一個本子寫日記。

分析:比如多個線程寫日誌,同時只能有一個線程寫入日誌文件。

 public class 多線程互斥鎖Mutex
    {
        static void Main(string[] args)
        {
            IncThread ict = new IncThread("大和尚", 3);
            DecThread dct = new DecThread("小和尚", 3);
            Console.ReadKey();
        }
    }
    class SharedRes
    {
        public static int count = 0;
        //初始化互斥鎖,沒被獲取
        public static Mutex mtx = new Mutex();
        ////初始化互斥鎖,被主調線程獲取
        //public static Mutex mtx = new Mutex(true);
    }

    class IncThread
    {
        int num;
        public Thread thrd;
        public IncThread(string name ,int n)
        {
            thrd = new Thread(new ThreadStart(this.run));
            thrd.Name = name;
            num = n;
            thrd.Start();
        }
        //寫日記,過程
        void run()
        {
            Console.WriteLine(thrd.Name + " , 等待互斥鎖 。");
            SharedRes.mtx.WaitOne();           
            Console.WriteLine(thrd.Name + " ,得到互斥鎖 。");
            do
            {
                Thread.Sleep(500);
                SharedRes.count++;
                Console.WriteLine("今天我 " + thrd.Name + " 比較強,這樣寫吧 :" + SharedRes.count);
                num--;
            }while(num>0);
           Console.WriteLine(thrd.Name + " , 釋放互斥鎖 。");
           SharedRes.mtx.ReleaseMutex();
        }
    }
    class DecThread
    {
        int num;
        public Thread thrd;

        public DecThread(string name, int n)
        {
            thrd = new Thread(new ThreadStart(this.run));
            thrd.Name = name;
            num = n;
            thrd.Start();
        }
        //寫日記,過程
        void run()
        {           
            Console.WriteLine(thrd.Name + ", 等待互斥鎖 。");
            SharedRes.mtx.WaitOne();
            Console.WriteLine(thrd.Name + " ,得到互斥鎖 。");
            do
            {
                Thread.Sleep(500);
                SharedRes.count--;
                Console.WriteLine("今天我 " + thrd.Name + " 比較衰,這樣寫吧 :" + SharedRes.count);
                num--;
            } while (num > 0);
            Console.WriteLine(thrd.Name + " , 釋放互斥鎖 。");
            SharedRes.mtx.ReleaseMutex();
        }
    }

(六)信號量semaphore

相似於互斥鎖,只不過他能夠指定多個線程來訪問,共享資源。在初始化信號量的同時,指定多少個線程能夠訪問,假如容許2個線程訪問,而卻有3個線程等待訪問,那麼他將只容許2個訪問,一旦已訪問的2個線程中有一個訪問完成釋放信號量,那麼沒有訪問的線程立馬能夠進入訪問。

案例:和尚抓雞,3個和尚抓雞,只有兩隻雞,那麼雞圈管理員只容許2個和尚先進,抓到說三句話放下,出來,讓第三個和尚進去抓。

分析:三個線程,初始信號量容許2個線程訪問。

public class 信號量semaphore
    {
       static void Main(string[] args)
       {
           MyThread td1 = new MyThread("降龍");
           MyThread td2 = new MyThread("伏虎");
           MyThread td3 = new MyThread("如來");           
           td1.td.Start();
           td2.td.Start();
           td3.td.Start();
           Console.ReadKey();
       }    
    }

  public class MyThread
   {
       //初始化,新號量,容許2個線程訪問,最大2也是2個。
       public static Semaphore sem = new Semaphore(2,2);
       public Thread td;
       public MyThread(string name)
       {
           td = new Thread(new ThreadStart(this.Run));
           td.Name = name;
       }
       //過程很美好
       public void Run()
       {
           Console.WriteLine(td.Name+",等待一個信號量。");
           sem.WaitOne();
           Console.WriteLine(td.Name+",已經得到新號量。");
           Thread.Sleep(500);           //頗有深意的三句話
           char[] cr = { 'a','o','e'};
           foreach (char v in cr)
           {
               Console.WriteLine(td.Name+",輸出v: "+v);
               Thread.Sleep(300);
           }
           Console.WriteLine(td.Name+",釋放新號量。");
           sem.Release();
       }
   } 

 

三 線程同步

(一)什麼是線程安全:

     線程安全是指在當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。

   線程有可能和其餘線程共享一些資源,好比,內存,文件,數據庫等。當多個線程同時讀寫同一份共享資源的時候,可能會引發衝突。這時候,咱們須要引入線程「同步」機制,即各位線程之間要有個先來後到,不能一窩蜂擠上去搶做一團。線程同步的真實意思和字面意思剛好相反。線程同步的真實意思,實際上是「排隊」:幾個線程之間要排隊,一個一個對共享資源進行操做,而不是同時進行操做。

爲何要實現同步呢,下面的例子咱們拿著名的單例模式來講吧。看代碼

public class Singleton { private static Singleton instance; private Singleton() //私有函數,防止實例  { } public static Singleton GetInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

       單例模式就是保證在整個應用程序的生命週期中,在任什麼時候刻,被指定的類只有一個實例,併爲客戶程序提供一個獲取該實例的全局訪問點。但上面代碼有一個明顯的問題,那就是假如兩個線程同時去獲取這個對象實例,那。。。。。。。。

咱們對代碼進行修改:

public class Singleton { private static Singleton instance; private static object obj=new object(); private Singleton()        //私有化構造函數 { } public static Singleton GetInstance() { if(instance==null) { lock(obj) //經過Lock關鍵字實現同步 { if(instance==null) { instance=new Singleton(); } } } return instance; } }

通過修改後的代碼。加了一個 lock(obj)代碼塊。這樣就可以實現同步了,假如不是很明白的話,我們看後面繼續講解~

(二)使用Lock關鍵字實現線程同步 

  首先建立兩個線程,兩個線程執行同一個方法,參考下面的代碼:

static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //執行的必須是無返回值的方法 threadA.Name = "王文建"; Thread threadB = new Thread(ThreadMethod); //執行的必須是無返回值的方法 threadB.Name = "生旭鵬"; threadA.Start(); threadB.Start(); Console.ReadKey(); } public static void ThreadMethod(object parameter) { for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循環{1}次", Thread.CurrentThread.Name, i); Thread.Sleep(300); } }

執行結果:


經過上面的執行結果,能夠很清楚的看到,兩個線程是在同時執行ThreadMethod這個方法,這顯然不符合咱們線程同步的要求。咱們對代碼進行修改以下:

static void Main(string[] args)
        {
            Program pro = new Program();
            Thread threadA = new Thread(pro.ThreadMethod); //執行的必須是無返回值的方法 
            threadA.Name = "王文建";
            Thread threadB = new Thread(pro.ThreadMethod); //執行的必須是無返回值的方法 
            threadB.Name = "生旭鵬";
            threadA.Start();
            threadB.Start();
            Console.ReadKey();
        }
        public void ThreadMethod(object parameter)
        {
            lock (this)             //添加lock關鍵字
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循環{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);
                }
            } 
        }

執行結果:

咱們經過添加了 lock(this) {...}代碼,查看執行結果實現了咱們想要的線程同步需求。可是咱們知道this表示當前類實例的自己,那麼有這麼一種狀況,咱們把須要訪問的方法所在的類型進行兩個實例A和B,線程A訪問實例A的方法ThreadMethod,線程B訪問實例B的方法ThreadMethod,這樣的話還可以達到線程同步的需求嗎。

static void Main(string[] args)
        {
            Program pro1 = new Program();                    
            Program pro2 = new Program();                   
            Thread threadA = new Thread(pro1.ThreadMethod); //執行的必須是無返回值的方法 
            threadA.Name = "王文建";
            Thread threadB = new Thread(pro2.ThreadMethod); //執行的必須是無返回值的方法 
            threadB.Name = "生旭鵬";
            threadA.Start();
            threadB.Start();
            Console.ReadKey();
        }
        public void ThreadMethod(object parameter)
        {
            lock (this)
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循環{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);
                }
            }
        }

執行結果:

咱們會發現,線程又沒有實現同步了!lock(this)對於這種狀況是不行的!因此須要咱們對代碼進行修改!修改後的代碼以下:

private static object obj = new object();
        static void Main(string[] args)
        {
            Program pro1 = new Program();                    
            Program pro2 = new Program();                   
            Thread threadA = new Thread(pro1.ThreadMethod); //執行的必須是無返回值的方法 
            threadA.Name = "王文建";
            Thread threadB = new Thread(pro2.ThreadMethod); //執行的必須是無返回值的方法 
            threadB.Name = "生旭鵬";
            threadA.Start();
            threadB.Start();
            Console.ReadKey();
        }
        public void ThreadMethod(object parameter)
        {
            lock (obj)
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循環{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);
                }
            }
        }

經過查看執行結果。會發現代碼實現了咱們的需求。那麼 lock(this) 和lock(Obj)有什麼區別呢? 

lock(this) 鎖定 當前實例對象,若是有多個類實例的話,lock鎖定的只是當前類實例,對其它類實例無影響。全部不推薦使用。 
lock(typeof(Model))鎖定的是model類的全部實例。 
lock(obj)鎖定的對象是全局的私有化靜態變量。外部沒法對該變量進行訪問。 
lock 確保當一個線程位於代碼的臨界區時,另外一個線程不進入臨界區。若是其餘線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。 
因此,lock的結果好很差,仍是關鍵看鎖的誰,若是外邊能對這個誰進行修改,lock就失去了做用。因此通常狀況下,使用私有的、靜態的而且是隻讀的對象。

總結:

一、lock的是必須是引用類型的對象,string類型除外。

二、lock推薦的作法是使用靜態的、只讀的、私有的對象。

三、保證lock的對象在外部沒法修改纔有意義,若是lock的對象在外部改變了,對其餘線程就會暢通無阻,失去了lock的意義。

     不能鎖定字符串,鎖定字符串尤爲危險,由於字符串被公共語言運行庫 (CLR)「暫留」。 這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了全部運行的應用程序域的全部線程中的該文本。所以,只要在應用程序進程中的任何位置處具備相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的全部實例。一般,最好避免鎖定 public 類型或鎖定不受應用程序控制的對象實例。例如,若是該實例能夠被公開訪問,則 lock(this) 可能會有問題,由於不受控制的代碼也可能會鎖定該對象。這可能致使死鎖,即兩個或更多個線程等待釋放同一對象。出於一樣的緣由,鎖定公共數據類型(相比於對象)也可能致使問題。並且lock(this)只對當前對象有效,若是多個對象之間就達不到同步的效果。lock(typeof(Class))與鎖定字符串同樣,範圍太廣了。

(三)使用Monitor類實現線程同步      

      Lock關鍵字是Monitor的一種替換用法,lock在IL代碼中會被翻譯成Monitor. 

   lock(obj)
          {
            //代碼段
          } 
            //就等同於 
    Monitor.Enter(obj); 
                //代碼段
    Monitor.Exit(obj);  

           Monitor的經常使用屬性和方法:

    Enter(Object) 在指定對象上獲取排他鎖。

    Exit(Object) 釋放指定對象上的排他鎖。

    Pulse 通知等待隊列中的線程鎖定對象狀態的更改。

    PulseAll 通知全部的等待線程對象狀態的更改。

    TryEnter(Object) 試圖獲取指定對象的排他鎖。

    TryEnter(Object, Boolean) 嘗試獲取指定對象上的排他鎖,並自動設置一個值,指示是否獲得了該鎖。

    Wait(Object) 釋放對象上的鎖並阻止當前線程,直到它從新獲取該鎖。

               經常使用的方法有兩個,Monitor.Enter(object)方法是獲取鎖,Monitor.Exit(object)方法是釋放鎖,這就是Monitor最經常使用的兩個方法,在使用過程當中爲了不獲取鎖以後由於異常,致鎖沒法釋放,因此須要在try{} catch(){}以後的finally{}結構體中釋放鎖(Monitor.Exit())。

Enter(Object)的用法很簡單,看代碼

     static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //執行的必須是無返回值的方法 threadA.Name = "A"; Thread threadB = new Thread(ThreadMethod); //執行的必須是無返回值的方法 threadB.Name = "B"; threadA.Start(); threadB.Start(); Thread.CurrentThread.Name = "C"; ThreadMethod(); Console.ReadKey(); } static object obj = new object(); public static void ThreadMethod() { Monitor.Enter(obj); //Monitor.Enter(obj) 鎖定對象 try { for (int i = 0; i < 500; i++) { Console.Write(Thread.CurrentThread.Name); } } catch(Exception ex){ } finally { Monitor.Exit(obj); //釋放對象 } } 
複製代碼

     TryEnter(Object)TryEnter() 方法在嘗試獲取一個對象上的顯式鎖方面和 Enter() 方法相似。然而,它不像Enter()方法那樣會阻塞執行。若是線程成功進入關鍵區域那麼TryEnter()方法會返回true. 和試圖獲取指定對象的排他鎖。看下面代碼演示:

      咱們能夠經過Monitor.TryEnter(monster, 1000),該方法也可以避免死鎖的發生,咱們下面的例子用到的是該方法的重載,Monitor.TryEnter(Object,Int32),。

static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //執行的必須是無返回值的方法 threadA.Name = "A"; Thread threadB = new Thread(ThreadMethod); //執行的必須是無返回值的方法 threadB.Name = "B"; threadA.Start(); threadB.Start(); Thread.CurrentThread.Name = "C"; ThreadMethod(); Console.ReadKey(); } static object obj = new object(); public static void ThreadMethod() { bool flag = Monitor.TryEnter(obj, 1000); //設置1S的超時時間,若是在1S以內沒有得到同步鎖,則返回false
       //上面的代碼設置了鎖定超時時間爲1秒,也就是說,在1秒中後,
       //lockObj還未被解鎖,TryEntry方法就會返回false,若是在1秒以內,lockObj被解鎖,TryEntry返回true。咱們可使用這種方法來避免死鎖 try { if (flag) { for (int i = 0; i < 500; i++) { Console.Write(Thread.CurrentThread.Name); } } } catch(Exception ex) { } finally { if (flag) Monitor.Exit(obj); } }

 Monitor.Wait和Monitor()Pause()

Pulse 向等待對象發出信號,當前擁有指定對象上的鎖的線程調用此方法以便向隊列中的下一個線程發出鎖的信號。接收到脈衝後,等待線程就被移動到就緒隊列中。在調用 Pulse 的線程釋放鎖後,就緒隊列中的下一個線程(不必定是接收到脈衝的線程)將得到該鎖。
另外

        Wait 和 Pulse 方法必須寫在 Monitor.Enter 和Moniter.Exit 之間

上面是MSDN的解釋。不明白看代碼:

 首先咱們定義一個攻擊類,

/// <summary> /// 怪物類 /// </summary> internal class Monster { public int Blood { get; set; } public Monster(int blood) { this.Blood = blood; Console.WriteLine("我是怪物,我有{0}滴血",blood); } }

而後在定義一個攻擊類

/// <summary> /// 攻擊類 /// </summary> internal class Play { /// <summary> /// 攻擊者名字 /// </summary> public string Name { get; set; } /// <summary> /// 攻擊力 /// </summary> public int Power{ get; set; } /// <summary> /// 法術攻擊 /// </summary> public void magicExecute(object monster) { Monster m = monster as Monster; Monitor.Enter(monster); while (m.Blood>0) { Monitor.Wait(monster); Console.WriteLine("當前英雄:{0},正在使用法術攻擊打擊怪物", this.Name); if(m.Blood>= Power) { m.Blood -= Power; } else { m.Blood = 0; } Thread.Sleep(300); Console.WriteLine("怪物的血量還剩下{0}", m.Blood); Monitor.PulseAll(monster); } Monitor.Exit(monster); } /// <summary> /// 物理攻擊 /// </summary> /// <param name="monster"></param> public void physicsExecute(object monster) { Monster m = monster as Monster; Monitor.Enter(monster); while (m.Blood > 0) { Monitor.PulseAll(monster); if (Monitor.Wait(monster, 1000)) //很是關鍵的一句代碼 { Console.WriteLine("當前英雄:{0},正在使用物理攻擊打擊怪物", this.Name); if (m.Blood >= Power) { m.Blood -= Power; } else { m.Blood = 0; } Thread.Sleep(300); Console.WriteLine("怪物的血量還剩下{0}", m.Blood); } } Monitor.Exit(monster); } }

執行代碼:

    static void Main(string[] args) { //怪物類 Monster monster = new Monster(1000); //物理攻擊類 Play play1 = new Play() { Name = "無敵劍聖", Power = 100 }; //魔法攻擊類 Play play2 = new Play() { Name = "流浪法師", Power = 120 }; Thread thread_first = new Thread(play1.physicsExecute); //物理攻擊線程 Thread thread_second = new Thread(play2.magicExecute); //魔法攻擊線程  thread_first.Start(monster); thread_second.Start(monster); Console.ReadKey(); }

輸出結果:

總結:

  第一種狀況:

  1. thread_first首先得到同步對象的鎖,當執行到 Monitor.Wait(monster);時,thread_first線程釋放本身對同步對象的鎖,流放本身到等待隊列,直到本身再次得到鎖,不然一直阻塞。

  2. 而thread_second線程一開始就競爭同步鎖因此處於就緒隊列中,這時候thread_second直接從就緒隊列出來得到了monster對象鎖,開始執行到Monitor.PulseAll(monster)時,發送了個Pulse信號。

  3. 這時候thread_first接收到信號進入到就緒狀態。而後thread_second繼續往下執行到 Monitor.Wait(monster, 1000)時,這是一句很是關鍵的代碼,thread_second將本身流放到等待隊列並釋放自身對同步鎖的獨佔,該等待設置了1S的超時值,當B線程在1S以內沒有再次獲取到鎖自動添加到就緒隊列。

  4. 這時thread_first從Monitor.Wait(monster)的阻塞結束,返回true。開始執行、打印。執行下一行的Monitor.Pulse(monster),這時候thread_second假如1S的時間還沒過,thread_second接收到信號,因而將本身添加到就緒隊列。

  5. thread_first的同步代碼塊結束之後,thread_second再次得到執行權, Monitor.Wait(m_smplQueue, 1000)返回true,因而繼續從該代碼處往下執行、打印。當再次執行到Monitor.Wait(monster, 1000),又開始了步驟3。

  6. 依次循環。。。。

   第二種狀況thread_second首先得到同步鎖對象,首先執行到Monitor.PulseAll(monster),由於程序中沒有須要等待信號進入就緒狀態的線程,因此這一句代碼沒有意義,當執行到 Monitor.Wait(monster, 1000),自動將本身流放到等待隊列並在這裏阻塞,1S 時間事後thread_second自動添加到就緒隊列,線程thread_first得到monster對象鎖,執行到Monitor.Wait(monster);時發生阻塞釋放同步對象鎖,線程thread_second執行,執行Monitor.PulseAll(monster)時通知thread_first。因而又開始第一種狀況...

Monitor.Wait是讓當前進程睡眠在臨界資源上並釋放獨佔鎖,它只是等待,並不退出,當等待結束,就要繼續執行剩下的代碼。

(四) 使用Mutex類實現線程同步

        Mutex的突出特色是能夠跨應用程序域邊界對資源進行獨佔訪問,便可以用於同步不一樣進程中的線程,這種功能固然這是以犧牲更多的系統資源爲代價的。

  主要經常使用的兩個方法:

public virtual bool WaitOne()   阻止當前線程,直到當前 System.Threading.WaitHandle 收到信號獲取互斥鎖。

 public void ReleaseMutex()     釋放 System.Threading.Mutex 一次。

  使用實例:

    static void Main(string[] args) { Thread[] thread = new Thread[3]; for (int i = 0; i < 3; i++) { thread[i] = new Thread(ThreadMethod1); thread[i].Name = i.ToString(); } for (int i = 0; i < 3; i++) { thread[i].Start(); } Console.ReadKey(); } public static void ThreadMethod1(object val) { mutet.WaitOne();    //獲取鎖 for (int i = 0; i < 500; i++) { Console.Write(Thread.CurrentThread.Name); } mutet.ReleaseMutex(); //釋放鎖  }

 四  如何使用.NET中的線程池?

  (1).NET中的線程池是神馬

  咱們都知道,線程的建立和銷燬須要很大的性能開銷,在Windows NT內核的操做系統中,每一個進程都會包含一個線程池。而在.NET中呢,也有本身的線程池,它是由CLR負責管理的。

  線程池至關於一個緩存的概念,在該池中已經存在了一些沒有被銷燬的線程,而當應用程序須要一個新的線程時,就能夠從線程池中直接獲取一個已經存在的線程。相對應的,當一個線程被使用完畢後並不會馬上被銷燬,而是放入線程池中等待下一次使用

  .NET中的線程池由CLR管理,管理的策略是靈活可變的,所以線程池中的線程數量也是可變的,使用者只需向線程池提交需求便可,下圖則直觀地展現了CLR是如何處理線程池需求的:

PS:線程池中運行的線程均爲後臺線程(即線程的 IsBackground 屬性被設爲true),所謂的後臺線程是指這些線程的運行不會阻礙應用程序的結束。相反的,應用程序的結束則必須等待全部前臺線程結束後才能退出。

  (2)在.NET中使用線程池

  在.NET中經過 System.Threading.ThreadPool 類型來提供關於線程池的操做,ThreadPool 類型提供了幾個靜態方法,來容許使用者插入一個工做線程的需求。經常使用的有如下三個靜態方法:

  ① static bool QueueUserWorkItem(WaitCallback callback)

  ② static bool QueueUserWorkItem(WaitCallback callback, Object state)

  ③ static bool UnsafeQueueUserWorkItem(WaitCallback callback, Object state)

  有了這幾個方法,咱們只須要將線程要處理的方法做爲參數傳入上述方法便可,隨後的工做都由CLR的線程池管理程序來完成。其中,WaitCallback 是一個委託類型,該委託方法接受一個Object類型的參數,而且沒有返回值。下面的代碼展現瞭如何使用線程池來編寫多線程的程序:

    class Program { static void Main(string[] args) { string taskInfo = "運行10秒"; // 插入一個新的請求到線程池 bool result = ThreadPool.QueueUserWorkItem(DoWork, taskInfo); // 分配線程有可能會失敗 if (!result) { Console.WriteLine("分配線程失敗"); } else { Console.WriteLine("按回車鍵結束程序"); } Console.ReadKey(); } private static void DoWork(object state) { // 模擬作了一些操做,耗時10s for (int i = 0; i < 10; i++) { Console.WriteLine("工做者線程的任務是:{0}", state); Thread.Sleep(1000); } } }

  上述代碼執行後,若是不輸入任何字符,那麼會獲得以下圖所示的執行結果:

PS:事實上,UnsafeQueueWorkItem方法實現了徹底相同的功能,兩者的差異在於UnsafeQueueWorkItem方法不會將調用線程的堆棧傳遞給輔助線程,這就意味着主線程的權限限制不會傳遞給輔助線程。UnsafeQueueWorkItem因爲不進行這樣的傳遞,所以會獲得更高的運行效率,可是潛在地提高了輔助線程的權限,也就有可能會成爲一個潛在的安全漏洞。

五 如何查看和設置線程池的上下限?

  線程池的線程數是有限制的,一般狀況下,咱們無需修改默認的配置。但在一些場合,咱們可能須要瞭解線程池的上下限和剩餘的線程數。線程池做爲一個緩衝池,有着其上下限。在一般狀況下,當線程池中的線程數小於線程池設置的下限時,線程池會設法建立新的線程,而當線程池中的線程數大於線程池設置的上限時,線程池將銷燬多餘的線程

PS:在.NET Framework 4.0中,每一個CPU默認的工做者線程數量最大值爲250個,最小值爲2個。而IO線程的默認最大值爲1000個,最小值爲2個。

  在.NET中,經過 ThreadPool 類型提供的5個靜態方法能夠獲取和設置線程池的上限和下限,同時它還額外地提供了一個方法來讓程序員獲知當前可用的線程數量,下面是這五個方法的簽名:

  ① static void GetMaxThreads(out int workerThreads, out int completionPortThreads)

  ② static void GetMinThreads(out int workerThreads, out int completionPortThreads)

  ③ static bool SetMaxThreads(int workerThreads, int completionPortThreads)

  ④ static bool SetMinThreads(int workerThreads, int completionPortThreads)

  ⑤ static void GetAvailableThreads(out int workerThreads, out int completionPortThreads)

  下面的代碼示例演示瞭如何查詢線程池的上下限閾值和可用線程數量:

    class Program { static void Main(string[] args) { // 打印閾值和可用數量  GetLimitation(); GetAvailable(); // 使用掉其中三個線程 Console.WriteLine("此處申請使用3個線程..."); ThreadPool.QueueUserWorkItem(Work); ThreadPool.QueueUserWorkItem(Work); ThreadPool.QueueUserWorkItem(Work); Thread.Sleep(1000); // 打印閾值和可用數量  GetLimitation(); GetAvailable(); // 設置最小值 Console.WriteLine("此處修改了線程池的最小線程數量"); ThreadPool.SetMinThreads(10, 10); // 打印閾值  GetLimitation(); Console.ReadKey(); } // 運行10s的方法 private static void Work(object o) { Thread.Sleep(10 * 1000); } // 打印線程池的上下限閾值 private static void GetLimitation() { int maxWork, minWork, maxIO, minIO; // 獲得閾值上限 ThreadPool.GetMaxThreads(out maxWork, out maxIO); // 獲得閾值下限 ThreadPool.GetMinThreads(out minWork, out minIO); // 打印閾值上限 Console.WriteLine("線程池最多有{0}個工做者線程,{1}個IO線程", maxWork.ToString(), maxIO.ToString()); // 打印閾值下限 Console.WriteLine("線程池最少有{0}個工做者線程,{1}個IO線程", minWork.ToString(), minIO.ToString()); Console.WriteLine("------------------------------------"); } // 打印可用線程數量 private static void GetAvailable() { int remainWork, remainIO; // 獲得當前可用線程數量 ThreadPool.GetAvailableThreads(out remainWork, out remainIO); // 打印可用線程數量 Console.WriteLine("線程池中當前有{0}個工做者線程可用,{1}個IO線程可用", remainWork.ToString(), remainIO.ToString()); Console.WriteLine("------------------------------------"); } }

  該實例的執行結果以下圖所示:

PS:上面代碼示例在不一樣的計算機上運行可能會獲得不一樣的結果,線程池中的可用數碼不會再初始時達到最大值,事實上CLR會嘗試以必定的時間間隔來逐一地建立新線程,但這個時間間隔很是短。

相關文章
相關標籤/搜索