C# 溫故而知新: 線程篇(一)


c# 溫故而知新: 線程篇(一)

Thread

目錄:

 

 

 

1 線程基礎的簡單介紹

 

 

首先讓我們翻開書本來了解下線程的一些基礎知識:

1 線程有時被稱為輕量級進程,程序執行流的最小單元

2 線程時由線程ID,當前指令指針(PC),寄存器集合堆棧組成。

3 線程自身不能擁有系統資源,但是可以使用線程所屬進程所占有的系統資源

4 線程可以創建和撤銷另一個線程

5 線程可以擁有自身的狀態,例如 運行狀態,掛起狀態,銷毀釋放狀態等等

6 線程具有優先級,每個線程都分配了0-31 級別的其中一個優先級,數字越大,優先級越高,然而手動分配優先級過於復雜,

所以微軟為我們的Thread類提供一個優先級的枚舉,ThreadPriority枚舉便是優先級枚舉,我們可以利用thread.Priority屬性來進行設置

7 線程開銷,這個是個復雜的話題,希望有機會的話能夠單獨寫一遍文章解釋下

 

 

 

 

 

 

 

 

   

 

那么多線程有什么實際好處呢?

首先讓我們了解下多線程的概念:個程序或者進程中同時運行多個線程完成不同的工作

從概念中我們便可知道多線程的優點了

1 能夠實現並行操作,也就是說多個線程可以同時進行工作

2 利用多線程后許多復雜的業務或者是計算可以交給后台線程去完成,從而提高整體程序的性能

3 類似於第一條利用多線程可以達到異步的作用(注意,實現異步的一種方式是多線程

 

 

 

 

 

當然多線程也有一定的問題需要注意,那就是線程同步問題,關於這個問題我會今后的文章中詳細說明

 

2 線程同步與線程異步的簡單介紹

*1 線程同步

關於線程同步的概念最簡單的理解就是

同步方法調用在程序繼續執行之前,需要等待同步方法執行完畢返回結果

很有可能多個線程都會對一個資源進行訪問,從而導致資源被破壞,所以必須采用線程的同步機制,例如為共享資源加鎖

,當其中一個線程占有了鎖之后,其余線程均不能使用共享資源,只有等其釋放鎖之后,接下來的其中一個線程會占有該

鎖,本系列會從Thread類開始講起,以后多章都會討論線程同步機制,例如鎖機制,臨界區,互斥,信號量 同步事件等待句柄; 等等

 

 

 

 

 

*2 線程異步

線程異步指的是一個調用請求發送給被調用者,而調用者不用等待其結果的返回,一般異步執行的任務都需要比較長的時間,

所以為了不影響主線程的工作,可以使用多線程或者新開辟一個線程來實現異步,同樣,異步和線程池也有着非常緊密的聯系,

這點我會在今后有關線程池的文章中詳細敘述,線程池和異步線程將在第二章中詳細闡述下

 

 

3 前台線程與后台線程的簡單介紹

前台線程:

諸如我們Console程序的主線程,wpf或者sliverlight的 界面線程等等,都屬於前台線程,一旦前台線程奔潰或者終止,相應的后台

線程都會終止本章中通過Thread類產生的線程默認都是前台線程,當然我們可以設置Thread的屬性讓該對象成為后台線程,必須

注意的是,一旦前台線程全部運行完畢,應用程序的進程也會釋放,但是假設Console程序中main函數運行完畢,但是其中幾個前台

線程還處在運行之中,那么這個Console程序的進程是不會釋放的,仍然處於運行之中,直到所有的前台線程都釋放為止      

 

后台線程:

和前台線程唯一的區別是,后台線程更加默默無聞,甚至后台線程因某種情況,釋放銷毀時不會影響到進程,也就是說后台線程釋放時

不會導致進程的釋放

用一個例子再來說明下前后台線程的區別:

有時我們打開outlook 后接受郵件時,程序會失去響應或被卡住,這時候我們去點擊outlook時系統會提示 outlook 失去響應,是否等待或者關閉,

當我們點擊關閉時,其實在程序中關於outlook的所有運行的前台線程被終止,導致了outlook被關閉了其進程也隨之釋放消失。但是,當我們在

outlook中點擊更新郵件時,后台線程會去收取郵件的工作,我們可以在此期間關閉 outlook接受新郵件的后台線程,而不會導致整個outlook的關閉

 

 

 

 

 

 

4 細說下Thread 最為關鍵的構造函數

相信大家再看過前幾章對於線程的介紹后,對線程應該有一個溫故的感覺,那么讓我們開始對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線程,大伙可千萬不要小看基礎啊,往往在復雜的項目中很多

就是因為一些基礎導致,所以一定不要忽視它。。。

 

5 細說下Thread 的 Sleep方法

  話說微軟對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競爭”。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

6 細說下Thread 的 join 方法

為什么我要把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()等等,

但是這種錯誤非常的嚴重,往往在很復雜的業務里讓你發狂,所以微軟決定放棄這兩個方法,將他們歸為過時方法,最后讓大家看下微軟那個深奧的解釋,

相信看完上述例子后大家都能理解這個含義了

 

 

9 簡單了解下Thread 的 一些常用的重要屬性

1 CurrentThread

   獲取到當前線程的對象

2 IsAlive

   判斷線程是否處於激活狀態

3 IsBackground

   設置該線程是否是后台線程,一旦設置true 的話,該線程就被標示為后台線程

再次強調下后台線程的終止不會導致進程的終止

4 IsThreadPoolThread

    只讀屬性標示該線程是否屬於線程池的托管線程,一般我通過線程池創建的線程該屬性都是true

5 Name

    獲取到線程的名字,我們可以根據業務或者邏輯來自定義線程的名字

6 Priority

這個屬性表示線程的優先級,我們可以用ThreadPriority這個枚舉來設置這個屬性

ThreadPriority包含有5個優先級大家了解下就行

 

10  Thread的簡單示例

 在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>

 11 本章總結

 本章介紹了線程一些簡單的基礎知識和對Thread類進行了詳細的介紹,在以后的章節中我會逐步向大家介紹線程同步,異步線程等等有關線程的知識,

文中估計會有錯誤的地方也請大家海涵並且幫助指出,馬上歐錦賽荷蘭的比賽開始了,祝大家多多鼓勵和關注!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM