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