C#異步的世界(下)
前言
今天說異步的主要是指C#5的async\await異步。在此為了方便的表述,我們稱async\await之前的異步為“舊異步”,async\await為“新異步”。
新異步的使用
只能說新異步的使用太簡單(如果僅僅只是說使用)
方法加上async修飾符,然后使用await關(guān)鍵字執(zhí)行異步方法,即可。對(duì)就是如此簡單。像使用同步方法邏輯一樣使用異步。
public async Task<int> Test() { var num1 = await GetNumber(1); var num2 = await GetNumber(num1); var task = GetNumber(num2); //或者 var num3 = await task; return num1 + num2 + num3; }
新異步的優(yōu)勢(shì)
在此之前已經(jīng)有了多種異步模式,為什么還要引入和學(xué)習(xí)新的async\await異步呢?當(dāng)然它肯定是有其獨(dú)特的優(yōu)勢(shì)。
我們分兩個(gè)方面來分析:WinForm、WPF等單線程UI程序和Web后臺(tái)服務(wù)程序。
對(duì)于WinForm、WPF等單線程UI程序
代碼1(舊異步)
private void button1_Click(object sender, EventArgs e) { var request = WebRequest.Create("https://github.com/"); request.BeginGetResponse(new AsyncCallback(t => { //(1)處理請(qǐng)求結(jié)果的邏輯必須寫這里 label1.Invoke((Action)(() => { label1.Text = "[舊異步]執(zhí)行完畢!"; }));//(2)這里跨線程訪問UI需要做處理 }), null); }
代碼2(同步)
private void button3_Click(object sender, EventArgs e) { HttpClient http = new HttpClient(); var htmlStr = http.GetStringAsync("https://github.com/").Result; //(1)處理請(qǐng)求結(jié)果的邏輯可以寫這里 label1.Text = "[同步]執(zhí)行完畢!";//(2)不在需要做跨線程UI處理了 }
代碼3(新異步)
private async void button2_Click(object sender, EventArgs e) { HttpClient http = new HttpClient(); var htmlStr = await http.GetStringAsync("https://github.com/"); //(1)處理請(qǐng)求結(jié)果的邏輯可以寫這里 label1.Text = "[新異步]執(zhí)行完畢!";//(2)不在需要做跨線程UI處理了 }
新異步的優(yōu)勢(shì):
- 沒有了煩人的回調(diào)處理
- 不會(huì)像同步代碼一樣阻塞UI界面(造成假死)
- 不在像舊異步處理后訪問UI不在需要做跨線程處理
- 像使用同步代碼一樣使用異步(超清晰的邏輯)
是的,說得再多還不如看看實(shí)際效果圖來得實(shí)際:(新舊異步UI線程沒有阻塞,同步阻塞了UI線程)
【思考】:舊的異步模式是開啟了一個(gè)新的線程去執(zhí)行,不會(huì)阻塞UI線程。這點(diǎn)很好理解??墒?,新的異步看上去和同步區(qū)別不大,為什么也不會(huì)阻塞界面呢?
【原因】:新異步,在執(zhí)行await表達(dá)式前都是使用UI線程,await表達(dá)式后會(huì)啟用新的線程去執(zhí)行異步,直到異步執(zhí)行完成并返回結(jié)果,然后再回到UI線程(據(jù)說使用了SynchronizationContext)。所以,await是沒有阻塞UI線程的,也就不會(huì)造成界面的假死。
【注意】:我們?cè)谘菔就酱a的時(shí)候使用了Result。然,在UI單線程程序中使用Result來使異步代碼當(dāng)同步代碼使用是一件很危險(xiǎn)的事(起碼對(duì)于不太了解新異步的同學(xué)來說是這樣)。至于具體原因稍候再分析(哎呀,別跑啊)。
對(duì)于Web后臺(tái)服務(wù)程序
也許對(duì)于后臺(tái)程序的影響沒有單線程程序那么直觀,但其價(jià)值也是非常大的。且很多人對(duì)新異步存在誤解。
【誤解】:新異步可以提升Web程序的性能。
【正解】:異步不會(huì)提升單次請(qǐng)求結(jié)果的時(shí)間,但是可以提高Web程序的吞吐量。
1、為什么不會(huì)提升單次請(qǐng)求結(jié)果的時(shí)間?
其實(shí)我們從上面示例代碼(雖然是UI程序的代碼)也可以看出。
2、為什么可以提高Web程序的吞吐量?
那什么是吞吐量呢,也就是本來只能十個(gè)人同時(shí)訪問的網(wǎng)站現(xiàn)在可以二十個(gè)人同時(shí)訪問了。也就是常說的并發(fā)量。
還是用上面的代碼來解釋。[代碼2] 阻塞了UI線程等待請(qǐng)求結(jié)果,所以UI線程被占用,而[代碼3]使用了新的線程請(qǐng)求,所以UI線程沒有被占用,而可以繼續(xù)響應(yīng)UI界面。
那問題來了,我們的Web程序天生就是多線程的,且web線程都是跑的線程池線程(使用線程池線程是為了避免不斷創(chuàng)建、銷毀線程所造成的資源成本浪費(fèi)),而線程池線程可使用線程數(shù)量是一定的,盡管可以設(shè)置,但它還是會(huì)在一定范圍內(nèi)。如此一來,我們web線程是珍貴的(物以稀為貴),不能濫用。用完了,那么其他用戶請(qǐng)求的時(shí)候就無法處理直接503了。
那什么算是濫用呢?比如:文件讀取、URL請(qǐng)求、數(shù)據(jù)庫訪問等IO請(qǐng)求。如果用web線程來做這個(gè)耗時(shí)的IO操作那么就會(huì)阻塞web線程,而web線程阻塞得多了web線程池線程就不夠用了。也就達(dá)到了web程序最大訪問數(shù)。
此時(shí)我們的新異步橫空出世,解放了那些原本處理IO請(qǐng)求而阻塞的web線程(想偷懶?沒門,干活了。)。通過異步方式使用相對(duì)廉價(jià)的線程(非web線程池線程)來處理IO操作,這樣web線程池線程就可以解放出來處理更多的請(qǐng)求了。
不信?下面我們來測(cè)試下:
【測(cè)試步驟】:
1、新建一個(gè)web api項(xiàng)目
2、新建一個(gè)數(shù)據(jù)訪問類,分別提供同步、異步方法(在方法邏輯執(zhí)行前后讀取時(shí)間、線程id、web線程池線程使用數(shù))
public class GetDataHelper { /// <summary> /// 同步方法獲取數(shù)據(jù) /// </summary> /// <returns></returns> public string GetData() { var beginInfo = GetBeginThreadInfo(); using (HttpClient http = new HttpClient()) { http.GetStringAsync("https://github.com/").Wait();//注意:這里是同步阻塞 } return beginInfo + GetEndThreadInfo(); } /// <summary> /// 異步方法獲取數(shù)據(jù) /// </summary> /// <returns></returns> public async Task<string> GetDataAsync() { var beginInfo = GetBeginThreadInfo(); using (HttpClient http = new HttpClient()) { await http.GetStringAsync("https://github.com/");//注意:這里是異步等待 } return beginInfo + GetEndThreadInfo(); } public string GetBeginThreadInfo() { int t1, t2, t3; ThreadPool.GetAvailableThreads(out t1, out t3); ThreadPool.GetMaxThreads(out t2, out t3); return string.Format("開始:{0:mm:ss,ffff} 線程Id:{1} Web線程數(shù):{2}", DateTime.Now, Thread.CurrentThread.ManagedThreadId, t2 - t1); } public string GetEndThreadInfo() { int t1, t2, t3; ThreadPool.GetAvailableThreads(out t1, out t3); ThreadPool.GetMaxThreads(out t2, out t3); return string.Format(" 結(jié)束:{0:mm:ss,ffff} 線程Id:{1} Web線程數(shù):{2}", DateTime.Now, Thread.CurrentThread.ManagedThreadId, t2 - t1); } }
3、新建一個(gè)web api控制器
[HttpGet] public async Task<string> Get(string str) { GetDataHelper sqlHelper = new GetDataHelper(); switch (str) { case "異步處理":// return await sqlHelper.GetDataAsync(); case "同步處理":// return sqlHelper.GetData(); } return "參數(shù)不正確"; }
4、發(fā)布web api程序,部署到本地iis(同步鏈接:http://localhost:803/api/Home?str=同步處理 異步鏈接:http://localhost:803/api/Home?str=異步處理)
5、接著上面的winform程序里面測(cè)試請(qǐng)求:(同時(shí)發(fā)起10個(gè)請(qǐng)求)
private void button6_Click(object sender, EventArgs e) { textBox1.Text = ""; label1.Text = ""; Task.Run(() => { TestResultUrl("http://localhost:803/api/Home?str=同步處理"); }); } private void button5_Click(object sender, EventArgs e) { textBox1.Text = ""; label1.Text = ""; Task.Run(() => { TestResultUrl("http://localhost:803/api/Home?str=異步處理"); }); } public void TestResultUrl(string url) { int resultEnd = 0; HttpClient http = new HttpClient(); int number = 10; for (int i = 0; i < number; i++) { new Thread(async () => { var resultStr = await http.GetStringAsync(url); label1.Invoke((Action)(() => { textBox1.AppendText(resultStr.Replace(" ", "\r\t") + "\r\n"); if (++resultEnd >= number) { label1.Text = "全部執(zhí)行完畢"; } })); }).Start(); } }
6、重啟iis,并用瀏覽器訪問一次要請(qǐng)求的鏈接地址(預(yù)熱)
7、啟動(dòng)winform程序,點(diǎn)擊“訪問同步實(shí)現(xiàn)的Web”:
8、重復(fù)6,然后重新啟動(dòng)winform程序點(diǎn)擊“訪問異步實(shí)現(xiàn)的Web”
看到這些數(shù)據(jù)有什么感想?
數(shù)據(jù)和我們前面的【正解】完全吻合。仔細(xì)觀察,每個(gè)單次請(qǐng)求用時(shí)基本上相差不大。 但是步驟7"同步實(shí)現(xiàn)"最高投入web線程數(shù)是10,而步驟8“異步實(shí)現(xiàn)”最高投入web線程數(shù)是3。
也就是說“異步實(shí)現(xiàn)”使用更少的web線程完成了同樣的請(qǐng)求數(shù)量,如此一來我們就有更多剩余的web線程去處理更多用戶發(fā)起的請(qǐng)求。
接著我們還發(fā)現(xiàn)同步實(shí)現(xiàn)請(qǐng)求前后的線程ID是一致的,而異步實(shí)現(xiàn)前后線程ID不一定一致。再次證明執(zhí)行await異步前釋放了主線程。
【結(jié)論】:
- 使用新異步可以提升Web服務(wù)程序的吞吐量
- 對(duì)于客戶端來說,web服務(wù)的異步并不會(huì)提高客戶端的單次訪問速度。
- 執(zhí)行新異步前會(huì)釋放web線程,而等待異步執(zhí)行完成后又回到了web線程上。從而提高web線程的利用率。
【圖解】:
Result的死鎖陷阱
我們?cè)诜治鯱I單線程程序的時(shí)候說過,要慎用異步的Result屬性。下面我們來分析:
private void button4_Click(object sender, EventArgs e) { label1.Text = GetUlrString("https://github.com/").Result; } public async Task<string> GetUlrString(string url) { using (HttpClient http = new HttpClient()) { return await http.GetStringAsync(url); } }
代碼GetUlrString("https://github.com/").Result的Result屬性會(huì)阻塞(占用)UI線程,而執(zhí)行到GetUlrString方法的 await異步的時(shí)候又要釋放UI線程。此時(shí)矛盾就來了,由于線程資源的搶占導(dǎo)致死鎖。
且Result屬性和.Wait()方法一樣會(huì)阻塞線程。此等問題在Web服務(wù)程序里面一樣存在。(區(qū)別:UI單次線程程序和web服務(wù)程序都會(huì)釋放主線程,不同的是Web服務(wù)線程不一定會(huì)回到原來的主線程,而UI程序一定會(huì)回到原來的UI線程)
我們前面說過,.net為什么會(huì)這么智能的自動(dòng)釋放主線程然后等待異步執(zhí)行完畢后又回到主線程是因?yàn)镾ynchronizationContext的功勞。
但這里有個(gè)例外,那就是控制臺(tái)程序里面是沒有SynchronizationContext的。所以這段代碼放在控制臺(tái)里面運(yùn)行是沒有問題的。
static void Main(string[] args) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); GetUlrString("https://github.com/").Wait(); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Console.ReadKey(); } public async static Task<string> GetUlrString(string url) { using (HttpClient http = new HttpClient()) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); return await http.GetStringAsync(url); } }
打印出來的都是同一個(gè)線程ID
使用AsyncHelper在同步代碼里面調(diào)用異步
但可是,可但是,我們必須在同步方法里面執(zhí)行異步怎辦?辦法肯定是有的
我們首先定義一個(gè)AsyncHelper靜態(tài)類:
static class AsyncHelper { private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); public static TResult RunSync<TResult>(Func<Task<TResult>> func) { return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult(); } public static void RunSync(Func<Task> func) { _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult(); } }
然后調(diào)用異步:
private void button7_Click(object sender, EventArgs e) { label1.Text = AsyncHelper.RunSync(() => GetUlrString("https://github.com/")); }
這樣就不會(huì)死鎖了。
ConfigureAwait
除了AsyncHelper我們還可以使用Task的ConfigureAwait方法來避免死鎖
private void button7_Click(object sender, EventArgs e) { label1.Text = GetUlrString("https://github.com/").Result; } public async Task<string> GetUlrString(string url) { using (HttpClient http = new HttpClient()) { return await http.GetStringAsync(url).ConfigureAwait(false); } }
ConfigureAwait的作用:使當(dāng)前async方法的await后續(xù)操作不需要恢復(fù)到主線程(不需要保存線程上下文)。
異常處理
關(guān)于新異步里面拋出異常的正確姿勢(shì)。我們先來看下面一段代碼:
private async void button8_Click(object sender, EventArgs e) { Task<string> task = GetUlrStringErr(null); Thread.Sleep(1000);//一段邏輯。。。。 textBox1.Text = await task; } public async Task<string> GetUlrStringErr(string url) { if (string.IsNullOrWhiteSpace(url)) { throw new Exception("url不能為空"); } using (HttpClient http = new HttpClient()) { return await http.GetStringAsync(url); } }
調(diào)試執(zhí)行執(zhí)行流程:
在執(zhí)行完118行的時(shí)候竟然沒有把異常拋出來?這不是逆天了嗎。非得在等待await執(zhí)行的時(shí)候才報(bào)錯(cuò),顯然119行的邏輯執(zhí)行是沒有什么意義的。讓我們把異常提前拋出:
提取一個(gè)方法來做驗(yàn)證,這樣就能及時(shí)的拋出異常了。有朋友會(huì)說這樣的太坑爹了吧,一個(gè)驗(yàn)證還非得另外寫個(gè)方法。接下來我們提供一個(gè)沒有這么坑爹的方式:
在異步函數(shù)里面用匿名異步函數(shù)進(jìn)行包裝,同樣可以實(shí)現(xiàn)及時(shí)驗(yàn)證。
感覺也不比前種方式好多少...可是能怎么辦呢。
異步的實(shí)現(xiàn)
上面簡單分析了新異步能力和屬性。接下來讓我們繼續(xù)揭秘異步的本質(zhì),神秘的外套下面究竟是怎么實(shí)現(xiàn)的。
首先我們編寫一個(gè)用來反編譯的示例:
class MyAsyncTest { public async Task<string> GetUrlStringAsync(HttpClient http, string url, int time) { await Task.Delay(time); return await http.GetStringAsync(url); } }
反編譯代碼:
為了方便閱讀,我們把編譯器自動(dòng)命名的類型重命名。
GetUrlStringAsync方法變成了如此模樣:
public Task<string> GetUrlStringAsync(HttpClient http, string url, int time) { GetUrlStringAsyncdStateMachine stateMachine = new GetUrlStringAsyncdStateMachine() { _this = this, http = http, url = url, time = time, _builder = AsyncTaskMethodBuilder<string>.Create(), _state = -1 }; stateMachine._builder.Start(ref stateMachine); return stateMachine._builder.Task; }
方法簽名完全一致,只是里面的內(nèi)容變成了一個(gè)狀態(tài)機(jī)GetUrlStringAsyncdStateMachine 的調(diào)用。此狀態(tài)機(jī)就是編譯器自動(dòng)創(chuàng)建的。下面來看看神秘的狀態(tài)機(jī)是什么鬼:
private sealed class GetUrlStringAsyncdStateMachine : IAsyncStateMachine { public int _state; public MyAsyncTest _this; private string _str1; public AsyncTaskMethodBuilder<string> _builder; private TaskAwaiter taskAwaiter1; private TaskAwaiter<string> taskAwaiter2; //異步方法的三個(gè)形參都到這里來了 public HttpClient http; public int time; public string url; private void MoveNext() { string str; int num = this._state; try { TaskAwaiter awaiter; MyAsyncTest.GetUrlStringAsyncdStateMachine d__; string str2; switch (num) { case 0: break; case 1: goto Label_00CD; default: //這里是異步方法 await Task.Delay(time);的具體實(shí)現(xiàn) awaiter = Task.Delay(this.time).GetAwaiter(); if (awaiter.IsCompleted) { goto Label_0077; } this._state = num = 0; this.taskAwaiter1 = awaiter; d__ = this; this._builder.AwaitUnsafeOnCompleted<TaskAwaiter, MyAsyncTest.GetUrlStringAsyncdStateMachine>(ref awaiter, ref d__); return; } awaiter = this.taskAwaiter1; this.taskAwaiter1 = new TaskAwaiter(); this._state = num = -1; Label_0077: awaiter.GetResult(); awaiter = new TaskAwaiter(); //這里是異步方法await http.GetStringAsync(url);的具體實(shí)現(xiàn) TaskAwaiter<string> awaiter2 = this.http.GetStringAsync(this.url).GetAwaiter(); if (awaiter2.IsCompleted) { goto Label_00EA; } this._state = num = 1; this.taskAwaiter2 = awaiter2; d__ = this; this._builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, MyAsyncTest.GetUrlStringAsyncdStateMachine>(ref awaiter2, ref d__); return; Label_00CD: awaiter2 = this.taskAwaiter2; this.taskAwaiter2 = new TaskAwaiter<string>(); this._state = num = -1; Label_00EA: str2 = awaiter2.GetResult(); awaiter2 = new TaskAwaiter<string>(); this._str1 = str2; str = this._str1; } catch (Exception exception) { this._state = -2; this._builder.SetException(exception); return; } this._state = -2; this._builder.SetResult(str); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } }
明顯多個(gè)異步等待執(zhí)行的時(shí)候就是在不斷調(diào)用狀態(tài)機(jī)中的MoveNext()方法。經(jīng)驗(yàn)來至我們之前分析過的IEumerable,不過今天的這個(gè)明顯復(fù)雜度要高于以前的那個(gè)。猜測(cè)是如此,我們還是來驗(yàn)證下事實(shí):
在起始方法GetUrlStringAsync第一次啟動(dòng)狀態(tài)機(jī)stateMachine._builder.Start(ref stateMachine);
確實(shí)是調(diào)用了MoveNext。因?yàn)開state的初始值是-1,所以執(zhí)行到了下面的位置:
繞了一圈又回到了MoveNext。由此,我們可以現(xiàn)象成多個(gè)異步調(diào)用就是在不斷執(zhí)行MoveNext直到結(jié)束。
說了這么久有什么意思呢,似乎忘記了我們的目的是要通過之前編寫的測(cè)試代碼來分析異步的執(zhí)行邏輯的。
再次貼出之前的測(cè)試代碼,以免忘記了。
反編譯后代碼執(zhí)行邏輯圖:
當(dāng)然這只是可能性較大的執(zhí)行流程,但也有awaiter.Iscompleted為true的情況。其他可能的留著大家自己去琢磨吧。
以上就是C#異步的世界(下)的詳細(xì)內(nèi)容,更多關(guān)于C#異步的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
【C#基礎(chǔ)】Substring截取字符串的方法小結(jié)(推薦)
這篇文章主要介紹了Substring截取字符串方法小結(jié),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05C#創(chuàng)建Windows服務(wù)與服務(wù)的安裝、卸載
這篇文章介紹了C#創(chuàng)建Windows服務(wù)與服務(wù)的安裝、卸載,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-02-02Unity鍵盤WASD實(shí)現(xiàn)物體移動(dòng)
這篇文章主要為大家詳細(xì)介紹了Unity鍵盤WASD實(shí)現(xiàn)物體移動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02