一. 再談異步
1. 什么是異步方法
使用者發出調用指令后,不需要等待返回值,就可以繼續執行后面的代碼,異步方法基本上都是通過回調來通知調用者。
(PS:線程池是一組已經創建好的線程,隨用隨取,用完了不是銷毀線程,然后放到線程池中,供其他人用)
異步方法可以分為兩類:
(1).CPU-Bound(計算密集型任務):以線程為基礎,具體是用線程池里的線程還是新建線程,取決於具體的任務量。
(2).I/O-Bound(I/O密集型任務):是以windows事件為基礎(可能調用系統底層api),不需要新建一個線程或使用線程池里面的線程來執行具體工作,不涉及到使用系統原生線程。
2. .Net異步編程歷程
(1).EAP
基於事件的編程模型,會有一個回調方法,EAP 是 Event-based Asynchronous Pattern(基於事件的異步模型)的簡寫,類似於 Ajax 中的XmlHttpRequest,send之后並不是處理完成了,而是在 onreadystatechange 事件中再通知處理完成。
優點是簡單,缺點是當實現復雜的業務的時候很麻煩,比如下載 A 成功后再下載 b,如果下載 b成功再下載 c,否則就下載 d。
EAP 的類的特點是:一個異步方法配一個*** Completed 事件。.Net 中基於 EAP 的類比較少。也有更好的替代品,因此了解即可。
相關代碼:
WebClient wc = new WebClient(); wc.DownloadStringCompleted += Wc_DownloadStringCompleted; wc.DownloadStringAsync(new Uri("http://www.baidu.com")); private void Wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { MessageBox.Show(e.Result); }
(2).APM
APM(Asynchronous Programming Model)是.Net 舊版本中廣泛使用的異步編程模型。使用了 APM 的異步方法會返回一個 IAsyncResult 對象,這個對象有一個重要的屬性 AsyncWaitHandle,他是一個用來等待異步任務執行結束的一個同步信號。
APM 的特點是:方法名字以 BeginXXX 開頭,返回類型為 IAsyncResult,調用結束后需要EndXXX。 .Net 中有如下的常用類支持 APM:Stream、SqlCommand、Socket 等。
相關代碼:
FileStream fs = File.OpenRead("d:/1.txt"); byte[] buffer = new byte[16]; IAsyncResult aResult = fs.BeginRead(buffer, 0, buffer.Length, null, null); aResult.AsyncWaitHandle.WaitOne();//等待任務執行結束 MessageBox.Show(Encoding.UTF8.GetString(buffer)); fs.EndRead(aResult); // 如果不加aResult.AsyncWaitHandle.WaitOne() 那么很有可能打印出空白,因為 BeginRead只是“開始讀取”。調用完成一般要調用EndXXX 來回收資源。
(3).TAP(也有叫TPL的)
它是基於任務的異步編程模式,一定要注意,任務是一系列工作的抽象,而不是線程的抽象.也就是說當我們調用一個XX類庫提供的異步方法的時候,即使返回了Task/Task<T>,我們應該認為它是開始了一個新的任務,而不是開啟了一個新的線程。(TAP 以 Task 和 Task<T> 為基礎。它把具體的任務抽象成了統一的使用方式。這樣,不論是計算密集型任務,還是 I/O 密集型任務,我們都可以使用 async 、await 關鍵字來構建更加簡潔易懂的代碼)
相關代碼:
FileStream fs = File.OpenRead("d:/1.txt"); byte[] buffer = new byte[16]; int len = await fs.ReadAsync(buffer, 0, buffer.Length); MessageBox.Show("讀取了" + len + "個字節"); MessageBox.Show(Encoding.UTF8.GetString(buffer));
3. 剖析計算密集型任務和 I/O密集型任務
(1).計算密集型:await一個操作的時候,該操作通過Task.Run的方式啟動一個線程來處理相關的工作。當工作量大的時候,我們可以采用Task.Factory.StartNew,可以通過設置TaskCreateOptions.LongRunning選項 可以使新的任務運行於獨立的線程上,而非使用線程池里面的線程。
(2).I/O密集型: await一個操作的時候,雖然也返回一個Task或Task<T>,但這時並不開啟線程。
4.如何區分計算密集型任務還是I/O密集型任務?
計算密集型任務和I/O密集型任務的異步方法在使用上沒有任何差別,但底層實現卻大不相同, 判斷是計算型還是IO型主要看是占用CPU資源多 還是 占用I/O資源多。
比如:獲取某個網頁的內容
// 這是在 .NET 4.5 及以后推薦的網絡請求方式 HttpClient httpClient = new HttpClient(); var result = await httpClient.GetStringAsync("https://www.qq.com"); // 而不是以下這種方式(雖然得到的結果相同,但性能卻不一樣,並且在.NET 4.5及以后都不推薦使用) WebClient webClient = new WebClient(); var resultStr = Task.Run(() => { return webClient.DownloadString("https://www.qq.com"); });
比如:排序,屬於計算密集型任務
Random random = new Random(); List<int> data = new List<int>(); for (int i = 0; i< 50000000; i++) { data.Add(random.Next(0, 100000)); } // 這兒會啟動一個線程,來執行排序這種計算型任務 await Task.Run(() => { data.Sort(); });
所以我們在自己封裝的異步方法的時候,一定要注意任務的類型,來決定是否開啟線程。
5. TAP模式編碼注意事項
(先記住套路,后面通過代碼寫具體應用)
(1).異步方法返回Task或者Task<T>, 方法內部如果是返回void,則用Task; 如果有返回值,則用Task<T> ,且不要使用out和ref.
(2).async和await要成對出現,要么都有,要么都沒有,await不要加在返回值為void的前面,會編譯錯誤.
(3).我們應該使用非阻塞代碼來寫異步任務.
應該用:await、await Task.WhenAny、 await Task.WhenAll、await Task.Delay.
不要用:Task.Wait 、Task.Result、Task.WaitAny、Task.WaitAll、Thread.Sleep.
(4).如果是計算密集型任務,則應該使用 Task.Run 來執行任務;如果是耗時比較長的任務,則應該使用 Task.Factory.StartNew 並指定 TaskCreateOptions.LongRunning選項來執行任務如果是 I/O 密集型任務,不應該使用 Task.Run.
(5). 如果是 I/O 密集型任務,不應該使用 Task.Run!!! 因為 Task.Run 會在一個單獨的線程中運行(線程池或者新建一個獨立線程),而對於 I/O 任務來說,啟用一個線程意義不大,反而會浪費線程資源.
二. 深剖async和await
1.說明
async和await是一種異步編程模型,用於簡化代碼,達到“同步的方式寫異步的代碼”,編譯器會將async和await修飾的代碼編譯成狀態機,它們本身是不開啟線程的。
(async和await一般不要用於winform窗體程序,會出現一些意想不到的錯誤)
2.深層理解
(1).async和await只是一個狀態機,執行流程如下: await時釋放當前線程(當前線程回到線程池,可供別人調用)→進入狀態機等待【異步操作】完成→退出狀態機,從線程池中返回一個新的線程執行await下面的代碼(這里新的線程,有一點幾率是原線程;狀態機本身不會產生新的線程)
(2).異步操作分為兩種
A.CPU-Bound(計算密集型):比如 Task.Run ,這時釋放當前線程,異步操作會在一個新的線程中執行。
B.IO-Bound(IO密集型):比如一些非阻止Api, 像EF的SaveChangesAsync、寫文件的WriteLineAsync,這時釋放當前線程,異步操作不占用線程。
那么IO操作是靠什么執行的呢?
是以 Windows 事件為基礎的,因此不需要新建一個線程或使用線程池里面的線程來執行具體工作。
①.比如上面 SaveChangesAsync, await后,釋放當前線程,寫入數據庫的操作當然是由數據庫來做了; 再比如 await WriteLineAsync,釋放當前線程,寫入文件是調用系統底層的 的API來進行,至於系統Api怎么調度,我們就無法干預了.
②.我們使用的是系統的原生線程,而系統使用的是cpu線程,效率要高的多,我們能做的是盡量減少原生線程的占用.
(3).好處
A.提高了線程的利用率(即提高系統的吞吐量,提高了系統處理的並發請求數)----------針對IO-Bound場景。
一定要注意,是提高了系統的吞吐量,不能提升性能,也不能提高訪問速度。
B.多線程執行任務時,不會卡住當前線程--------------------------------------針對CPU-Bound場景。
3. IO-Bound異步對服務器的意義
每個服務器的工作線程數目是有限的,比如該服務器的用於處理項目請求的線程數目是8個,該cpu是單核,那么這8個線程在做時間片切換,也就是我們所謂的並發;假設該服務器收到了9個並發請求, 每個請求都要執行一個耗時的IO操作,下面分兩種情況討論:
(1).如果IO操作是同步,那么會有8個線程開始並發執行IO操作,第9個請求只能在那等待,必須等着這個8個請求中的某一個執行完才能去執行第9個請求,這個時候我們設想並發進來20個請求甚至 更多,從第9個開始,必須排隊等待,隨着隊列越來越長,服務器開始變慢,當隊列數超過IIS配置的數目的時候,會報503錯誤。
(2).如果IO操作是異步的,並且配合async和await關鍵字,同樣開始的時候8個線程並發執行IO操作,線程走到await關鍵字的時候,await會釋放當前線程,不再占用線程,等待異步操作完成后,再重新去線程池中分配一個線程;從而await釋放的當前線程就可以去處理別的請求,依次類推,線程的利用率變高了,也就是提高了系統處理的並發請求數(也叫系統的吞吐量).
4.測試
(1).同步場景:主線程會卡住不釋放,tId1和tId2的值一定相同。
(2).CPU-Bound場景: 利用Task.Run模擬耗時操作,經測試tId1和tId2是不同的(也有一定幾率是相同的),這就說明了await的時候當前線程已經釋放了.
(如下的:CalData2方法 和 CalData3Async方法)
(3).IO-Bound場景: 以EF插入10000條數據為例,調用SaveChangesAsync模擬IO-Bound場景,經測試tId1和tId2是不同的(也有一定幾率是相同的),這就說明了await的時候當前線程已經釋放了.
(直接在主線程中寫EF的相關的代碼 和 將相關代碼封裝成一個異步方法IOTestAsync 效果一樣)
(4).異步不回掉的場景:自己封裝一個異步方法,然后在接口中調用,注意調用的時候不加await,經測試主線程進入異步方法內,走到第一個await的時候會立即返回外層,繼續方法調用下面的代碼。
(如下面:TBDataAsync方法,主線程快速執行完, 異步方法還在那自己繼續執行,前提:要求該異步方法中不能有阻塞性的代碼!!!!)
這種異步不回調的場景如何理解呢?
主線程調用該異步方法,相當於執行一個任務,因為調用的時候沒有加await,所以不需要等待,即使異步方法內部會等待,但那已經是另外一個任務了,主線程本身並沒有等待這個任務,任務里的await那是任務自己事.
相關代碼:
/// <summary> /// 剖析async和await /// </summary> /// <returns></returns> public async Task<IActionResult> Index() { var tId1 = Thread.CurrentThread.ManagedThreadId; #region 01-同步場景 //{ // CalData1(); //} #endregion #region 02-CPU-Bound場景 //{ // await Task.Run(() => // { // //模擬耗時操作 // Thread.Sleep(3000); // }); // //等價於上面的代碼 // //await CalData2Async(); //} #endregion #region 03-CPU-Bound場景(自己封裝異步方法) //{ // int result = await CalData3Async(); //} #endregion #region 04-IO-Bound場景1 //{ // AsyncDBContext context = new AsyncDBContext(); // for (int i = 0; i < 10000; i++) // { // UserInfor uInfor = new UserInfor() // { // id = Guid.NewGuid().ToString("N"), // userName = "ypf", // addTime = DateTime.Now // }; // await context.AddAsync(uInfor); // //context.Add(uInfor); // } // await context.SaveChangesAsync(); //} #endregion #region 05-IO-Bound場景2(封裝成異步方法) //{ // int result = await IOTestAsync(); //} #endregion #region 06-異步不等待的場景 //{ // TBDataAsync(); //} #endregion var tId2 = Thread.CurrentThread.ManagedThreadId; ViewBag.tId1 = tId1; ViewBag.tId2 = tId2; return View(); } /// <summary> /// 同步場景-模擬耗時運算 /// </summary> /// <returns></returns> public void CalData1() { //模擬耗時操作 Thread.Sleep(4000); } /// <summary> /// 模擬耗時運算-一異步方法 /// (本身不會被編譯成狀態機) /// </summary> /// <returns></returns> public Task CalData2Async() { var task = Task.Run(() => { Thread.Sleep(3000); }); return task; } /// <summary> /// 模擬耗時操作,封裝成異步方法 /// (本身會被編譯成狀態機) /// </summary> /// <returns></returns> public async Task<int> CalData3Async() { var result = await Task.Run(() => { //耗時操作 Thread.Sleep(3000); return 100; }); return result; } /// <summary> /// IO-Bound場景(封裝成異步方法) /// </summary> /// <returns></returns> public async Task<int> IOTestAsync() { AsyncDBContext context = new AsyncDBContext(); for (int i = 0; i < 10000; i++) { UserInfor uInfor = new UserInfor() { id = Guid.NewGuid().ToString("N"), userName = "ypf", addTime = DateTime.Now }; await context.AddAsync(uInfor); //context.Add(uInfor); } return await context.SaveChangesAsync(); } /// <summary> /// 模擬耗時IO操作-用於測試異步不等待場景 /// </summary> /// <returns></returns> public async Task<int> TBDataAsync() { var tId2 = Thread.CurrentThread.ManagedThreadId; AsyncDBContext context = new AsyncDBContext(); await Task.Delay(7000); //模擬耗時操作,同步調用的時候,主線程走到這立即返回,接着走主線程的任務 //一個新的線程執行后面的代碼 Console.WriteLine($"線程id為:{Thread.CurrentThread.ManagedThreadId}"); var list = await context.Set<UserInfor>().ToListAsync(); foreach (var item in list) { item.userName = "001"; } return await context.SaveChangesAsync(); }
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。