微軟的Task已經出來很久了,一直沒有去研究,以為就是和Thread差不多的東西。直到最近看到了Task的使用介紹,發現比Thread的語法要精煉多了,於是便在項目中用上了。
結果就出問題了,數據庫連接池用一段時間就滿了,排除了各種原因,最后開始懷疑是不是Task有什么不為人知的隱患。
由於對Task的使用只是停留在開一個線程去執行一個不需要返回結果的任務這種階段。為了查明是否是Task引起的線程池滿,便開始各種查資料。
最終的結果是,連接池滿是因為程序中的一個SqlConnection沒有關閉,和Task沒有半毛錢關系......
問題解決了。Task也研究的差不多了。
下面我們來談一下Task的使用.....
開啟一個Task
開啟task有以下三種方式,曾經一度糾結在到底該用哪種方式來開始一個任務,最終發現其實在沒有特殊要求的情況下,這三種方式除了語法不同外,執行方式和結果是一樣的
Task<int> t1 = Task.Factory.StartNew(() => { Task.Delay(1000); return 1; }); Task<int> t2 = new Task<int>(() => { Task.Delay(1000); return 1; }); t2.Start(); Task<int> t3 = Task.Run(() => { Task.Delay(1000); return 1; });
上面的示例返回一個數字,所以接收者是Task的泛型,同樣也可以執行一個不帶返回結果的函數。
Task返回值
1.可以直接通過Task .Result屬性來獲取Task的結果
使用這種方式來獲取結果,主線程會等待Task執行完成。也就是說,用這種方式來獲取Task的返回結果,和不使用Task並沒有什么區別。
但是需要注意的是,慎用.Result或者Wait來獲取Task的返回值,除非你明確地知道Task的代碼邏輯。我們來看下面一段代碼
當點擊button2時,應用程序會卡死。因為在調用.Result時,UI線程會阻塞,
而我們給GetResult的任務指出需要用UI線程來執行任務中的代碼。
UI線程在等待GetResult完成,卻又無法去運行GetResult中的代碼。造成死鎖
2.await 和 async
先上一個測試代碼
static void Main(string[] args) { Console.WriteLine("程序啟動"); Task<int> t = GetNum(); Console.WriteLine("主線程繼續執行"); int num = t.Result; Console.WriteLine("主線程獲取到數字:" + num); } public static async Task<int> GetNum() { Console.WriteLine("GetNum函數進入"); int num = await Task.Run(() => { Task.Delay(1000); return 2; }); Console.WriteLine("GetNum函數獲取到數字:"+num); return num; }
await 運算符只能用於異步方法中,所以包含await運算符的方法都需要有async修飾符來修飾,稱之為異步方法。
通過實驗,我們看到在異步函數中,遇到await運算符之后,主線程就繼續往下執行了,更確切的解釋是,Main函數開始和GetNum函數並行執行,
直到獲取t.Result時,若GetNum()函數仍未執行完畢,Main函數則需要等待GetNum返回。
在GetNum函數中,await后面的代碼需要等待await的Task執行完成后方可執行,等同於下面不適用await的代碼
static void Main(string[] args) { Console.WriteLine("程序啟動"); Task<int> t = GetNumNoAwait(); Console.WriteLine("主線程繼續執行"); int num = t.Result; Console.WriteLine("主線程獲取到數字:" + num); Console.Read(); } public static Task<int> GetNumNoAwait() { Console.WriteLine("GetNum函數進入"); Task<int> t = Task<int>.Run(() => { Task.Delay(1000); return 2; }); t.ContinueWith(m => { Console.WriteLine("GetNum函數獲取到數字:" + m.Result); }); return t; }
通過await來獲取返回值的操作,比前一種方式的優點在於他不會阻塞主線程的。這一點在winform或wpf等gui程序上可以很明顯地提現出來
Task在winform中的使用
這是一個winform程序的代碼片段,頁面中有兩個按鈕,我們用maketext函數來模擬一個需要耗時的比如調用api獲取數據的操作。
當點擊button1時程序會一直等待結果返回,期間窗體無法拖動
而用異步方法則不會阻塞主窗體的其他操作
AsyncController
看過很多在Action中使用異步action的文章,並以此和未使用異步的Action來進行網站吞吐量的對比。
大概的代碼類似於下面這樣
最終都會得出一個結論,以上代碼的吞吐量要遠遠高於未使用異步的
當時我就很不解,await就是在等待異步代碼執行完成,並不會釋放請求占用的線程,為什么會提升網站的吞吐量呢?
經過各種實驗,仍然無法來證明以上代碼的寫法會使得網站的吞吐量更高,反而大多數情況下,效率稍微低了一些
(剛剛看過一本書中有介紹,通常的方法調用比使用async關鍵字的同樣方法調用要快上40-50倍。所以異步函數在合適的場景被正確地使用也是非常重要的)
最終看了Msdn上關於異步控制器的介紹,方才找到正確的寫法
以下是截取MSdn上的代碼片段
首先使用 AsyncManager.OutstandingOperations.Increment()函數來設定未完成的請求操作,默認是1,然后每一個異步操作完成,通過Decrement來使計數器減1,當計數器歸零之后,則會調用xxxCompleted函數來返回結果。
這樣解釋就行的通了,當執行完NewsAsync中的代碼之后,請求線程就會釋放,直到異步函數執行完成,系統會重新獲取一個線程通過NewsCompleted來返回給客戶端執行結果。
下面是我的測試代碼
public class TaskTestController : AsyncController { public void GetDataAsync() { AsyncManager.OutstandingOperations.Increment(); WebClient client = new WebClient(); client.DownloadDataCompleted += (sender, e) => { AsyncManager.Parameters["bytelength"] = e.Result.Length; AsyncManager.OutstandingOperations.Decrement(); }; client.DownloadDataAsync(new Uri("https://docs.microsoft.com/zh-cn/dotnet/framework/get-started/")); } public int GetDataCompleted(int bytelength) { return bytelength; } }
public class TaskTest1Controller : Controller { public int GetData() { WebClient client = new WebClient(); var rsu = client.DownloadData(new Uri("https://docs.microsoft.com/zh-cn/dotnet/framework/get-started/")); return rsu.Length; ; } }
然后我進行模擬1000個並發2000條請求,下面是測試結果
這里就可以看到異步控制器的優勢已經顯露出來了
然后我將iis的最大並發設置為10,模擬了一個20並發200條請求的操作,
異步控制器用時3.001s,失敗0條
普通控制器用時4.551s,失敗8條
測試完成,希望對有需要的人有所幫助