第十六節:時隔兩年再談異步及深度剖析async和await(新)


一. 再談異步

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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM