一:創建加法類
//定義委托 public delegate int AddHandler(int a, int b); class AddMethod { public static int Add(int a, int b) { Console.WriteLine("開始計算:" + a + "+" + b); Thread.Sleep(3000); //模擬該方法運行三秒 Console.WriteLine("計算完成!"); return a + b; } }
二.同步調用
委托的Invoke方法用來進行同步調用。同步調用也可以叫阻塞調用,它將阻塞當前線程,然后執行調用,調用完畢后再繼續向下進行。
class Program { static void Main(string[] args) { Console.WriteLine("===== 同步調用 SyncInvokeTest ====="); AddHandler handler = new AddHandler(AddMethod.Add); int result = handler.Invoke(1, 2); Console.WriteLine("繼續做別的事情。。。"); Console.WriteLine(result); Console.ReadKey(); }
運行結果:

同步調用會阻塞線程,如果是要調用一項繁重的工作(如大量IO操作),可能會讓程序停頓很長時間,造成糟糕的用戶體驗,這時候異步調用就很有必要了。
三.異步調用
1.BeginInvoke方法和EndInvoke方法
BeginInvoke方法觸發你的異步方法,它和你想要執行的異步方法有相同的參數。另外還有兩個可選參數,第一個是AsyncCallback委托是異步完成的回調方法。第二個是用戶自定義對象,該對象將傳遞到回調方法中。BeginInvoke立即返回並且不等待完成異步的調用(繼續執行該下面的代碼,不需要等待)。BeginInvoke返回IAsyncResult接口,可用於檢測異步調用的過程。
通過EndInvoke方法檢測異步調用的結果。如果異步調用尚未完成,EndInvoke將阻塞調用線程,直到它完成。EndInvoke參數包括out和ref參數。
下面代碼演示使用BeginInvoke和EndInvoke進行異步調用的四種常見方式。在調用BeginInvoke可以做以下工作:
- 做一些其他操作,然后調用EndInvoke方法阻塞線程直到該方法完成。
- 使用IAsyncResult.AsyncWaitHandle屬性,使用它的WaitOne方法阻塞線程直到收到WaitHandle信號,然后調用EndInvoke。
- 檢查BeginInvoke返回值IAsyncResult的狀態來決定方法是否完成,然后調用EndInvoke方法。
- 通過在BeginInvoke方法中傳遞該委托,在回調方法中調用該委托的EenInvoke方法。
注意
無論你怎么使用,都必須調用EndInvoke方法結束你的異步調用。
2.異步調用
異步調用不阻塞線程,而是把調用塞到線程池中,程序主線程或UI線程可以繼續執行。
委托的異步調用通過BeginInvoke和EndInvoke來實現。
class Program { static void Main(string[] args) { Console.WriteLine("===== 異步調用 AsyncInvokeTest ====="); AddHandler handler = new AddHandler(AddMethod.Add);//委托 //IAsyncResult: 異步操作接口(interface) //BeginInvoke: 委托(delegate)的一個異步方法的開始,調用BeginInvoke方法,做一些其他操作(主線程繼續),然后調用EndInvoke方法阻塞線程(阻塞主線程)直到該方法完成。 IAsyncResult result = handler.BeginInvoke(1, 2, null, null); Console.WriteLine("繼續做別的事情。。。");
//調用EndInvoke,等待異步執行完成 Console.WriteLine("等待異步方法TestMethodAsync執行完成");
//等待異步執行完畢信號
//ressult.AsyncWaitHandle.WaitOne();
//Console.WriterLine("收到WaitHandle信號");
//異步操作返回,無論怎么使用,都必須調用EndInvoke方法結束你的異步調用 Console.WriteLine(handler.EndInvoke(result)); Console.ReadKey(); } }
運行結果:

可以看到,主線程並沒有等待,而是直接向下運行了。
但是問題依然存在,當主線程運行到EndInvoke時,如果這時調用沒有結束(這種情況很可能出現),這時為了等待調用結果,線程依舊會被阻塞。
注意:
如果創建的是NET.Core項目,則會報一下錯誤:

因為在.NET Core中不支持Action.BeginInvoke(null, null)的委托異步調用方法。
重新創建一個.NET Framework項目(測試用的是4.5.2),把代碼重新復制過去后,就沒有異常了。
3.Task
任務(Task)表示一個通過或不通過線程實現的並發操作,任務是可組合的,使用延續(continuation)可將它們串聯在一起,它們可以使用線程池減少啟動延遲,可使用回調方法避免多個線程同時等待I/O密集操作。
Task 類的表示單個操作不返回一個值,通常以異步方式執行。 Task 對象是一個的中心思想 基於任務的異步模式。
3.1Task和ThreadPool的區別
1.任務是架構在線程之上的,也就是說任務最終還是要拋給線程去執行;
2.任務和線程不是一對一的關系,比如開10個任務並不是說會開10個線程,這點任務有點類似線程池,但是任務相比線城池有很小的開銷和精確的控制;
ThreadPool相對於Thread來說可以減少線程的創建,有效減小系統開銷;但是ThreadPool不能控制線程的執行順序,我們也不能獲取線程池內線程取消/異常/完成的通知,即我們不能有效監控和控制線程池中的線程。
3.2Task的創建和運行
class Program { static void Main(string[] args) { //1.new方式實例化一個Task,需要通過Start方法啟動 Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine($"hello, task1的線程ID為{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //2.Task.Factory.StartNew(Action action)創建和啟動一個Task Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100); Console.WriteLine($"hello, task2的線程ID為{ Thread.CurrentThread.ManagedThreadId}"); }); //3.Task.Run(Action action)將任務放在線程池隊列,返回並啟動一個Task Task task3 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"hello, task3的線程ID為{ Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("執行主線程!"); Console.ReadKey(); } }
運行結果:

我們看到先打印"執行主線程",然后再打印各個任務,說明了Task不會阻塞主線程。上邊的例子Task都沒有返回值,我們也可以創建有返回值的~task,用法和沒有返回值的基本一致,我們簡單修改一下上邊的例子,代碼如下:
class Program { static void Main(string[] args) { //1.new方式實例化一個Task,需要通過Start方法啟動 Task<string> task = new Task<string>(() => { return $"hello, task1的ID為{Thread.CurrentThread.ManagedThreadId}"; }); task.Start(); //2.Task.Factory.StartNew(Func func)創建和啟動一個Task Task<string> task2 = Task.Factory.StartNew<string>(() => { return $"hello, task2的ID為{ Thread.CurrentThread.ManagedThreadId}"; }); //3.Task.Run(Func func)將任務放在線程池隊列,返回並啟動一個Task Task<string> task3 = Task.Run<string>(() => { return $"hello, task3的ID為{ Thread.CurrentThread.ManagedThreadId}"; }); Console.WriteLine("執行主線程!"); Console.WriteLine(task.Result); Console.WriteLine(task2.Result); Console.WriteLine(task3.Result); Console.ReadKey(); } }
運行結果:

注意task.Resut獲取結果時會阻塞線程,即如果task沒有執行完成,會等待task執行完成獲取到Result,然后再執行后邊的代碼。
上面實例中Task的執行都是異步,不會阻塞主線程。但有些場景想讓Task同步執行,這時Task提供了task.RunSynchronously()用於同步執行Task任務,代碼如下:
class Program { static void Main(string[] args) { Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine("執行Task結束!"); }); //同步執行,task會阻塞主線程 task.RunSynchronously(); task.Start(); Console.WriteLine("執行主線程結束!"); Console.ReadKey(); } }
運行結果:

異步執行:
Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine("執行Task結束!"); }); //同步執行,task會阻塞主線程 task.Start(); Console.WriteLine("執行主線程結束!"); Console.ReadKey();
運行結果:

3.3Task任務控制

3.3.1 阻塞方法
1.Thread阻塞線程的方法:
static void Main(string[] args) { Thread th1 = new Thread(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); th1.Start(); Thread th2 = new Thread(() => { Thread.Sleep(500); Console.WriteLine("線程2執行完畢"); }); th2.Start(); th1.Join(); th2.Join(); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); }
如果注釋掉兩個Join,執行結果是:先打印【主線程執行完畢】,而添加兩個Join方法后執行結果如下,實現了線程阻塞:

Thread使用join方法可以阻塞調用線程,但是有弊端:
(1)如果實現很多線程的阻塞時,每個線程都要調用一次join方法;
(2)如果想讓所有線程執行完畢(或任一線程執行完畢時),立即解除阻塞,使用join方法不容易實現。
2.Task阻塞線程的方法
Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制線程阻塞。
task.Wait() 表示等待task執行完畢,功能類似於thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都執行完成了再解除阻塞;Task.WaitAny(Task[] tasks)表示只要有一個task執行完畢就解除阻塞;
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線程2執行完畢!"); }); task2.Start(); //阻塞主線程。task1,task2都執行完畢再執行主線程 //執行【task1.Wait();task2.Wait();】可以實現相同功能 Task.WaitAll(new Task[] { task1, task2 }); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); }
運行結果:

如果將上例中的WaitAll換成WaitAny,那么任一task執行完畢就會解除線程阻塞,執行結果是:先打印【線程1執行完畢】,然后打印【主線程執行完畢】,最后打印【線程2執行完畢】;
3.阻塞方法:
(1)Task.Wait()
Task1.Wait():就是等待任務執行(task1)完成,task1的狀態變為Completed。
(2)Task.WaitAll()
等待所有的任務都執行完成。即當task,task2,task3…N全部任務都執行完成之后才會往下執行代碼(打印出:“主線程執行完畢!”)。
(3)Task.WaitAny()
同Task.WaitAll,就是等待任何一個任務完成就繼續向下執行
3.3.2 Task的延續操作
Wait/WaitAny/WaitAll方法返回值為void,這些方法單純的實現阻塞線程。我們現在想讓所有task執行完畢(或者任一task執行完畢)后,開始執行后續操作,怎么實現呢?這時就可以用到WhenAny/WhenAll方法了,這些方法執行完成返回一個task實例。 task.WhenAll(Task[] tasks) 表示所有的task都執行完畢后再去執行后續的操作, task.WhenAny(Task[] tasks) 表示任一task執行完畢后就開始執行后續操作;
class Program { static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線程2執行完畢!"); }); task2.Start(); //task1,task2執行完了后執行后續操作 Task.WhenAll(task1, task2).ContinueWith((t) => { Thread.Sleep(100); Console.WriteLine("執行后續操作完畢!"); }); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); } }
執行結果如下,我們看到WhenAll/WhenAny方法不會阻塞主線程,當使用WhenAll方法時所有的task都執行完畢才會執行后續操作;如果把例子中的WhenAll替換成WhenAny,則只要有一個線程執行完畢就會開始執行后續操作;

上面的例子也可以通過 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 來實現 ,修改上邊例子代碼如下,執行結果不變:
class Program { static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線程2執行完畢!"); }); task2.Start(); //通過TaskFactroy實現 Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) => { Thread.Sleep(100); Console.WriteLine("執行后續操作"); }); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); } }
第一個Task完成后自動啟動下一個Task,實現Task的延續;
3.3.3Task取消
Thread取消任務執行:Thread取消任務執行的一般流程:設置一個變量來控制任務是否停止,如設置一個變量isStop,然后線程輪詢查看isStop,如果isStop為true就停止;
static void Main(string[] args) { bool isStop = false; int index = 0; //開啟一個線程執行任務 Thread th1 = new Thread(() => { while (!isStop) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); th1.Start(); //五秒后取消任務執行 Thread.Sleep(5000); isStop = true; Console.ReadKey(); }
運行結果:

Task任務取消:
Task中有一個專門的類 CancellationTokenSource 來取消任務執行;
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); int index = 0; //開啟一個task執行任務 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); task1.Start(); //五秒后取消任務執行 Thread.Sleep(5000); //source.Cancel()方法請求取消任務,IsCancellationRequested會變成true source.Cancel(); Console.ReadKey(); }
CancellationTokenSource的功能不僅僅是取消任務執行,我們可以使用 source.CancelAfter(5000)實現5秒后自動取消任務,也可以通過 source.Token.Register(Action action)注冊取消任務觸發的回調函數,即任務被取消時注冊的action會被執行。
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); //注冊任務取消的事件 source.Token.Register(() => { Console.WriteLine("任務被取消后執行xx操作!"); }); int index = 0; //開啟一個task執行任務 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); task1.Start(); //延時取消,效果等同於Thread.Sleep(5000);source.Cancel(); source.CancelAfter(5000); Console.ReadKey(); }
執行結果如下,第5次執行在取消回調后打印,這是因為,執行取消的時候第5次任務已經通過了while()判斷,任務已經執行中了:

跨線程實例:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Task.Run(() => { Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); }; for (int i = 0; i < 1000000; i++) { myTxtbox.Invoke(setValue, i); } }); } }
運行結果:

3.4異步方法(asyc/await)
獲取文件內容示例:
class Program { static void Main(string[] args) { string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result; //調用同步方法 //string content = GetContent(Environment.CurrentDirectory + @"/test.txt"); Console.WriteLine(content); Console.ReadKey(); } //異步讀取文件內容 async static Task<string> GetContentAsync(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //ReadAync方法異步讀取內容,不阻塞線程 Console.WriteLine("開始讀取文件"); int len = await fs.ReadAsync(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } //同步讀取文件內容 static string GetContent(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //Read方法同步讀取內容,阻塞線程 int len = fs.Read(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } }
運行結果:

注意一個小問題:異步方法中方法簽名返回值為Task,代碼中的返回值為T。上邊栗子中GetContentAsync的簽名返回值為Task,而代碼中返回值為string。牢記這一細節對我們分析異步代碼很有幫助。
異步方法簽名的返回值有以下三種:
① Task:如果調用方法想通過調用異步方法獲取一個T類型的返回值,那么簽名必須為Task;
② Task:如果調用方法不想通過異步方法獲取一個值,僅僅想追蹤異步方法的執行狀態,那么我們可以設置異步方法簽名的返回值為Task;
③ void:如果調用方法僅僅只是調用一下異步方法,不和異步方法做其他交互,我們可以設置異步方法簽名的返回值為void,這種形式也叫做“調用並忘記”。
3.5工作示例
Task任務:
//沒有返回值得Task任務 private async Task GetDDPT(RequestPram reqPram, DataList repPram) { await Task.Run(() => { try { //業務代碼。。。 } catch (Exception ex) { } }); }
//沒有返回值得Task任務 private async Task GetDDPT(RequestPram reqPram, DataList repPram) { await Task.Run(() => { try { //業務代碼。。。 } catch (Exception ex) { } }); }
//沒有返回值得Task任務 private async Task GetDDPT(RequestPram reqPram, DataList repPram) { await Task.Run(() => { try { //業務代碼。。。 } catch (Exception ex) { } }); }
調用:
[HttpPost] public async Task<ReponsePram> QueryInsulateData(RequestPram pram) { ReponsePram reponsePrams = new ReponsePram(); reponsePrams.code = "200"; reponsePrams.msg = "數據查詢成功"; DataList dataList = new DataList(); //times.Start(); var taskDdpt = GetDDPT(pram, dataList); var taskPangGu = GetPanGu(pram, dataList); var taskMoEr = GetMoEr(pram, dataList); var tasks = new[] { taskDdpt, taskPangGu, taskMoEr };//任務組 var process = tasks.Select(async ts => { await ts; }); await Task.WhenAll(process); //等待處理完全 // times.Stop(); //業務代碼 return reponsePrams; }
三.異步回調
1.異步調用
static void Main(string[] args) { Console.WriteLine("===== 異步調用 AsyncInvokeTest ====="); AddHandler handler = new AddHandler(AddMethod.Add);//委托 //IAsyncResult: 異步操作接口(interface) //BeginInvoke: 委托(delegate)的一個異步方法的開始,調用BeginInvoke方法,做一些其他操作(主線程繼續),然后調用EndInvoke方法阻塞線程(阻塞主線程)直到該方法完成。 IAsyncResult result = handler.BeginInvoke(1, 2, null, null); Console.WriteLine("繼續做別的事情。。。"); //異步操作返回,無論怎么使用,都必須調用EndInvoke方法結束你的異步調用 Console.WriteLine(handler.EndInvoke(result)); Console.WriteLine("異步調用方法結束后,繼續做其他事!"); Console.ReadKey(); }
運行結果:

即調用EndInvoke方法時,這是會阻塞主線程,直到異步方法執行完畢,主線程才會繼續執行下去!
2.異步回調
用回調函數,當調用結束時會自動調用回調函數,解決了為等待調用結果,而讓線程依舊被阻塞的局面;
static void Main(string[] args) { Console.WriteLine("===== 異步回調 AsyncInvokeTest ====="); AddHandler handler = new AddHandler(AddMethod.Add); Console.WriteLine("主線程做第一件事!"); //異步操作接口(注意BeginInvoke方法的不同!) IAsyncResult result = handler.BeginInvoke(1, 2, new AsyncCallback(CallBackMethod), "AsycState:OK"); Console.WriteLine("繼續做別的事情,主線程不會被阻塞。。。"); Console.ReadKey(); } static void CallBackMethod(IAsyncResult result) { //result 是“加法類.Add()方法”的返回值 //AsyncResult 是IAsyncResult接口的一個實現類,空間:System.Runtime.Remoting.Messaging //AsyncDelegate 屬性可以強制轉換為用戶定義的委托的實際類。 AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate; Console.WriteLine(handler.EndInvoke(result)); Console.WriteLine(result.AsyncState); }
運行結果:

我定義的委托的類型為AddHandler,則為了訪問 AddHandler.EndInvoke,必須將異步委托強制轉換為 AddHandler。可以在異步回調函數(類型為 AsyncCallback)中調用 MAddHandler.EndInvoke,以獲取最初提交的 AddHandler.BeginInvoke 的結果。
問題:
(1)int result = handler.Invoke(1,2);
為什么Invoke的參數和返回值和AddHandler委托是一樣的呢?
答:
Invoke方法的參數很簡單,一個委托,一個參數表(可選),而Invoke方法的主要功能就是幫助你在UI線程上調用委托所指定的方法。Invoke方法首先檢查發出調用的線程(即當前線程)是不是UI線程,如果是,直接執行委托指向的方法,如果不是,它將切換到UI線程,然后執行委托指向的方法。不管當前線程是不是UI線程,Invoke都阻塞直到委托指向的方法執行完畢,然后切換回發出調用的線程(如果需要的話),返回。
所以Invoke方法的參數和返回值和調用他的委托應該是一致的。
(2)IAsyncResult result = handler.BeginInvoke(1,2,null,null);
BeginInvoke : 開始一個異步的請求,調用線程池中一個線程來執行,
返回IAsyncResult 對象(異步的核心). IAsyncResult 簡單的說,他存儲異步操作的狀態信息的一個接口,也可以用他來結束當前異步。
注意: BeginInvoke和EndInvoke必須成對調用.即使不需要返回值,但EndInvoke還是必須調用,否則可能會造成內存泄漏。
(3)IAsyncResult.AsyncState 屬性:
獲取用戶定義的對象,它限定或包含關於異步操作的信息。 例如:
{
AddHandler handler = (AddHandler)result.AsyncState;
Console.WriteLine(handler.EndInvoke(result));
。。。。。
}
