接着前一篇博文的內容我們開始學習線程的同步和異步相關的內容,很多自學的新手同學可能精力的回避這個問題,其實很簡單的,下面先給那些不理解這個概念的同學講兩個關於某人的故事,聽完了,你就明白實戰出真理的道理了(如果新手從本文中略有所獲就支持一下同樣是新手的偶,給我個信息知道我沒耽誤你的時間,當然大家可以給我點建設性的意見和指導)。
什么是線程同步?
從前某人混社會的時候,某人第一次去江湖廝殺,結果馬上就遇到了地頭蛇,某人靠被砍的很慘,連掏手機打個電話的機會都不給我,這時候有兩條路給某人選擇,要嗎掏手機命沒了,要嗎繼續扛着結果手機沒法掏出來,於是某人發揮了珍惜生命的優良傳統,某人就硬是扛到最后。你現在明白我在干什么了,呵呵,這就是同步,就是說你在干一件事的時候不能去同時做別的事情,否則就會發生意外(那就是你把命掉了)。這就是線程同步的意義,假設多個線程同時訪問一個資源,.NET的定義的線程優先級還要看操作系統的心情,人家愛理不理,此外就是你應該有這么一個概念:Win32線程調度程序和CLR允許線程可以自由的跨越應用程序域的邊界,但是任何一個時間點上,任何應用程序域上都可以有多個線程,但是一個線程只能在一個特定的應用程序域內,也就是說一個線程在任何時刻在多個應用程序域內是不可能同時執行的。很經典的例子就是那個啥,假設你不用多線程你使用Winfrom在其中運行一個超大循環的時候,(我發揮我高超的美術功底畫了個了畫演示一下)(這個程序雖然簡單但是涉及一個跨線程訪問的問題,如果有時間就到后面的博文拿出來分析一下)。
你再去拖動程序窗體,結果你發現你力氣沒window力氣大,而且還吃了沒文化的虧,因為這個循環的執行和你這個托這個動作都是出於同一個線程(主線程中),同時干這兩件事,如果你亂來,就算拖動了,他也就崩潰了,哈哈,現在明白了吧,這就是同步。
現在我們解釋什么是異步!
經過第一次的教訓出去的時候某人帶了給小弟,結果還是更背,這次遇到了古惑仔,但是某人在招架的時候給小弟用嘴說你快打電話給老子搬救兵,最少三卡車,結果,某人沒有自己掏電話,某人只是用特定的信息告訴小弟,某人繼續干自己的事,搬救兵的事小弟就替我辦了,結果這次某人沒有吃虧,還是某人在小學學好了統籌學,同樣的時間,辦了幾件事,還是小弟聽話,某人自豪的笑了。多說無益,怕兄弟們煩,這就是異步操作,ajax的異步機制就是這樣了,可以實現網頁的局部更新等等......就不扯到asp.net上了,現在你應該明白了什么是異步了吧。
我在這系列博文中不打算提到線程的優先級以及線程狀態,這些大家就自己看吧,畢竟是次要的內容,瞄一眼就懂了。
.NET提供了兩種方法可以實現同步,就是簡單方法和高級方法,這就廢話了,哈哈,對了據說.NET1.0還支持跨進程訪問線程的,后還就給禁止掉了,不過現在還是可以通過取消跨進程訪問的某個屬性可以解除這種不安全的訪問手段,靠又扯遠了。
繼續,簡單方法就是輪詢和等待,高級方法就是使用同步對象。
什么是輪詢?
輪詢其實效率低的都沒人去用它,它的原理就是循環的去偵聽線程的狀態,而且就算偵聽到了都有可能誤判,假設砸門使用IsAlive來檢測某個線程是否退出的時候我們要有這么個概念處於活動狀態的線程不一定是運行的,就是說他亦可能出於休眠狀態。
什么又是等待?
所謂的等待,懂漢語的孩子都知道就是等着某個對象辦事情了,這個我在前面的一篇博文用到的很多,那就是Join,同樣,好多小弟弟們搞不明白Join到底是啥,好多文章加了那么個調用線程#¥%……的概念就把孩子們搞暈了,看群里搞暈的小弟弟還是蠻多的,其實你管那么多干嘛,試驗下不就知道了,下面由於我在前面的博文中已經大量使用Join了,這里就不演示了,最要給個通俗的解釋就是那個線程執行了Join方法,那么其他的線程都必須等待到該線程執行完為止才會有反應,但是我在這里再補充一下,就是這個“執行完”也是相對的,就是在該線程的執行過程中萬一有調用了Sleep()方法休眠了一下,那么別的線程管你還有沒有執行完,CPU就會把時間片分給別的線程去執行他們的任務,直到這個線程再次喚醒為止。當然既然是簡單問題肯定只能解決簡單的邏輯,要是流程一復雜,你就找比爾蓋茲的工程師給你用Join給你解決線程同步的問題吧^@^!具體的實例可以看上一篇文章。
演示並發
說了這么久,我們干脆先演示下並發的程序,來分析下並發產生的問題,在進入下文介紹解決並發的高級方法。
先在有這么一點代碼,先貼出來咱們在分析。
1 using System; 2 using System.Threading; 3 public class Printer 4 { 5 public void PrintNumbers() 6 { 7 8 for (int i = 0; i < 10; i++) 9 { 10 Random r = new Random(); 11 Thread.Sleep(500 * r.Next(3)); 12 Console.Write("{0}, ", i); 13 } 14 Console.WriteLine(); 15 } 16 class Program 17 { 18 static void Main(string[] args) 19 { 20 Console.WriteLine("*****線程同步 *****\n"); 21 22 Printer p = new Printer(); 23 24 Thread[] threads = new Thread[10]; 25 for (int i = 0; i < 10; i++) 26 { 27 threads[i] =new Thread(new ThreadStart(p.PrintNumbers)); 28 threads[i].Name = string.Format("工作線程 thread #{0}啟動執行!", i); 29 Console.WriteLine(threads[i].Name); 30 } 31 foreach (Thread t in threads) 32 t.Start(); 33 Console.ReadLine(); 34 } 35 } 36 }
乍一看,咋們肯定知道要是按正常的運行,肯定是每次循環輸出10個數字,但是結果並非我們一廂情願,我們運行看一下:(注:由於使用隨機函數這個結果只是其中一種)
結果我們發現結果不是我們預料大的,毛呀,這是腫么了,有木有搞錯啊,為什么會這樣?
threads[i] =new Thread(new ThreadStart(p.PrintNumbers));
我們來看這一句代碼,我們發現每一個線程都是調用同一個對象p的PrintNumbers方法,或許這只是個線索而已,
接着我們再看這句:Thread.Sleep(500 * r.Next(3));
我們就會發現,這里隨機掛起線程的時間不能確定 ,可能的情況就是當即將發生printNumbers方法的時候,還沒等輸出
到控制台,當前的線程就被掛起了,win32的線程調度程序就切換線程,於是就發生了我們不可預料的結果。
怎么解決了,哈哈是不是想用上面的上面的Join一下啊,完全可以有什么不可以的,咋們試試先,不過我們要引入高級方法,
又要用低級方法驗證一下,可不可以,我們的思路是什么呢:就是要線程調度程序等到哥執行完了當前線程再去執行下一個線
程,稍加修改代碼我們測試一下,
我們只在這里動一下手腳:
foreach (Thread t in threads) { t.Start(); t.Join(); }
但是結果就立馬不同了,因為我們都知道每個線程都執行完成了,結果如你所願:
那么怎么用高級方法解決這個問題呢?
我們為了節約時間先做一個最簡單lock方法,這個其實是為了方便Monitor類的使用應用而生(完全等價於Monitor類的調用形式)。具體的后期再解釋,咋們先只要知道高級方法解決這些問題,還是最優的選擇就行了,先有個大概影響,后續博文慢慢研究。不可能一次都寫完,我還要上課,還要去看電影,呵呵....
我們先解釋下lock關鍵字,這個關鍵字允許定義一段線程定義的代碼語句,后進入的線程不會中斷當前的線程,而是如同實現Join類似的功能,停止自身的線程執行。lock需要指定一個標記(即一個對象的引用),你不指定,你鎖了人家大門咋辦,這個要注意O(∩_∩)O哈!當線程進入鎖定范圍的時候就需要獲得這個標記,知道你家大門被鎖了,進不去了,那就等等唄。
當然,如果我們去鎖定一個實例對象的私有方法的時候,這個方法只有你這個對象可以訪問,那么這個對象的引用(也就是標記)使用方法本身的對象引用就OK了,簡單點就是this.鎖定的就是你自家的門啦。。。
1 private void SomePrivateMethod() 2 { 3 //使用當前對象為鎖定標記 4 lock(this) 5 { 6 //這個語句塊(范圍)中的代碼是線程安全的 7 } 8 }
問題是我們有個鎖子不一定所的都是自家的大門,指不定你就是個看大門的,哈哈。這樣問題就來了,如果鎖定公共成員中的一段代碼,.NET推薦的方式就是使用Object成原來作為鎖標記,所謂的標記你別看的那么神聖,那就是個ID而已,就是用來唯一識別的而已,不要深究這個問題。
1 private Object myLock=new Object(); 2 public void PrintNumbers() 3 { 4 //使用Object成員作為鎖標記 5 lock ( myLock) 6 { 7 ... 8 } 9 } 10 }
好了,當我們了解完這些我們就開始解決上面的那個問題,我們分析,每次循環的線程掛起和控制台輸出的部分有可能出問題,OK,那我們把它鎖起來,於是我們這樣做了:
1 public void PrintNumbers() 2 { 3 lock (myLock) 4 { 5 for (int i = 0; i < 10; i++) 6 { 7 Random r = new Random(); 8 Thread.Sleep(500 * r.Next(3)); 9 Console.Write("{0}, ", i); 10 } 11 Console.WriteLine(); 12 } 13 }
我們運行看下效果:
事實證明我們成功的掌握了基本的解決多線程並發的一點點知識。
本篇博客結束語:
多線程雖然可以發揮我們多核電腦的優勢,即便是俺曾經用過的那台02年單核的邵陽筆記本也是支持超線程的,但是不是任何時候都可以用多線程,就好比再貴的法拉利你在我們這里的山村里你也飆不起來,但是要是讓我開着拖拉機上了高速公路跑是可以跑,就是都遭人圍觀,於是有生之年哥決定要開個悍馬,高速山村都能爽爽的跑上那么一會!!開玩笑,意思就是,不一定多核了你訪問的資源就多了,看情況決定使用多線程這才是正確的決定。
如果我在寫博客的過程中幫到了你,就支持自學的孩子們包括我一下!