为什么我们要使用Async、Await关键字
前不久,在工作中由于默認(xihuan)使用Async、Await關鍵字受到了很多質問,所以由此引發這篇博文“為什么我們要用Async/Await關鍵字”,請聽下面分解:
?
Async/Await關鍵字
Visual Studio(.net framework 4.5)提供了異步編程模型,相比之前實現方式,新的異步編程模型降低了使用的復雜度并且更容易維護和調試,編譯器代替用戶做了很多復雜的工作來實現異步編程模型[^4]。
?
?
?
調用異步方法AccesstheWebAsync
創建HttpClient實例,并使用HttpClient獲取異步數據。
利用Task執行獲取數據方法(假設獲取數據需要很長時間),不阻塞當前線程,getStringTask代表進行中的任務。
因為getStringTask還沒有使用await 關鍵字,使之可以繼續執行不依賴于其返回結果的其他任務,同步執行DoIndependentWork。
當同步任務DoIndependentWork執行完畢之后,返回調用給AccessTheWebAsync線程。
使用await強制等待getStringTask完成,并獲取基于Task<String>類型的返回值。(如果getStringTask在同步方法DoIndependentWork執行之前完成,調用會返回給AccessTheWebAsync線程,調用await將會執行不必要的掛起操作)
當獲取web數據之后,返回結果記錄在Task中并返回給await調用處(當然,返回值并沒有在第二行返回)。
獲取數據并返回計算結果。?
?
刨根問底
?
以上是官方給的說明文檔,例子詳盡表達清楚,但是有一個問題沒有解決(被證明):
?
1. 當線程在await處返回給線程池之后,該線程是否“真的”被其他請求所消費?
2. 服務器線程資源是一定的,是誰在真正執行Await所等待的操作,或者說異步IO操作?
3. 如果使用IO線程執行異步IO操作,相比線程池的線程有什么優勢?或者說異步比同步操作優勢在哪里?
?
前提條件:
?
1. 相對Console應用程序來說,可以使用ThreadPool的SetMaxThread來模擬當前進程所支持的最大工作線程和IO線程數。
2. 通過ThreadPool的GetAvailableThreads可以獲得當前進程工作線程和IO線程的可用數量。
3. ThreadPool是基于進程的,每一個進程有一個線程池,IIS Host的進程可以單獨管理線程池。
4. 如果要真正意義上的模擬異步IO線程操作文件需要設置FileOptions.Asynchronous,而不是僅僅是使用BeginXXX一類的方法,詳情請參考[^1]的異步IO線程。
5. 在驗證同步和異步調用時,執行的任務數量要大于當前最大工作線程的2倍,這樣才可以測出當Await釋放工作線程后,其他請求可繼續利用該線程。
?
?
結論:
?
1. ?Await使用異步IO線程來執行,異步操作的任務,釋放工作線程回線程池。
2. ?線程池分為工作線程和異步IO線程,分別執行不同級別的任務。
3. ?使用Await來執行異步操作效率并不總是高于同步操作,需要根據異步執行長短來判斷。
4. ?當工作線程和IO線程相互切換時,會有一定性能消耗。
?
各位可以Clone代碼,并根據Commit去Review代碼,相信大家能理解代碼意圖,如果不能,請留言,我改進:)?
?
[GitHubRepo](https://github.com/Cuiyansong/Why-To-Use-Async-Await-In-DotNet.git)
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
?
namespace AsyncAwaitConsole
{
? ? class Program
? ? {
? ? ? ? static int maxWorkerThreads;
? ? ? ? static int maxAsyncIoThreadNum;
? ? ? ? const string UserDirectory = @"files\";
? ? ? ? const int BufferSize = 1024 * 4;
?
? ? ? ? static void Main(string[] args)
? ? ? ? {
? ? ? ? ? ? AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Directory.Delete("files", true);
? ? ? ? ? ? };
?
? ? ? ? ? ? maxWorkerThreads = Environment.ProcessorCount;
? ? ? ? ? ? maxAsyncIoThreadNum = Environment.ProcessorCount;
? ? ? ? ? ? ThreadPool.SetMaxThreads(maxWorkerThreads, maxAsyncIoThreadNum);
?
? ? ? ? ? ? LogRunningTime(() =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? for (int i = 0; i < Environment.ProcessorCount * 2; i++)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ?Task.Factory.StartNew(SyncJob, new {Id = i});
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
?
? ? ? ? ? ? Console.WriteLine("===========================================");
?
? ? ? ? ? ? LogRunningTime(() =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? for (int i = 0; i < Environment.ProcessorCount * 2; i++)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Task.Factory.StartNew(AsyncJob, new { Id = i });
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
?
? ? ? ? ? ? Console.ReadKey();
? ? ? ? }
?
? ? ? ? static void SyncJob(dynamic stateInfo)
? ? ? ? {
? ? ? ? ? ? var id = (long)stateInfo.Id;
? ? ? ? ? ? Console.WriteLine("Job Id: {0}, sync starting...", id);
?
? ? ? ? ? ? using (FileStream sourceReader = new FileStream(UserDirectory + "BigFile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? using (FileStream destinationWriter = new FileStream(UserDirectory + $"CopiedFile-{id}.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, BufferSize))
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? CopyFileSync(sourceReader, destinationWriter);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? Console.WriteLine("Job Id: {0}, completed...", id);
? ? ? ? }
?
? ? ? ? static async Task AsyncJob(dynamic stateInfo)
? ? ? ? {
? ? ? ? ? ? var id = (long)stateInfo.Id;
? ? ? ? ? ? Console.WriteLine("Job Id: {0}, async starting...", id);
?
? ? ? ? ? ? using (FileStream sourceReader = new FileStream(UserDirectory + "BigFile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.Asynchronous))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? using (FileStream destinationWriter = new FileStream(UserDirectory + $"CopiedFile-{id}.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, BufferSize, FileOptions.Asynchronous))
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? await CopyFilesAsync(sourceReader, destinationWriter);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? Console.WriteLine("Job Id: {0}, async completed...", id);
? ? ? ? }
?
? ? ? ? static async Task CopyFilesAsync(FileStream source, FileStream destination)
? ? ? ? {
? ? ? ? ? ? var buffer = new byte[BufferSize + 1];
? ? ? ? ? ? int numRead;
? ? ? ? ? ? while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? await destination.WriteAsync(buffer, 0, numRead);
? ? ? ? ? ? }
? ? ? ? }
?
? ? ? ? static void CopyFileSync(FileStream source, FileStream destination)
? ? ? ? {
? ? ? ? ? ? var buffer = new byte[BufferSize + 1];
? ? ? ? ? ? int numRead;
? ? ? ? ? ? while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? destination.Write(buffer, 0, numRead);
? ? ? ? ? ? }
? ? ? ? }
?
? ? ? ? static void LogRunningTime(Action callback)
? ? ? ? {
? ? ? ? ? ? var awailableWorkingThreadCount = 0;
? ? ? ? ? ? var awailableAsyncIoThreadCount = 0;
?
? ? ? ? ? ? var watch = Stopwatch.StartNew();
? ? ? ? ? ? watch.Start();
?
? ? ? ? ? ? callback();
?
? ? ? ? ? ? while (awailableWorkingThreadCount != maxWorkerThreads)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Thread.Sleep(500);
? ? ? ? ? ? ? ? ThreadPool.GetAvailableThreads(out awailableWorkingThreadCount, out awailableAsyncIoThreadCount);
?
? ? ? ? ? ? ? ? Console.WriteLine("[Alive] working thread: {0}, async IO thread: {1}", awailableWorkingThreadCount, awailableAsyncIoThreadCount);
? ? ? ? ? ? }
?
? ? ? ? ? ? watch.Stop();
? ? ? ? ? ? Console.WriteLine("[Finsih] current awailible working thread is {0} and used {1}ms", awailableWorkingThreadCount, watch.ElapsedMilliseconds);
? ? ? ? }
? ? }
}
注:Async/Await并沒有創建新的線程,而是基于當前同步上線文的線程,相比Thread/Task或者是基于線程的BackgroundWorker使用起來更方便。Async關鍵字的作用是標識在Await處需要等待方法執行完成,過多的await不會導致編譯器錯誤,但如果沒有await時,方法將轉換為同步方法.?
?
基于IIS Host的應用程序?
?
?
?
?1.?IIS 可以托管ThreadPool,通過在IIS Application Pool中增加,并且可以設置Working Thread 和 Async IO Thread 數目。
2. 服務端接受請求并從線程池中獲取當前閑置的線程進行處理,如果是同步處理請求,當前線程等待處理完成然后返回給線程池. 服務器線程數量有限,當超過IIS所能處理的最大請求時,將返回503錯誤。
3. 服務端接受請求并異步處理請求時,當遇到異步IO類型操作時,當前線程返回給線程池。當異步操作完成時,從線程池中拿到新的線程并繼續執行任務,直至完成后續任務[^7]。
?
例如,在MVC Controller中加入awaitable方法,證明當遇到阻塞任務時,當前線程立即返回線程池。當阻塞任務完成時,將從線程池中獲取新的線程執行后續任務:
?
? var availableWorkingThreadCount = 0;
? ? ? ? ? ? ? ? var availableAsyncIoThreadCount = 0;
? ? ? ? ? ? ? ? ThreadPool.GetAvailableThreads(out availableWorkingThreadCount, out availableAsyncIoThreadCount);
? ? ? ? ? ? ? ? AddErrors(new IdentityResult(string.Format("[IIS Host] Thread Id {0}, ThreadPool Thread: {1}",
? ? ? ? ? ? ? ? ? ? Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread)));
? ? ? ? ? ? ? ? AddErrors(new IdentityResult(string.Format("[IIS Host] current working thread: {0}, current async thread: {1}", availableWorkingThreadCount, availableAsyncIoThreadCount)));
?
? ? ? ? ? ? ? ? HttpClient httpClient = new HttpClient();
? ? ? ? ? ? ? ? var response = httpClient.GetStringAsync("https://msdn.microsoft.com/en-us/library/system.threading.thread.isthreadpoolthread(v=vs.110).aspx");
? ? ? ? ? ? ? ? await response;
?
? ? ? ? ? ? ? ? AddErrors(new IdentityResult(string.Format("[IIS Host] Thread Id {0}, ThreadPool Thread: {1}",
? ? ? ? ? ? ? ? Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread)));
?
?
[IIS Host] Thread Id 4, ThreadPool Thread: True
[IIS Host] current working thread: 4094, current async thread: 1000
[IIS Host] Thread Id 9, ThreadPool Thread: True
?
?
結論:
同步方法應用場景:
請求處理非常快
代碼簡潔大于代碼效率
主要是基于CPU耗時操作
?
異步方法應用場景:
基于Network或者I/O類型操作,而非CPU耗時操作
當阻塞操作成為瓶頸時,通過異步方法能使IIS處理更多的請求
并行化處理比代碼簡潔更重要
提供一種機制可以讓用戶取消長時間運行的請求?
?
?更多線程優化
Stephen Cleary 介紹了三種異步編程模型的規范[^5]:
1. Avoid Async Void,?void和task<T>將產生不同的異常類型
2. 總是使用Async關鍵字
3. 使用Task.WaitXXX 代替Task.WhenXXX
4. Configure context?盡量不要捕捉線程上下文,使用Task.ConfigureAwait(false)
?
引用
[^1] 《CLR via C# Edition3》 25章線程基礎
[^2]百科-蜜蜂舞:http://baike.baidu.com/link?url=ixwDjgocRIg4MJGTQyR3mUC1fspHZtfPYEtADfJAJdC6X0xIVU4lJUe2iVvCNHEj3JeE1JalBCNyyPcVMdhaoyBFz_xXcLPMEJ_2iUcHjithF8_F8A9yI61EAzpmpYR4
[^3] 異步編程模型:https://msdn.microsoft.com/en-us/library/mt674882.aspx
[^4] C# Async、Await關鍵字:https://msdn.microsoft.com/library/hh191443(vs.110).aspx
[^5] Task Best Practice[Stephen Cleary]:?https://msdn.microsoft.com/en-us/magazine/jj991977.aspx
[^6] 異步編程模型最佳實踐中文翻譯版:http://www.cnblogs.com/farb/p/4842920.html
[^7] 同步vs異步Controller:https://msdn.microsoft.com/en-us/library/ee728598%28v=vs.100%29.aspx
[^8] IIS 優化:?https://docs.microsoft.com/en-us/aspnet/mvc/overview/performance/using-asynchronous-methods-in-aspnet-mvc-4
?
原文地址:http://www.cnblogs.com/cuiyansong/p/7424997.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的为什么我们要使用Async、Await关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: asp.net core MVC 过滤器
- 下一篇: 【上海】关于云计算,你想学习哪些知识,快