一文带你搞懂C#多线程的5种写法
一文帶你搞懂C#多線程的5種寫法
1.簡(jiǎn)介
超長(zhǎng)警告!
在學(xué)習(xí)本篇文章前你需要學(xué)習(xí)的相關(guān)知識(shí):
線程基本知識(shí)
此篇文章簡(jiǎn)單總結(jié)了C#中主要的多線程實(shí)現(xiàn)方法,包括:
- Thread
線程 - ThreadPool
線程池 - Parallel
- Task
任務(wù) - BackgroundWorker組件
2. Thread類
2.1 概述
-
使用Thread類通過(guò)ThreadStart(無(wú)參數(shù))或ParameterizedThreadStart(一個(gè)輸入?yún)?shù))類型的委托創(chuàng)建一個(gè)Thread對(duì)象,開啟一個(gè)新線程,執(zhí)行該委托傳遞的任務(wù),此時(shí)線程尚未處于運(yùn)行狀態(tài)。
-
調(diào)用Start()函數(shù)啟動(dòng)線程,當(dāng)前線程繼續(xù)執(zhí)行。
-
調(diào)用Join()函數(shù)可以阻塞當(dāng)前線程,直到調(diào)用Join()的線程終止。
-
調(diào)用Abort()方法,如需中止線程,在調(diào)用該方法的線程上拋出ThreadAbortException異常,以結(jié)束該線程
-
可以通過(guò)Thread.ResetAbort()方法阻止線程的中止。
2.1 屬性表
| Name | 屬性,獲取或設(shè)置線程的名稱 |
| Priority | 屬性,獲取或設(shè)置線程的優(yōu)先級(jí) |
| ThreadState | 屬性,獲取線程當(dāng)前的狀態(tài) |
| IsAlive | 屬性,獲取當(dāng)前線程是否處于啟動(dòng)狀態(tài) |
| IsBackground | 屬性,獲取或設(shè)置值,表示該線程是否為后臺(tái)線程 |
| CurrentThread | 屬性,獲取當(dāng)前正在運(yùn)行的線程 |
2.2 方法表
| Start() | 方法,啟動(dòng)線程 |
| Sleep(int millisecondsTimout) | 方法,將當(dāng)前線程暫停指定的毫秒數(shù) |
| Suspend() | 方法,掛起當(dāng)前線程(已經(jīng)被棄用) |
| Join() | 方法,阻塞調(diào)用線程,直到某個(gè)線程終止為止 |
| Interrupt() | 方法,中斷當(dāng)前線程 |
| Resume() | 方法,繼續(xù)已經(jīng)掛起的線程(已經(jīng)被棄用) |
| Abort() | 方法,終止線程(已經(jīng)被棄用) |
2.3 開啟線程
首先用new申請(qǐng)Thread對(duì)象,然后對(duì)象調(diào)用Start()方法啟用線程。
代碼如下所示:
class Program {static void DownLoad(){Console.WriteLine("DownLoad Begin " + Thread.CurrentThread.ManagedThreadId);Thread.Sleep(1000);Console.WriteLine("DownLoad End");}static void Main(string[] args){//創(chuàng)建Thread對(duì)象Thread thread = new Thread(DownLoad);//啟動(dòng)線程thread.Start();Console.WriteLine("Main");Console.ReadKey();} }Thread.CurrentThread.ManagedThreadId獲取當(dāng)前線程的ID,便于管理。
用Lambda表達(dá)式代替函數(shù)調(diào)用,也能達(dá)到相同的效果
class Program {static void Main(string[] args){Thread thread = new Thread(() =>{Console.WriteLine("DownLoad Begin " + Thread.CurrentThread.ManagedThreadId);Thread.Sleep(1000);Console.WriteLine("DownLoad End");});thread.Start();Console.WriteLine("Main");Console.ReadKey();} }2.4 傳遞參數(shù)
有兩種為線程傳遞參數(shù)的方法:
- Start()函數(shù)傳參法
- 對(duì)象成員方法傳參法
- 匿名方法傳參法
2.4.1 Start()函數(shù)傳參
為某方法創(chuàng)建新線程后,在使用Start()方法啟動(dòng)線程時(shí)傳遞該方法需要的參數(shù)。
代碼如下:
class Program {static void DownLoad(object name){Console.WriteLine("DownLoad Begin " + name);Thread.Sleep(1000);Console.WriteLine("DownLoad End");}static void Main(string[] args){//創(chuàng)建Thread對(duì)象Thread thread = new Thread(DownLoad);//啟動(dòng)線程thread.Start("April");Console.WriteLine("Main");Console.ReadKey();}2.4.1 對(duì)象傳遞
初始化一個(gè)對(duì)象,然后用對(duì)象的方法初始化Thread,這樣該線程就可以使用這個(gè)對(duì)象的所有成員。
class Program {public class Download{private int Id;private string Name;public Download(int id, string name){Id = id;Name = name;}public void DownloadFile(){Console.WriteLine("DownLoad Begin " + "ID: " + Id + " Name: " + Name);Thread.Sleep(1000);Console.WriteLine("DownLoad End"); }}static void Main(string[] args){Download download = new Download(1, "人民日?qǐng)?bào)");Thread thread = new Thread(download.DownloadFile);thread.Start();Console.WriteLine("Main");Console.ReadKey();} }2.4.5 匿名方法
需要接收多個(gè)參數(shù)的解決方案是使用一個(gè)匿名方法調(diào)用,方法如下
static void Main() { Thread t = new Thread(delegate() { WriteText ("Hello"); });t.Start();}static void WriteText (stringtext) { Console.WriteLine (text); }它的優(yōu)點(diǎn)是目標(biāo)方法(這里是WriteText),可以接收任意數(shù)量的參數(shù),并且沒(méi)有裝箱操作。
不過(guò)這需要將一個(gè)外部變量放入到匿名方法中,如下示例:
static voidMain() { stringtext = "Before";Threadt = new Thread(delegate() { WriteText (text); });text = "After";t.Start();}static void WriteText (stringtext) { Console.WriteLine (text); }需要注意的是:
當(dāng)外部變量的值被修改,匿名方法可能進(jìn)行無(wú)意的互動(dòng),導(dǎo)致一些古怪的現(xiàn)象。
一旦線程開始運(yùn)行,外部變量最好被處理成只讀的——除非有人愿意使用適當(dāng)?shù)逆i。
2.5 線程命名
線程可以通過(guò)它的Name屬性進(jìn)行命名,這非常有利于調(diào)試:
可以用Console.WriteLine打印出線程的名字
Microsoft Visual Studio可以將線程的名字顯示在調(diào)試工具欄的位置上。
線程的名字可以在被任何時(shí)間設(shè)置——但只能設(shè)置一次,重命名會(huì)引發(fā)異常。
程序的主線程也可以被命名,下面例子里主線程通過(guò)CurrentThread命名:
Class ThreadNaming { static void Main() { Thread.CurrentThread.Name= "main";Thread worker = new Thread(Go);worker.Name= "worker";worker.Start();Go();}static void Go() { Console.WriteLine ("Hello from "+ Thread.CurrentThread.Name);}}輸出
Hellofrom main
Hellofrom worker
2.6 前臺(tái)線程和后臺(tái)線程
- 前臺(tái)線程(用戶界面線程)
只要存在有一個(gè)前臺(tái)線程在運(yùn)行,應(yīng)用程序就在運(yùn)行
通常用來(lái)處理用戶的輸入并響應(yīng)各種事件和消息 - 后臺(tái)線程(工作線程)
應(yīng)用程序關(guān)閉時(shí),如果后臺(tái)線程沒(méi)有執(zhí)行完,會(huì)被強(qiáng)制性的關(guān)閉
用來(lái)執(zhí)行程序的后臺(tái)處理任務(wù),比如計(jì)算、調(diào)度、對(duì)串口的讀寫操作等
例如:
class Program {static void DownLoad(){Console.WriteLine("DownLoad Begin " + Thread.CurrentThread.ManagedThreadId);Thread.Sleep(1000);Console.WriteLine("DownLoad End");}static void Main(string[] args){//創(chuàng)建Thread對(duì)象Thread thread = new Thread(DownLoad);//設(shè)為后臺(tái)線程thread.IsBackground = true;//啟動(dòng)線程thread.Start();Console.WriteLine("Main");} }在上例中,thread被設(shè)置為后臺(tái)線程。
Main執(zhí)行完后,沒(méi)有前臺(tái)線程了,應(yīng)用程序就結(jié)束,雖然后臺(tái)線程thread此時(shí)尚未執(zhí)行完,也被終止。
改變線程從前臺(tái)到后臺(tái)不會(huì)以任何方式改變它在CPU協(xié)調(diào)程序中的優(yōu)先級(jí)和狀態(tài)。
擁有一個(gè)后臺(tái)工作線程是有益的,
最直接的理由是當(dāng)提到結(jié)束程序它總是可能有最后的發(fā)言權(quán)。
交織以不會(huì)消亡的前臺(tái)線程,保證程序的正常退出。
拋棄一個(gè)前臺(tái)工作線程是尤為險(xiǎn)惡的,尤其對(duì)Windows Forms程序,
因?yàn)槌绦蛑钡街骶€程結(jié)束時(shí)才退出(至少對(duì)用戶來(lái)說(shuō)),但是它的進(jìn)程仍然運(yùn)行著。
在Windows任務(wù)管理器它將從應(yīng)用程序欄消失不見,但卻可以在進(jìn)程欄找到它。
除非用戶找到并結(jié)束它,它將繼續(xù)消耗資源,并可能阻止一個(gè)新的實(shí)例的運(yùn)行從開始或影響它的特性。
對(duì)于程序失敗退出的普遍原因就是存在“被忘記”的前臺(tái)線程。
| 前臺(tái)線程 | 主程序關(guān)閉 | 否 | 顯示關(guān)閉線程/殺掉當(dāng)前進(jìn)程 |
| 后臺(tái)線程 | 主程序關(guān)閉 | 是 | 無(wú) |
2.7 注意事項(xiàng)
-
Thread類創(chuàng)建的線程默認(rèn)為前臺(tái)線程,可以通過(guò)IsBackground屬性設(shè)置其為前臺(tái)或后臺(tái)線程。
用Thread類創(chuàng)建的線程是前臺(tái)線程,線程池中的線程總是后臺(tái)線程
-
可以通過(guò)Priority屬性設(shè)置線程的優(yōu)先級(jí)。
-
線程內(nèi)部可以通過(guò)try catch捕獲該異常,在catch模塊中進(jìn)行一些必要的處理
如釋放持有的鎖和文件資源等 -
慎重使用Abort()方法
如果在當(dāng)前線程中拋出該異常,其結(jié)果是可預(yù)測(cè)的
但是對(duì)于其他線程,它會(huì)中斷任何正在執(zhí)行的代碼,有可能中斷靜態(tài)對(duì)象的生成,造成不可預(yù)測(cè)的結(jié)果。
3. 線程池
3.1 概述
ThreadPool類維護(hù)一個(gè)線程的列表,提供給用戶以執(zhí)行不同的小任務(wù),減少頻繁創(chuàng)建線程的開銷。
該線程池可用于執(zhí)行任務(wù)、發(fā)送工作項(xiàng)、處理異步 I/O、代表其他線程等待以及處理計(jì)時(shí)器。
線程池其實(shí)就是一個(gè)存放線程對(duì)象的“池子(pool)”,他提供了一些基本方法,如:設(shè)置pool中最小/最大線程數(shù)量、把要執(zhí)行的方法排入隊(duì)列等等。ThreadPool是一個(gè)靜態(tài)類,因此可以直接使用,不用創(chuàng)建對(duì)象。
3.2 線程池的優(yōu)點(diǎn)
每新建一個(gè)線程都需要占用內(nèi)存空間和其他資源
而新建了那么多線程,有很多在休眠,或者在等待資源釋放;
又有許多線程只是周期性的做一些小工作,如刷新數(shù)據(jù)等等,太浪費(fèi)了,劃不來(lái)。
實(shí)際編程中大量線程突發(fā),然后在短時(shí)間內(nèi)結(jié)束的情況很少見。
于是,就提出了線程池的概念。
線程池中的線程執(zhí)行完指定的方法后并不會(huì)自動(dòng)消除,而是以掛起狀態(tài)返回線程池,如果應(yīng)用程序再次向線程池發(fā)出請(qǐng)求,那么處以掛起狀態(tài)的線程就會(huì)被激活并執(zhí)行任務(wù),而不會(huì)創(chuàng)建新線程,這就節(jié)約了很多開銷。
只有當(dāng)線程數(shù)達(dá)到最大線程數(shù)量,系統(tǒng)才會(huì)自動(dòng)銷毀線程。
因此,使用線程池可以避免大量的創(chuàng)建和銷毀的開支,具有更好的性能和穩(wěn)定性,其次,開發(fā)人員把線程交給系統(tǒng)管理,可以集中精力處理其他任務(wù)。
3.3 線程池的使用
-
設(shè)置線程池最大最小:
ThreadPool.SetMaxThreads (int workerThreads,int completionPortThreads)
設(shè)置可以同時(shí)處于活動(dòng)狀態(tài)的線程池的請(qǐng)求數(shù)目。
所有大于此數(shù)目的請(qǐng)求將保持排隊(duì)狀態(tài),直到線程池線程變?yōu)榭捎谩?br /> 還可以設(shè)置最小線程數(shù)。 -
將任務(wù)添加進(jìn)線程池:
**ThreadPool.QueueUserWorkItem(new WaitCallback(方法名));**或
ThreadPool.QueueUserWorkItem(new WaitCallback(方法名), 參數(shù));
但是線程池的使用也有一些限制:
- 線程池中的線程均為后臺(tái)線程,并且不能修改為前臺(tái)線程
- 不能給入池的線程設(shè)置優(yōu)先級(jí)或名稱
- 對(duì)于COM對(duì)象,入池的所有線程都是多線程單元(MTA)線程,許多COM對(duì)象都需要單線程單元(STA) 線程
- 入池的線程只適合時(shí)間較短的任務(wù),如果線程需要長(zhǎng)時(shí)間運(yùn)行,應(yīng)使用Thread類創(chuàng)建線程或使用Task的LongRunning選項(xiàng)
- .Net下線程池最小默認(rèn)允許4個(gè)工作線程,最大允許2048個(gè)工作線程。
并發(fā)線程啟動(dòng)后,瞬間會(huì)啟動(dòng)4個(gè)線程。
而剩下的會(huì)依據(jù)環(huán)境每0.5秒或者1秒啟動(dòng)一個(gè)。
如果同時(shí)運(yùn)行的線程達(dá)到Max工作線程,那么剩下的就會(huì)掛起
直到線程池中的線程有空閑得了,才會(huì)去執(zhí)行。
4. Parallel類
4.1 概述
整理自https://blog.csdn.net/honantic/article/details/46876871
Parallel和Task類都位于System.Threading.Task命名空間中,是對(duì)Thread和ThreadPool類更高級(jí)的抽象。
Parrallel類有For()、ForEach()、Invoke()三個(gè)方法
-
Invoke()
實(shí)現(xiàn)任務(wù)并行性
允許同時(shí)調(diào)用不同的方法, -
Parallel.For()和 Parallel.ForEach()
實(shí)現(xiàn)數(shù)據(jù)并行性
在每次迭代中調(diào)用相同的代碼
4.2 常用方法
4.2.1 Parallel.For()
Parallel.For()方法類似于 C#的 for循環(huán)語(yǔ)旬,也是多次執(zhí)行一個(gè)任務(wù)。
使用Parallel.For()方法,可以并行運(yùn)行迭代。
迭代的順序沒(méi)有定義,不能保證。
在For()方法中:
- 前兩個(gè)參數(shù)定義了循環(huán)的開頭和結(jié)束。示例從0迭代到 9。
- 第 3個(gè)參數(shù)是一個(gè)Action委托
是要并行運(yùn)行迭代的方法 - 整數(shù)參數(shù)是循環(huán)的迭代次數(shù),該參數(shù)被傳遞給Action委托引用的方法。
- Parallel.For()方法的返回類型是ParalleLoopResult結(jié)構(gòu),它提供了循環(huán)是否結(jié)束的信息。
案例如下:
public static void Main(){ParallelLoopResult result = Parallel.For(0, 10, i =>{Console.WriteLine("i:{0}, thread id: {1}", i, Thread.CurrentThread.ManagedThreadId);Thread.Sleep(10);});Console.WriteLine("Is completed: {0}", result.IsCompleted);//i: 0, thread id: 9//i: 2, thread id: 10//i: 1, thread id: 9//i: 3, thread id: 10//i: 4, thread id: 9//i: 6, thread id: 11//i: 7, thread id: 10//i: 5, thread id: 9//i: 8, thread id: 12//i: 9, thread id: 11//Is completed: TrueConsole.ReadKey();}同F(xiàn)or()循環(huán)類似,Parallel.For()方法也可以中斷循環(huán)的執(zhí)行。
Parallel.For()方法的一個(gè)重載版本接受第3個(gè)Action<int, ParallelLoopState>類型的參數(shù)。
使用這些參數(shù)定義一個(gè)方法,就可以調(diào)用ParalleLoopState的Break()或Stop()方法,以影響循環(huán)的結(jié)果。
注意,迭代的順序沒(méi)有定義
案例如下:
public static void Main(){ParallelLoopResult result = Parallel.For(0, 100, (i, state) =>{Console.WriteLine("i:{0}, thread id: {1}", i, Thread.CurrentThread.ManagedThreadId);if (i > 10)state.Break();Thread.Sleep(10);});Console.WriteLine("Is completed: {0}", result.IsCompleted);Console.WriteLine("Lowest break iteration: {0}", result.LowestBreakIteration);//i: 0, thread id: 10//i: 25, thread id: 6//i: 1, thread id: 10//i: 2, thread id: 10//i: 3, thread id: 10//i: 4, thread id: 10//i: 5, thread id: 10//i: 6, thread id: 10//i: 7, thread id: 10//i: 8, thread id: 10//i: 9, thread id: 10//i: 10, thread id: 10//i: 11, thread id: 10//Is completed: False//Lowest break iteration: 11Console.ReadKey();}4.2.2 Parallel.For < TLocal >
Parallel.For()方法可能使用幾個(gè)線程來(lái)執(zhí)行循環(huán) 。
如果需要對(duì)每個(gè)線程進(jìn)行初始化,就可以使用Parallel.For方法。
除了from和to對(duì)應(yīng)的值之外,For()方法的泛型版本還接受3個(gè)委托參數(shù):
-
第一個(gè)參數(shù)的類型是Func< TLocal >
因?yàn)檫@里的例子對(duì)于TLocal使用字符串,所以該方法需要定義為Func< string >,即返回string的方法。
這個(gè)方法僅對(duì)于用于執(zhí)行迭代的每個(gè)線程調(diào)用一次 -
第二個(gè)委托參數(shù)為循環(huán)體定義了委托
在示例中,該參數(shù)的類型是Func<int, ParallelLoopState, string, string>。
其中第一個(gè)參數(shù)是循環(huán)迭代,第二個(gè)參數(shù) ParallelLoopstate允許停止循環(huán),如前所述 。
循環(huán)體方法通過(guò)第3個(gè)參數(shù)接收從init方法返回的值,循環(huán)體方法還需要返回一個(gè)值,其類型是用泛型for參數(shù)定義的。 -
For()方法的最后一個(gè)參數(shù)指定一個(gè)委托Action< TLocal >;在該示例中,接收一個(gè)字符串。
這個(gè)方法僅對(duì)于每個(gè)線程調(diào)用一次,這是一個(gè)線程退出方法。
案例如下:
Parallel.For<string>(0, 20,() =>{Console.WriteLine("init thread {0},\t task {1}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId);return string.Format("t{0}", Thread.CurrentThread.ManagedThreadId);},(i, pls, str) =>{Console.WriteLine("body i {0} \t str {1} \t thread {2} \t task {3}", i, str, Thread.CurrentThread.ManagedThreadId, Task.CurrentId);Thread.Sleep(10);return string.Format("i \t{0}", i);},(str) =>{Console.WriteLine("finally\t {0}", str);});Console.ReadKey();Parallel.For 方法 (Int32, Int32, Func, Func<Int32, ParallelLoopState, TLocal, TLocal>, Action)
參數(shù)表:
| TLoca | 線程本地?cái)?shù)據(jù)的類型 | |
| fromInclusive | System.Int32 | 開始索引(含) |
| toExclusive | System.Int32 | 結(jié)束索引(不含) |
| localInit | System.Func | 用于返回每個(gè)任務(wù)的本地?cái)?shù)據(jù)的初始狀態(tài)的函數(shù)委托 |
| body | System.Func<Int32, ParallelLoopState, TLocal, TLocal> | 將為每個(gè)迭代調(diào)用一次的委托 |
| localFinally | System.Action | 用于對(duì)每個(gè)任務(wù)的本地狀態(tài)執(zhí)行一個(gè)最終操作的委托 |
| 返回值 | System.Threading.Tasks.ParallelLoopResult |
在迭代范圍 (fromInclusive,toExclusive) ,為每個(gè)值調(diào)用一次body 委托。
為它提供以下參數(shù):
- 迭代次數(shù) (Int32)
- 可用來(lái)提前退出循環(huán)的ParallelLoopState實(shí)例
- 可以在同一線程上執(zhí)行的迭代之間共享的某些本地狀態(tài)。
對(duì)于參與循環(huán)執(zhí)行的每個(gè)任務(wù)調(diào)用 localInit 委托一次,并返回每個(gè)任務(wù)的初始本地狀態(tài)。
這些初始狀態(tài)傳遞給第一個(gè)在該任務(wù)上 調(diào)用的 body。
然后,每個(gè)后續(xù)正文調(diào)用返回可能修改過(guò)的狀態(tài)值,傳遞到下一個(gè)正文調(diào)用。
最后,每個(gè)任務(wù)上的最后正文調(diào)用返回傳遞給 localFinally 委托的狀態(tài)值。
每個(gè)任務(wù)調(diào)用 localFinally 委托一次,以對(duì)每個(gè)任務(wù)的本地狀態(tài)執(zhí)行最終操作。
此委托可以被多個(gè)任務(wù)同步調(diào)用;
因此您必須同步對(duì)任何共享變量的訪問(wèn)。
Parallel.For方法比在它執(zhí)行生存期的線程可能使用更多任務(wù),作為現(xiàn)有的任務(wù)完成并被新任務(wù)替換。
這使基礎(chǔ) TaskScheduler 對(duì)象有機(jī)會(huì)添加、更改或移除服務(wù)循環(huán)的線程。
如果 fromInclusive 大于或等于 toExclusive,則該方法立即返回,而無(wú)需執(zhí)行任何迭代。
4.2.3 Parallel.ForEach()
Parallel.ForEach()方法遍歷實(shí)現(xiàn)了IEnumerable的集合,其方式類似于foreach語(yǔ)句,但以異步方式遍歷。
這里也沒(méi)有確定遍歷順序。
中斷循環(huán)
如果需要中斷循環(huán),就可以使用ForEach()方法的重載版本和ParallelLoopState參數(shù)。其方式與前面的For()方法相同。
ForEach()方法的一個(gè)重載版本也可以用于訪問(wèn)索引器,從而獲得迭代次數(shù)
如下所示:
4.2.4 Parallel.Invoke()
如果多個(gè)任務(wù)應(yīng)并行運(yùn)行,就可以使用Parallel.Invoke()方法。
Parallel.Invoke()方法允許傳遞一個(gè)Action委托數(shù)組,在其中可以指定應(yīng)運(yùn)行的方法。
示例代碼傳遞了要并行調(diào)用的Foo()和Bar()方法:
如需同時(shí)執(zhí)行多個(gè)不同的任務(wù),可以使用Parallel.Invoke()方法,它允許傳遞一個(gè)Action委托數(shù)組。
public static void Main(){Parallel.Invoke(Func1, Func2, Func3);Console.ReadKey();}5. Task類
5.1 概述
相比于Thread類,Task類為控制線程提供了更大的靈活性。
-
Task類可以獲取線程的返回值
-
可以定義連續(xù)的任務(wù):在一個(gè)任務(wù)結(jié)束結(jié)束后開啟下一個(gè)任務(wù)
-
可以在層次結(jié)構(gòu)中安排任務(wù),在父任務(wù)中可以創(chuàng)建子任務(wù)
這樣就創(chuàng)建了一種依賴關(guān)系,如果父任務(wù)被取消,子任務(wù)也隨之取消
注意:
Task類默認(rèn)使用線程池中的線程,如果該任務(wù)需長(zhǎng)期運(yùn)行,應(yīng)使用TaskCreationOptions.LongRunning屬性告訴任務(wù)管理器創(chuàng)建一個(gè)新的線程,而不是使用線程池中的線程。
5.2 任務(wù)Task和線程Thread的區(qū)別:
- 任務(wù)是架構(gòu)在線程之上的
也就是說(shuō)任務(wù)最終還是要拋給線程去執(zhí)行。 - 任務(wù)跟線程不是一對(duì)一的關(guān)系
比如開10個(gè)任務(wù)并不是說(shuō)會(huì)開10個(gè)線程,這一點(diǎn)任務(wù)有點(diǎn)類似線程池,但是任務(wù)相比線程池有很小的開銷和精確的控制。 - Task和Thread一樣,位于System.Threading命名空間下!
5.3 Task的生存周期與狀態(tài)
| Created | 表示默認(rèn)初始化任務(wù),但是“工廠創(chuàng)建的”實(shí)例直接跳過(guò)。 |
| WaitingToRun | 這種狀態(tài)表示等待任務(wù)調(diào)度器分配線程給任務(wù)執(zhí)行。 |
| RanToCompletion | 任務(wù)執(zhí)行完畢。 |
5.4 Task的使用方法
5.4.1 啟動(dòng)任務(wù)
以下程序演示了幾種通過(guò)Task類啟動(dòng)任務(wù)的方式:
-
實(shí)例化后手動(dòng)start()
var task1 = new Task(() =>{//TODO you code});task1.Start(); -
使用Task工廠對(duì)象創(chuàng)建新任務(wù)并執(zhí)行
TaskFactory tf = new TaskFactory(); Task t1 = tf.StartNew(TaskMethod.DoTask, "using a task factory"); -
工廠創(chuàng)建,直接執(zhí)行
Task t2 = Task.Factory.StartNew(TaskMethod.DoTask, "factory via a task");
案例如下:
public class ThreadExample{public static void Main(){TaskFactory tf = new TaskFactory();Task t1 = tf.StartNew(TaskMethod.DoTask, "using a task factory");Task t2 = Task.Factory.StartNew(TaskMethod.DoTask, "factory via a task");Task t3 = new Task(TaskMethod.DoTask, "using a task constructor and start");t3.Start();//需要.NetFramework 4.5以上var t4 = Task.Run(() => TaskMethod.DoTask("using Run method"));Console.ReadKey();}class TaskMethod{static object taskLock = new object();public static void DoTask(object msg){lock (taskLock){Console.WriteLine(msg);Console.WriteLine("Task id:{0}, Thread id :{1}",Task.CurrentId == null ? "no task" : Task.CurrentId.ToString(),Thread.CurrentThread.ManagedThreadId);}}}5.4.2 任務(wù)控制
5.4.2.1 Task.Wait()
就是等待任務(wù)執(zhí)行(task1)完成,task1的狀態(tài)變?yōu)镃ompleted。
5.4.2.2 Task.WaitAll()
等待所有的任務(wù)都執(zhí)行完成:
例如:
即當(dāng)task,task2,task3…N全部任務(wù)都執(zhí)行完成之后才會(huì)往下執(zhí)行代碼(打印出:“All task finished!”)
5.4.2.3 Task.WaitAny()
同Task.WaitAll,等待任何一個(gè)任務(wù)完成就繼續(xù)向下執(zhí)行,將上面的代碼WaitAll替換為WaitAny
Task.WaitAny(task,task2,task3...N) Console.WriteLine("Any task finished!");即當(dāng)task,task2,task3…N任意一個(gè)任務(wù)都執(zhí)行完成之后就會(huì)往下執(zhí)行代碼(打印出:” Any task finished!”)
5.4.2.4 Task.ContinueWith()
在第一個(gè)Task完成后自動(dòng)啟動(dòng)下一個(gè)Task,實(shí)現(xiàn)Task的延續(xù),編寫如下代碼:
public static void Main(){TaskFactory tf = new TaskFactory();Task t1 = tf.StartNew(()=>{Console.WriteLine("Current Task id = {0}", Task.CurrentId);Console.WriteLine("執(zhí)行任務(wù)1\r\n");Thread.Sleep(10);});Task t2 = t1.ContinueWith((t) =>{Console.WriteLine("Last Task id = {0}", t.Id);Console.WriteLine("Current Task id = {0}", Task.CurrentId);Console.WriteLine("執(zhí)行任務(wù)2\r\n");Thread.Sleep(10);});Task t3 = t2.ContinueWith(delegate(Task t) {Console.WriteLine("Last Task id = {0}", t.Id);Console.WriteLine("Current Task id = {0}", Task.CurrentId);Console.WriteLine("執(zhí)行任務(wù)3\r\n");}, TaskContinuationOptions.OnlyOnRanToCompletion);Console.ReadKey(); }//執(zhí)行結(jié)果////Current Task id = 1//執(zhí)行任務(wù)1//Last Task id = 1//Current Task id = 2//執(zhí)行任務(wù)2//Last Task id = 2//Current Task id = 3//執(zhí)行任務(wù)3從執(zhí)行結(jié)果可以看出,任務(wù)1,2,3被順序執(zhí)行,同時(shí)通過(guò) TaskContinuationOptions 還可以指定何種情況下繼續(xù)執(zhí)行該任務(wù),常用的值包括OnlyOnFaulted, OnlyOnCanceled, NotOnFaulted, NotOnCanceled等。如將上例中的OnlyOnRanToCompletion改為OnlyOnFaulted,任務(wù)2結(jié)束之后,任務(wù)3將不被執(zhí)行。
對(duì)于ContinueWith()的使用,MSDN演示了更加優(yōu)雅的“流式”調(diào)用方法:
private void Button1_Click(object sender, EventArgs e) { var backgroundScheduler = TaskScheduler.Default; var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Task.Factory.StartNew(delegate { DoBackgroundComputation(); }, backgroundScheduler). ContinueWith(delegate { UpdateUI(); }, uiScheduler). ContinueWith(delegate { DoAnotherBackgroundComputation(); }, backgroundScheduler). ContinueWith(delegate { UpdateUIAgain(); }, uiScheduler); }5.4.2.5 RunSynchronously()
用于實(shí)現(xiàn)同步調(diào)用,直接在當(dāng)前線程上調(diào)用該任務(wù)。
public static void Main(){TaskMethod.DoTask("Just Main thread");Task t1 = new Task(TaskMethod.DoTask, "using Run Sync");t1.RunSynchronously();//輸出結(jié)果//Just Main thread//Task id: no task, Thread id: 9////using Run Sync//Task id:1, Thread id :9}5.4.3 任務(wù)取消
當(dāng)我們啟動(dòng)了一個(gè)task,出現(xiàn)異常或者用戶點(diǎn)擊取消等等,我們可以取消這個(gè)任務(wù)。
我們通過(guò)cancellation的tokens來(lái)取消一個(gè)Task。
在很多Task的Body里面包含循環(huán),我們可以在輪詢的時(shí)候判斷IsCancellationRequested屬性是否為True
如果是True的話就return或者拋出異常,拋出異常后面再說(shuō),因?yàn)檫€沒(méi)有說(shuō)異常處理的東西。
下面在代碼中看下如何實(shí)現(xiàn)任務(wù)的取消,代碼如下:
var tokenSource = new CancellationTokenSource();var token = tokenSource.Token;var task = Task.Factory.StartNew(() =>{for (var i = 0; i < 1000; i++){System.Threading.Thread.Sleep(1000);if (token.IsCancellationRequested){Console.WriteLine("Abort mission success!");return;}}}, token);token.Register(() =>{Console.WriteLine("Canceled");});Console.WriteLine("Press enter to cancel task...");Console.ReadKey();tokenSource.Cancel();123456789101112131415161718192021這里開啟了一個(gè)Task,并給token注冊(cè)了一個(gè)方法,輸出一條信息,然后執(zhí)行ReadKey開始等待用戶輸入,用戶點(diǎn)擊回車后,執(zhí)行tokenSource.Cancel方法,取消任務(wù)。
注意:
因?yàn)槿蝿?wù)通常運(yùn)行以異步方式在線程池線程上,創(chuàng)建并啟動(dòng)任務(wù)的線程將繼續(xù)執(zhí)行,一旦該任務(wù)已實(shí)例化。
在某些情況下,當(dāng)調(diào)用線程的主應(yīng)用程序線程,該應(yīng)用程序可能會(huì)終止之前任何任務(wù)實(shí)際開始執(zhí)行。
其他情況下,應(yīng)用程序的邏輯可能需要調(diào)用線程繼續(xù)執(zhí)行,僅當(dāng)一個(gè)或多個(gè)任務(wù)執(zhí)行完畢。
您可以同步調(diào)用線程的執(zhí)行,以及異步任務(wù)它啟動(dòng)通過(guò)調(diào)用 Wait 方法來(lái)等待要完成的一個(gè)或多個(gè)任務(wù)。
若要等待完成一項(xiàng)任務(wù),可以調(diào)用其 Task.Wait 方法。
調(diào)用 Wait 方法將一直阻塞調(diào)用線程直到單一類實(shí)例都已完成執(zhí)行。
5.4.4 接收任務(wù)的返回值
對(duì)于任務(wù)有返回值的情況,可使用Task泛型類,TResult定義了返回值的類型,以下代碼演示了調(diào)用返回int值的任務(wù)的方法。
public static void Main(){var t5 = new Task<int>(TaskWithResult, Tuple.Create<int, int>(1, 2));t5.Start();t5.Wait();Console.WriteLine("adder results: {0}", t5.Result);Console.ReadKey(); }public static int TaskWithResult(object o){Tuple<int, int> adder = (Tuple<int, int>)o;return adder.Item1 + adder.Item2;}5.5 任務(wù)的層次結(jié)構(gòu)
如果在一個(gè)Task內(nèi)部創(chuàng)建了另一個(gè)任務(wù),這兩者間就存在父/子的層次結(jié)構(gòu),當(dāng)父任務(wù)被取消時(shí),子任務(wù)也會(huì)被取消。
如果不希望使用該層次結(jié)構(gòu),可在創(chuàng)建子任務(wù)時(shí)選擇TaskCreationOptions.DetachedFromParent。
6. BackgroundWorker控件
6.1 概述
C#提供了BackgroundWorker控件幫助用戶更簡(jiǎn)單、安全地實(shí)現(xiàn)多線程運(yùn)算。
該控件提供了DoWork, ProgressChanged 和 RunWorkerCompleted事件
為DoWork添加事件處理函數(shù),再調(diào)用RunWorkerAsync()方法,即可創(chuàng)建一個(gè)新的線程執(zhí)行DoWork任務(wù)
ProgressChanged和RunWorkerCompleted事件均在UI線程中執(zhí)行,添加相應(yīng)的處理函數(shù),即可完成任務(wù)線程與UI線程間的交互,可用于顯示任務(wù)的執(zhí)行狀態(tài)(完成百分比)、執(zhí)行結(jié)果等。
同時(shí),該控件還提供了CancleAsync()方法,以中斷線程的執(zhí)行
需注意的是,調(diào)用該方法后,只是將控件的CancellationPending屬性置True,用戶需在程序執(zhí)行過(guò)程中查詢?cè)搶傩砸耘卸ㄊ欠駪?yīng)中斷線程。
具體用法可參考MSDN:BackgroundWorker用法范例
可以看的出來(lái),BackgroundWorker組件提供了一種執(zhí)行異步操作(后臺(tái)線程)的同時(shí),并且還能妥妥的顯示操作進(jìn)度的解決方案。
6.2 屬性表
6.2.1 WorkerReportsProgress
bool類型,指示BackgroundWorker是否可以報(bào)告進(jìn)度更新。
- True時(shí),可以成功調(diào)用ReportProgress方法
- 否則將引發(fā)InvalidOperationException異常
用法:
private BackgroundWorker bgWorker = new BackgroundWorker(); bgWorker.WorkerReportsProgress = true;6.2.2 WorkerSupportsCancellation
bool類型,指示BackgroundWorker是否支持異步取消操作
-
True時(shí),將可以成功調(diào)用CancelAsync方法
-
否則將引發(fā)InvalidOperationException異
用法:
6.2.3 CancellationPending
bool類型,指示應(yīng)用程序是否已請(qǐng)求取消后臺(tái)操作。
此屬性通常放在用戶執(zhí)行的異步操作內(nèi)部,用來(lái)判斷用戶是否取消執(zhí)行異步操作。
當(dāng)執(zhí)行BackgroundWorker.CancelAsync()方法時(shí),該屬性值將變?yōu)門rue。
用法:
6.2.4 IsBusy
bool類型,指示BackgroundWorker是否正在執(zhí)行一個(gè)異步操作。
此屬性通常放在BackgroundWorker.RunWorkerAsync()方法之前,避免多次調(diào)用RunWorkerAsync()方法引發(fā)異常。
當(dāng)執(zhí)行BackgroundWorker.RunWorkerAsync()方法是,該屬性值將變?yōu)門rue。
6.3 方法表
6.3.1 RunWorkerAsync()
開始執(zhí)行一個(gè)后臺(tái)操作。
調(diào)用該方法后,將觸發(fā)BackgroundWorker.DoWork事件,并以異步的方式執(zhí)行DoWork事件中的代碼。
該方法還有一個(gè)帶參數(shù)的重載方法:RunWorkerAsync(Object)。
該方法允許傳遞一個(gè)Object類型的參數(shù)到后臺(tái)操作中,并且可以通過(guò)DoWork事件的DoWorkEventArgs.Argument屬性將該參數(shù)提取出來(lái)。
注:當(dāng)BackgroundWorker的IsBusy屬性為True時(shí),調(diào)用該方法將引發(fā)InvalidOperationException異常。
//在啟動(dòng)異步操作的地方鍵入代碼 bgWorker.RunWorkerAsync("hello");6.3.2 ReportProgress(Int percentProgress)
報(bào)告操作進(jìn)度。
調(diào)用該方法后,將觸發(fā)BackgroundWorker. ProgressChanged事件。
另外,該方法包含了一個(gè)int類型的參數(shù)percentProgress,用來(lái)表示當(dāng)前異步操作所執(zhí)行的進(jìn)度百分比。
該方法還有一個(gè)重載方法:ReportProgress(Int percentProgress,?Object userState)。
允許傳遞一個(gè)Object類型的狀態(tài)對(duì)象到 ProgressChanged事件中
并且可以通過(guò)ProgressChanged事件的ProgressChangedEventArgs.UserState屬性取得參數(shù)值。
注:調(diào)用該方法之前需確保WorkerReportsProgress屬性值為True,否則將引發(fā)InvalidOperationException異常。
用法:
for (int i = 0; i <= 100; i++) {c//向ProgressChanged報(bào)告進(jìn)度bgWorker.ReportProgress(i,"Working");System.Threading.Thread.Sleep(10); }6.3.3 CancelAsync()
請(qǐng)求取消當(dāng)前正在執(zhí)行的異步操作。
調(diào)用該方法將使BackgroundWorker.CancellationPending屬性設(shè)置為True。
但需要注意的是,并非每次調(diào)用CancelAsync()都能確保異步操作,CancelAsync()通常不適用于取消一個(gè)緊密執(zhí)行的操作,更適用于在循環(huán)體中執(zhí)行。
用法:
6.4 事件表
6.4.1 DoWork
用于承載異步操作。當(dāng)調(diào)用BackgroundWorker.RunWorkerAsync()時(shí)觸發(fā)。
需要注意的是:
由于DoWork事件內(nèi)部的代碼運(yùn)行在非UI線程之上,所以在DoWork事件內(nèi)部應(yīng)避免于用戶界面交互,
而于用戶界面交互的操作應(yīng)放置在ProgressChanged和RunWorkerCompleted事件中。
6.4.2 ProgressChanged
當(dāng)調(diào)用BackgroundWorker.ReportProgress(int percentProgress)方式時(shí)觸發(fā)該事件。
該事件的ProgressChangedEventArgs.ProgressPercentage屬性可以接收來(lái)自ReportProgress方法傳遞的percentProgress參數(shù)值,ProgressChangedEventArgs.UserState屬性可以接收來(lái)自ReportProgress方法傳遞的userState參數(shù)。
6.4.3 RunWorkerCompleted
異步操作完成或取消時(shí)執(zhí)行的操作,當(dāng)調(diào)用DoWork事件執(zhí)行完成時(shí)觸發(fā)。
該事件的RunWorkerCompletedEventArgs參數(shù)包含三個(gè)常用的屬性Error,Cancelled,Result。其中,Error表示在執(zhí)行異步操作期間發(fā)生的錯(cuò)誤;Cancelled用于判斷用戶是否取消了異步操作;Result屬性接收來(lái)自DoWork事件的DoWorkEventArgs參數(shù)的Result屬性值,可用于傳遞異步操作的執(zhí)行結(jié)果。
6.3 案例
using System; using System.ComponentModel; using System.Threading; using System.Windows.Forms;namespace bcworker {public partial class Form1 : Form{//后臺(tái)工作private BackgroundWorker bw = new BackgroundWorker();public Form1(){InitializeComponent();//后臺(tái)工作初始化bw.WorkerReportsProgress = true;//報(bào)告進(jìn)度bw.WorkerSupportsCancellation = true;//支持取消bw.DoWork += new DoWorkEventHandler(bgWorker_DoWork);//開始工作bw.ProgressChanged += new ProgressChangedEventHandler(bgWorker_ProgessChanged);//進(jìn)度改變事件bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgWorker_WorkerCompleted);//進(jìn)度完成事件}private void btnStart_Click(object sender, EventArgs e){//后臺(tái)工作運(yùn)行中,避免重入if (bw.IsBusy) return;bw.RunWorkerAsync("參數(shù)");//觸發(fā)DoWork事件并異步執(zhí)行,IsBusy置為True}//后臺(tái)工作將異步執(zhí)行public void bgWorker_DoWork(object sender, DoWorkEventArgs e){//(string)e.Argument == "參數(shù)";for (int i = 0; i <= 100; i++){if (bw.CancellationPending){//用戶取消了工作e.Cancel = true;return;}else{bw.ReportProgress(i, "Working");//報(bào)告進(jìn)度,觸發(fā)ProgressChanged事件Thread.Sleep(10);//模擬工作}}}//進(jìn)度改變事件public void bgWorker_ProgessChanged(object sender, ProgressChangedEventArgs e){//(string)e.UserState=="Working"progressBar1.Value = e.ProgressPercentage;//取得進(jìn)度更新控件,不用Invoke了}//后臺(tái)工作執(zhí)行完畢,IsBusy置為Falsepublic void bgWorker_WorkerCompleted(object sender, RunWorkerCompletedEventArgs e){//e.Error == null 是否發(fā)生錯(cuò)誤//e.Cancelled 完成是由于取消還是正常完成}private void btnCancel_Click(object sender, EventArgs e){if (bw.IsBusy) bw.CancelAsync();//設(shè)置CancellationPending屬性為True}} }總結(jié)
以上是生活随笔為你收集整理的一文带你搞懂C#多线程的5种写法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 学前教育计算机结束A卷,学前儿童发展心理
- 下一篇: 【C#公共帮助类】枚举独特类