c# 溫故而知新: 線程篇(一)
Thread
目錄:
- 目錄:
- 1 線程基礎的簡單介紹
- 2 線程同步與線程異步的簡單介紹
- 3 前台線程與后台線程的簡單介紹
- 4 細說下Thread 最為關鍵的構造函數
- 5 細說下Thread 的 Sleep方法
- 6 細說下Thread 的 join 方法
- 7 細說下Thread 的 Abort和 Interrupt方法
- 8 細說下Thread 的 Suspend,Resume方法
- 9 簡單了解下Thread 的 一些重要屬性
- 10 簡單示例
- 多線程從一個圖片中截取部分圖片
- 11 本章總結
首先讓我們翻開書本來了解下線程的一些基礎知識:
1 線程有時被稱為輕量級進程,是程序執行流的最小單元 2 線程時由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。 3 線程自身不能擁有系統資源,但是可以使用線程所屬進程所占有的系統資源 4 線程可以創建和撤銷另一個線程 5 線程可以擁有自身的狀態,例如 運行狀態,掛起狀態,銷毀釋放狀態等等 6 線程具有優先級,每個線程都分配了0-31 級別的其中一個優先級,數字越大,優先級越高,然而手動分配優先級過於復雜, 所以微軟為我們的Thread類提供一個優先級的枚舉,ThreadPriority枚舉便是優先級枚舉,我們可以利用thread.Priority屬性來進行設置 7 線程開銷,這個是個復雜的話題,希望有機會的話能夠單獨寫一遍文章解釋下 |
那么多線程有什么實際好處呢?
首先讓我們了解下多線程的概念:一個程序或者進程中同時運行多個線程完成不同的工作
從概念中我們便可知道多線程的優點了
1 能夠實現並行操作,也就是說多個線程可以同時進行工作 2 利用多線程后許多復雜的業務或者是計算可以交給后台線程去完成,從而提高整體程序的性能 3 類似於第一條利用多線程可以達到異步的作用(注意,實現異步的一種方式是多線程) |
當然多線程也有一定的問題需要注意,那就是線程同步問題,關於這個問題我會今后的文章中詳細說明
*1 線程同步
關於線程同步的概念最簡單的理解就是
同步方法調用在程序繼續執行之前,需要等待同步方法執行完畢返回結果
很有可能多個線程都會對一個資源進行訪問,從而導致資源被破壞,所以必須采用線程的同步機制,例如為共享資源加鎖 ,當其中一個線程占有了鎖之后,其余線程均不能使用共享資源,只有等其釋放鎖之后,接下來的其中一個線程會占有該 鎖,本系列會從Thread類開始講起,以后多章都會討論線程同步機制,例如鎖機制,臨界區,互斥,信號量 同步事件等待句柄; 等等 |
*2 線程異步
線程異步指的是一個調用請求發送給被調用者,而調用者不用等待其結果的返回,一般異步執行的任務都需要比較長的時間,
所以為了不影響主線程的工作,可以使用多線程或者新開辟一個線程來實現異步,同樣,異步和線程池也有着非常緊密的聯系,
這點我會在今后有關線程池的文章中詳細敘述,線程池和異步線程將在第二章中詳細闡述下
前台線程:
諸如我們Console程序的主線程,wpf或者sliverlight的 界面線程等等,都屬於前台線程,一旦前台線程奔潰或者終止,相應的后台
線程都會終止,本章中通過Thread類產生的線程默認都是前台線程,當然我們可以設置Thread的屬性讓該對象成為后台線程,必須
注意的是,一旦前台線程全部運行完畢,應用程序的進程也會釋放,但是假設Console程序中main函數運行完畢,但是其中幾個前台
線程還處在運行之中,那么這個Console程序的進程是不會釋放的,仍然處於運行之中,直到所有的前台線程都釋放為止
后台線程:
和前台線程唯一的區別是,后台線程更加默默無聞,甚至后台線程因某種情況,釋放銷毀時不會影響到進程,也就是說后台線程釋放時
不會導致進程的釋放
用一個例子再來說明下前后台線程的區別: 有時我們打開outlook 后接受郵件時,程序會失去響應或被卡住,這時候我們去點擊outlook時系統會提示 outlook 失去響應,是否等待或者關閉, 當我們點擊關閉時,其實在程序中關於outlook的所有運行的前台線程被終止,導致了outlook被關閉了,其進程也隨之釋放消失。但是,當我們在 outlook中點擊更新郵件時,后台線程會去收取郵件的工作,我們可以在此期間關閉 outlook接受新郵件的后台線程,而不會導致整個outlook的關閉 |
相信大家再看過前幾章對於線程的介紹后,對線程應該有一個溫故的感覺,那么讓我們開始對thread這個線程類進行深層次的研究下,
首先要啟動一個線程必須將該線程將要做的任務告訴該線程,否則,線程會不知道干什么事導致線程無意義的開啟,浪費系統資源,果然,
Thread類的構造函數提供了以下的版本
ThreadStart 和 ParameterThreadStart 參數都是委托,所以可以看出委托其實就是方法的抽象,前者用於不帶參數的並且無返回值的
方法的抽象,后者是帶object參數的方法的抽象,大家通過以下簡單的方法注意下線程如何調用帶參數的方法
public class ThreadStartTest { //無參數的構造函數 Thread thread = new Thread(new ThreadStart(ThreadMethod)); //帶有object參數的構造函數 Thread thread2 = new Thread(new ParameterizedThreadStart(ThreadMethodWithPara)); public ThreadStartTest() { //啟動線程1 thread.Start(); //啟動線程2 thread2.Start(new Parameter { paraName="Test" }); } static void ThreadMethod() { //.... } static void ThreadMethodWithPara(object o) { if (o is Parameter) { // (o as Parameter).paraName............. } } } public class Parameter { public string paraName { get; set; } }
不帶參數的方法似乎很簡單的能被調用,只要通過第一個構造函數便行,對於帶參數的方法,大家注意下參數是如何傳入線程所調用的方法,
當啟動線程時,參數通過thread.Start方法傳入,於是我們便成功啟動了thread線程,大伙可千萬不要小看基礎啊,往往在復雜的項目中很多
就是因為一些基礎導致,所以一定不要忽視它。。。
話說微軟對Thread.Sleep方法的解釋過於簡單,導致許多人會誤認為這個方法並不重要,其實這是錯誤的,其實線程是非常復雜的,
而且我們圍繞這個方法來溫故下windows系統對於CPU競爭的策略:
所謂搶占式操作系統,就是說如果一個進程得到了 CPU 時間,除非它自己放棄使用 CPU ,否則將完全霸占 CPU 。因此可以看出,
在搶占式操作系統中,操作系統假設所有的進程都是“人品很好”的,會主動退出 CPU 。
發現寫到這里貌似真的已經比較復雜了,由於本人對操作系統底層的知識比較匱乏,決定還是引用下別人的理解,順便自己也學習下
引用:
假設有源源不斷的蛋糕(源源不斷的時間),一副刀叉(一個CPU),10個等待吃蛋糕的人(10 個進程)。如果是 Unix 操作系統來負責分蛋糕, 那么他會這樣定規矩:每個人上來吃 1 分鍾,時間到了換下一個。最后一個人吃完了就再從頭開始。於是,不管這10個人是不是優先級不同、飢餓 程度不同、飯量不同,每個人上來的時候都可以吃 1 分鍾。當然,如果有人本來不太餓,或者飯量小,吃了30秒鍾之后就吃飽了,那么他可以跟操 作系統說:我已經吃飽了(掛起)。於是操作系統就會讓下一個人接 着來。如果是 Windows 操作系統來負責分蛋糕的,那么場面就很有意思了。 他會這樣定規矩:我會根據你們的優先級、飢餓程度去給你們每個人計算一個優先級。優先級最高的那個人,可 以上來吃蛋糕——吃到你不想吃為止。 等這個人吃完了,我再重新根據優先級、飢餓程度來計算每個人的優先級,然后再分給優先級最高的那個人。這樣看來,這個 場面就有意思了—— 可能有些人是PPMM,因此具有高優先級,於是她就可以經常來吃蛋糕。可能另外一個人的優先級特別低,於是好半天了才輪到他一次(因為 隨着時間 的推移,他會越來越飢餓,因此算出來的總優先級就會越來越高,因此總有一天會輪到他的)。而且,如果一不小心讓一個大胖子得到了刀叉,因為他 飯量 大,可能他會霸占着蛋糕連續吃很久很久,導致旁邊的人在那里咽口水。。。而且,還可能會有這種情況出現:操作系統現在計算出來的結果,是 5號PPMM總優 先級最高——高出別人一大截。因此就叫5號來吃蛋糕。5號吃了一小會兒,覺得沒那么餓了,於是說“我不吃了”(掛起)。因此操作系 統就會重新計算所有人的 優先級。因為5號剛剛吃過,因此她的飢餓程度變小了,於是總優先級變小了;而其他人因為多等了一會兒,飢餓程度都變大了, 所以總優先級也變大了。不過這時 候仍然有可能5號的優先級比別的都高,只不過現在只比其他的高一點點——但她仍然是總優先級最高的啊。因此操作 系統就會說:5號mm上來吃蛋糕……(5號 mm心里郁悶,這不剛吃過嘛……人家要減肥……誰叫你長那么漂亮,獲得了那么高的優先級)。那么, Thread.Sleep 函數是干嗎的呢?還用剛才的分蛋糕的場景來描述。上面的場景里面,5號MM在吃了一次蛋糕之后,覺得已經有8分飽了,她覺得在未來 的半個小時之內都不想再 來吃蛋糕了,那么她就會跟操作系統說:在未來的半個小時之內不要再叫我上來吃蛋糕了。這樣,操作系統在隨后的半個小時 里面重新計算所有人總優先級的時候, 就會忽略5號mm。Sleep函數就是干這事的,他告訴操作系統“在未來的多少毫秒內我不參與CPU競爭”。 |
為什么我要把Thread.Join()方法單獨細說下,個人認為join方法非常重要,
在細說前我想再次強調下主線程和子線程的區別:
首先大家肯定知道在Console程序中,主線程自上而下着運行着main函數,假如我們在main函數中新增一個線程thread對象的話,
也就是說,在主線程中再開啟一個子線程,同時子線程和主線程可以同時工作(前提是子線程使用Start方法),同理,假如我在這
個子線程中再開辟一個屬於這個子線程的子線程,同理這3個爺爺,父親,兒子線程也可以使用Start()方法一起工作,假如在主線
程中添加2個thread對象並開啟,那么這2 線程便屬於同一層次的線程(兄弟線程)(和優先級無關,只同一位置層次上的兄弟),
有可能上述的讓你覺得郁悶或者難以理解?沒關系看簡單例子就能夠理解了
public static void ShowFatherAndSonThread(Thread grandFatherThread) { Console.WriteLine("爺爺主線程名:{0}", grandFatherThread.Name); Thread brotherThread = new Thread(new ThreadStart(() => { Console.WriteLine("兄弟線程名:{0}", Thread.CurrentThread.Name); })); Thread fatherThread = new Thread(new ThreadStart( () => { Console.WriteLine("父親線程名:{0}", Thread.CurrentThread.Name); Thread sonThread = new Thread(new ThreadStart(() => { Console.WriteLine("兒子線程名:{0}", Thread.CurrentThread.Name); })); sonThread.Name = "SonThread"; sonThread.Start(); } )); fatherThread.Name = "FatherThread"; brotherThread.Name="BrotherThread"; fatherThread.Start(); brotherThread.Start(); }
言歸正傳讓我們溫故下Jion方法,先看msdn中是怎么解釋的:
繼續執行標准的 COM 和 SendMessage 消息泵處理期間,阻塞調用線程,直到某個線程終止為止。
大家把注意力移到后面紅色的部分,什么是“調用線程”呢?如果你理解上述線程關系的話,可能已經理解了,主線程(爺爺輩)的調用了父親線程,
父親線程調用了兒子線程,假設現在我們有一個奇怪的需求,必須開啟爺爺輩和父親輩的線程但是,爺爺輩線程必須等待父親線程結束后再進行,
這該怎么辦? 這時候Join方法上場了,我們的目標是阻塞爺爺線程,那么后面的工作就明確了,讓父親線程(thread)對象去調用join方法就行
一下是個很簡單的例子,讓大家再深入理解下。
public static void ThreadJoin() { Console.WriteLine("我是爺爺輩線程,子線程馬上要來工作了我得准備下讓個位給他。"); Thread t1 = new Thread( new ThreadStart ( () => { for (int i = 0; i < 10; i++) { if (i == 0) Console.WriteLine("我是父親線層{0}, 完成計數任務后我會把工作權交換給主線程", Thread.CurrentThread.Name); else { Console.WriteLine("我是父親線層{0}, 計數值:{1}", Thread.CurrentThread.Name, i); } Thread.Sleep(1000); } } ) ); t1.Name = "線程1"; t1.Start(); //調用join后調用線程被阻塞
t1.Join(); Console.WriteLine("終於輪到爺爺輩主線程干活了"); }
代碼中當父親線程啟動后會立即進入Jion方法,這時候調用該線程爺爺輩線程被阻塞,直到父親線程中的方法執行完畢為止,最后父親線程將控制
權再次還給爺爺輩線程,輸出最后的語句。聰明的你肯定會問:兄弟線程怎么保證先后順序呢?很明顯如果不使用join,一並開啟兄弟線程后結果
是隨機的不可預測的(暫時不考慮線程優先級),但是我們不能在兄弟線程全都開啟后使用join,這樣阻塞了父親線程,而對兄弟線程是無效的,
其實我們可以變通一下,看以下一個很簡單的例子:
public static void ThreadJoin2() { IList<Thread> threads = new List<Thread>(); for (int i = 0; i < 3; i++) { Thread t = new Thread( new ThreadStart( () => { for (int j = 0; j < 10; j++) { if (j == 0) Console.WriteLine("我是線層{0}, 完成計數任務后我會把工作權交換給其他線程", Thread.CurrentThread.Name); else { Console.WriteLine("我是線層{0}, 計數值:{1}", Thread.CurrentThread.Name, j); } Thread.Sleep(1000); } })); t.Name = "線程" + i; //將線程加入集合 threads.Add(t); } foreach (var thread in threads) { thread.Start(); //每次按次序阻塞調用次方法的線程 thread.Join(); } }
輸出結果:
但是這樣我們即便能達到這種效果,也會發現其中存在着不少缺陷:
1:必須要指定順序
2:一旦一個運行了很久,后續的線程會一直等待很久
3: 很容易產生死鎖
從前面2個例子能夠看出 jion是利用阻塞調用線程的方式進行工作,我們可以根據需求的需要而靈活改變線程的運行順序,但是在復雜的項目或業務中
對於jion方法的調試和糾錯也是比較困難的。
7 細說下Thread 的 Abort和 Interrupt方法
Abort 方法:
其實 Abort 方法並沒有像字面上的那么簡單,釋放並終止調用線程,其實當一個線程調用 Abort方法時,會在調用此方法的線程上引發一個異常:
ThreadAbortException ,讓我們一步步深入下對這個方法的理解:
1 首先我們嘗試對主線程終止釋放
static void Main(string[] args) { try { Thread.CurrentThread.Abort(); } catch { //Thread.ResetAbort(); Console.WriteLine("主線程接受到被釋放銷毀的信號"); Console.WriteLine( "主線程的狀態:{0}",Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("主線程最終被被釋放銷毀"); Console.WriteLine("主線程的狀態:{0}", Thread.CurrentThread.ThreadState); Console.ReadKey(); } }
從運行結果上看很容易看出當主線程被終止時其實報出了一個ThreadAbortException, 從中我們可以進行捕獲,但是注意的是,主線程直到finally語
句塊執行完畢之后才真正結束(可以仔細看下主線程的狀態一直處於AbortRequest),如果你在finally語句塊中執行很復雜的邏輯或者計算的話,那
么只有等待直到運行完畢才真正銷毀主線程(也就是說主線程的狀態會變成Aborted,但是由於是主線程所以無法看出).
2 嘗試終止一個子線程
同樣先看下代碼:
static void TestAbort() { try { Thread.Sleep(10000); } catch { Console.WriteLine("線程{0}接受到被釋放銷毀的信號",Thread.CurrentThread.Name); Console.WriteLine("捕獲到異常時線程{0}主線程的狀態:{1}", Thread.CurrentThread.Name,Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("進入finally語句塊后線程{0}主線程的狀態:{1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } } Main: static void Main(string[] args) { Thread thread1 = new Thread(TestAbort); thread1.Name = "Thread1"; thread1.Start(); Thread.Sleep(1000); thread1.Abort(); thread1.Join(); Console.WriteLine("finally語句塊后,線程{0}主線程的狀態:{1}", thread1.Name, thread1.ThreadState); Console.ReadKey(); }
了解了主線程的銷毀釋放后,再來看下子線程的銷毀釋放的過程(Start->abortRequested->Aborted->Stop),從最后輸出的狀態變化來看,
子線程thread1 的狀態變化是十分清楚的,幾乎和主線程的例子一致,唯一的區別是我們在 main方法中故意讓主線程阻塞這樣能看見thread 1
在 finally語句塊后的狀態
3,嘗試對尚未啟動的線程調用Abort
如果對一個尚未啟動的線程調用Abort的話,一旦該線程啟動就被停止了
4 嘗試對一個掛起的線程調用Abort
如果在已掛起的線程上調用 Abort,則將在調用 Abort 的線程中引發 ThreadStateException,並將 AbortRequested 添加到被中止的線程的
ThreadState 屬性中。直到調用 Resume 后,才在掛起的線程中引發 ThreadAbortException。如果在正在執行非托管代碼的托管線程上調用 Abort,
則直到線程返回到托管代碼才引發 ThreadAbortException。
Interrupt 方法:
Interrupt 方法將當前的調用該方法的線程處於掛起狀態,同樣在調用此方法的線程上引發一個異常:ThreadInterruptedException,和Abort方法不同的是,
被掛起的線程可以喚醒
static void Main(string[] args) { Thread thread1 = new Thread(TestInterrupt); thread1.Name = "Thread1"; thread1.Start(); Thread.Sleep(1000); thread1.Interrupt(); thread1.Join(); Console.WriteLine("finally語句塊后,線程{0}主線程的狀態:{1}", thread1.Name, thread1.ThreadState); Console.ReadKey(); } static void TestInterrupt() { try { Thread.Sleep(3000); } catch (ThreadInterruptedException e) { Console.WriteLine("線程{0}接受到被Interrupt的信號", Thread.CurrentThread.Name); Console.WriteLine("捕獲到Interrupt異常時線程{0}的狀態:{1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("進入finally語句塊后線程{0}的狀態:{1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } }
從代碼中可以看出,當線程調用Interrupted后,它的狀態是已中斷的.這個狀態對於正在執行join,sleep的線程,卻改變了線程的運行結果
.因為它正在某一對象的休息室中,這時如果它的中斷狀態被改變,那么它就會拋出ThreadInterruptedException異常,意思就是這個線程不能再等待了,其意義就等同於喚醒它了。
讓我們想象一下我們將一個線程設置了其長達1星期的睡眠時間,有時后必須喚醒它,上述方法就能實現這點
8 細說下Thread 的 Suspend,Resume方法
Suspend 和Resume方法很奧妙,前者將當前運行的線程掛起,后者能夠恢復當錢被掛起的線程
Thread thread1 = new Thread(TestSuspend); Thread thread2 = new Thread(TestSuspend); thread1.Name = "Thread1"; thread2.Name = "Thread2"; thread1.Start(); thread2.Start(); //假設在做一些事情 Thread.Sleep(1000); Console.WriteLine("需要主線程幫忙了");
// throw new NullReferenceException("error!"); thread1.Resume(); thread2.Resume(); static void TestSuspend() { Console.WriteLine("Thread:{0} has been suspend!",Thread.CurrentThread.Name); //這里講當前線程掛起 Thread.CurrentThread.Suspend(); Console.WriteLine("{0} has been resume", Thread.CurrentThread.Name); }
如上代碼,我們制造兩個線程來實現Suspend和Resume的測試,(暫時不考慮臨界區共享同步的問題),TestSuspend方法便是兩個線程的共用方法,
方法中我們獲取當前運行該方法的線程,然后將其掛起操作,那么假設線程1先掛起了,線程1被中止當前的工作,面壁思過去了,可是這並不影響線程
2的工作,於是線程2也急匆匆的闖了進來,結果和線程1一樣的悲劇,聰明的你肯定會問,誰能讓線程1和線程2恢復工作?其實有很多方法能讓他們恢
復工作,但是個人認為,在不創建新線程的條件下,被我們忽視的主線程做不住了,看到自己的兄弟面壁,心里肯定不好受,於是做完他自己的一系列
事情之后,他便去召喚這2個兄弟回來工作了,可是也許會有這種情況,主線程迫於自己的事情太多太雜而甚至報出了異常, 那么完蛋了,這兩個線程永
遠無法繼續干活了,或者直接被回收。。。
這樣這次把他們共享區上鎖,上面部分的代碼保持不變,這樣會發生什么情況呢?
static void TestSuspend() { lock (lockObj) { 。。。。 } }
(由於在TestSuspend方法中加入了鎖,所以每次只允許一個線程工作,大伙不必在本文中深究鎖機制,后續章節會給大家詳細溫故下)
盡然在thread2.resume()方法上報錯了,仔細分析后發現在thread1離開共享區(testSuspend)方法之后剎那間,thread2進來了,與此同時,主線程
跑的太快了,導致thread2被掛起前去喚醒thread2,悲劇就這么發生了,其實修改這個bug很容易,只要判斷下線程的狀態,或者主線程中加一個Thread.Sleep()等等,
但是這種錯誤非常的嚴重,往往在很復雜的業務里讓你發狂,所以微軟決定放棄這兩個方法,將他們歸為過時方法,最后讓大家看下微軟那個深奧的解釋,
相信看完上述例子后大家都能理解這個含義了
1 CurrentThread
獲取到當前線程的對象
2 IsAlive
判斷線程是否處於激活狀態
3 IsBackground
設置該線程是否是后台線程,一旦設置true 的話,該線程就被標示為后台線程
再次強調下后台線程的終止不會導致進程的終止
4 IsThreadPoolThread
只讀屬性標示該線程是否屬於線程池的托管線程,一般我通過線程池創建的線程該屬性都是true
5 Name
獲取到線程的名字,我們可以根據業務或者邏輯來自定義線程的名字
6 Priority
這個屬性表示線程的優先級,我們可以用ThreadPriority這個枚舉來設置這個屬性
ThreadPriority包含有5個優先級大家了解下就行
在WPF中實現多線程從一個圖片中截取部分圖片
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Drawing; using System.Windows.Interop; using System.Threading; namespace ImageFlip { /// <summary> /// WPF 多線程將圖片分割 /// </summary> public partial class MainWindow : Window { BitmapSource source; private object lockObj = new object(); public MainWindow() { InitializeComponent(); //首先獲取圖片 Bitmap orginalImage = new Bitmap(@"G:\Picture\Tamriel_4E.png"); //創建線程1 Thread t1 = new Thread(new ParameterizedThreadStart ( obj => { //WPF中使用多線程的話最后一定要返回UI線程,否則操作界面控件時會報錯 //BeginInvoke方法便是返回UI線程的方法 this.Dispatcher.BeginInvoke((Action)(() => { //通過Parameter類的屬性裁剪圖片 ClipImageAndBind(obj); //圖片的部分綁定到頁面控件 this.TestImage1.Source = source; })); } )); //創建線程2 Thread t2 = new Thread(new ParameterizedThreadStart ( obj => { //WPF中使用多線程的話最后一定要返回UI線程,否則操作界面控件時會報錯 //BeginInvoke方法便是返回UI線程的方法 this.Dispatcher.BeginInvoke((Action)(() => { //通過Parameter類的屬性裁剪圖片 ClipImageAndBind(obj); //圖片的部分綁定到頁面控件 this.TestImage2.Source = source; //嘗試將線程1的啟動邏輯放在線程2所持有的方法中 // t1.Start(new Parameter { OrginalImage = orginalImage, ClipHeight = 500, ClipWidth = 500, StartX = 0, StartY = 0 }); })); } )); t2.Start(new Parameter { OrginalImage = orginalImage, ClipHeight = 500, ClipWidth = 500, StartX = orginalImage.Width - 500, StartY = orginalImage.Height - 500 }); //嘗試下注釋掉t2.join方法后是什么情況,其實注釋掉之后,兩個線程會一起工作, //去掉注釋后,界面一直到兩個圖片部分都綁定完成后才出現 //t2.Join(); t1.Start(new Parameter { OrginalImage = orginalImage, ClipHeight = 500, ClipWidth = 500, StartX = 0, StartY = 0 }); } /// <summary> /// 根據參數類進行剪裁圖片,加鎖防止共享資源被破壞 /// </summary> /// <param name="para">Parameter類對象</param> private void ClipImageAndBind(object para) { lock (lockObj) { Parameter paraObject = (para as Parameter); source = this.ClipPartOfImage(paraObject); Thread.Sleep(5000); } } /// <summary> /// 具體裁剪圖片,大家不必在意這個方法,關鍵是線程的使用 /// </summary> /// <param name="para">Parameter</param> /// <returns>部分圖片</returns> private BitmapSource ClipPartOfImage(Parameter para) { if (para == null) { throw new NullReferenceException("para 不能為空"); } if (para.OrginalImage == null) { throw new NullReferenceException("OrginalImage 不能為空"); } System.Drawing.Rectangle rect = new System.Drawing.Rectangle(para.StartX, para.StartY, para.ClipWidth, para.ClipHeight); var bitmap2 = para.OrginalImage.Clone(rect, para.OrginalImage.PixelFormat) as Bitmap; return ChangeBitmapToBitmapSource(bitmap2); } private BitmapSource ChangeBitmapToBitmapSource(Bitmap bmp) { BitmapSource returnSource; try { returnSource = Imaging.CreateBitmapSourceFromHBitmap(bmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } catch { returnSource = null; } return returnSource; } } /// <summary> /// 參數類 /// </summary> public class Parameter { public Bitmap OrginalImage { get; set; } public int StartX { get; set; } public int StartY { get; set; } public int ClipWidth { get; set; } public int ClipHeight { get; set; } } }
前台界面:
<Window x:Class="ImageFlip.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Image x:Name="TestImage1" Grid.Column="0"></Image> <Image x:Name="TestImage2" Grid.Column="1"></Image> </Grid> </Window>
本章介紹了線程一些簡單的基礎知識和對Thread類進行了詳細的介紹,在以后的章節中我會逐步向大家介紹線程同步,異步線程等等有關線程的知識,
文中估計會有錯誤的地方也請大家海涵並且幫助指出,馬上歐錦賽荷蘭的比賽開始了,祝大家多多鼓勵和關注!