為什麼我們要使用Async、Await關鍵字

来源:http://www.cnblogs.com/cuiyansong/archive/2017/08/24/7424997.html
-Advertisement-
Play Games

前不久,在工作中由於預設(xihuan)使用Async、Await關鍵字受到了很多質問,所以由此引發這篇博文“為什麼我們要用Async/Await關鍵字”,請聽下麵分解: Async/Await關鍵字 Visual Studio(.net framework 4.5)提供了非同步編程模型,相比之前實現 ...


 

前不久,在工作中由於預設(xihuan)使用Async、Await關鍵字受到了很多質問,所以由此引發這篇博文“為什麼我們要用Async/Await關鍵字”,請聽下麵分解:

 

Async/Await關鍵字

Visual Studio(.net framework 4.5)提供了非同步編程模型,相比之前實現方式,新的非同步編程模型降低了使用的複雜度並且更容易維護和調試,編譯器代替用戶做了很多複雜的工作來實現非同步編程模型[^4]。

 

 

 

  1. 調用非同步方法AccesstheWebAsync
  2. 創建HttpClient實例,並使用HttpClient獲取非同步數據。
  3. 利用Task執行獲取數據方法(假設獲取數據需要很長時間),不阻塞當前線程,getStringTask代表進行中的任務。
  4. 因為getStringTask還沒有使用await 關鍵字,使之可以繼續執行不依賴於其返回結果的其他任務,同步執行DoIndependentWork。
  5. 當同步任務DoIndependentWork執行完畢之後,返回調用給AccessTheWebAsync線程。
  6. 使用await強制等待getStringTask完成,並獲取基於Task<String>類型的返回值。(如果getStringTask在同步方法DoIndependentWork執行之前完成,調用會返回給AccessTheWebAsync線程,調用await將會執行不必要的掛起操作)
  7. 當獲取web數據之後,返回結果記錄在Task中並返回給await調用處(當然,返回值並沒有在第二行返回)。
  8. 獲取數據並返回計算結果。 

 

刨根問底

 

以上是官方給的說明文檔,例子詳盡表達清楚,但是有一個問題沒有解決(被證明):

 

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);
        }
    }
}
View Code

 

 

註: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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • -- 連接mysql 資料庫(前提是配置好MySQL資料庫的環境變數,加入path)mysql -uroot -p -- 設置文本的輸入輸出編碼:cmd 使用的是gbk,不然顯示亂碼set names gbk; -- 創建資料庫create database mydatabase charset u ...
  • case when語句,用於計算條件列表並返回多個可能結果表達式之一。CASE 具有兩種格式:1.when when_expression,是使用簡單 CASE 格式時所計算的表達式。Input_expression 是任何有效的SQL表達式。2.when Boolean_expression,使用... ...
  • 在SQL Server中使用Multiple Server Query Execution這個功能做資料庫維護或腳本發佈時非常方便,昨天由於磁碟空間原因,刪除清理了大量的軟體和組件,結果導致SSMS客戶端出了問題,重裝過後,使用Multiple Server Query Execution時,出現了 ...
  • 1創建文件repo文件 #vim /etc/yum.repos.d/mongodb-org-3.4.repo [mongodb-org-3.4] name=MongoDB Repository baseurl=https://repo.mongodb.org/yum/redhat/$releasev ...
  • 三層架構 常見架構: 開發中常見的23種設計模式: 創建型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。 結構型模式,共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。 行為型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、 ...
  • 1 private string GetMD5(string sDataIn) 2 { 3 MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); 4 byte[] bytValue, bytHash; 5 byt... ...
  • ServiceHub.DataWarehouseHost.exe記憶體泄漏問題的處理。 ...
  • 1 using System; 2 using System.Reflection; 3 4 namespace DynamicCall 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Console.WriteLin... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...