async/await 異步編程


前言

  最近在學習Web Api框架的時候接觸到了async/await,這個特性是.NET 4.5引入的,由於之前對於異步編程不是很了解,所以花費了一些時間學習一下相關的知識,並整理成這篇博客,如果在閱讀的過程中發現不對的地方,歡迎大家指正。

同步編程與異步編程

  通常情況下,我們寫的C#代碼就是同步的,運行在同一個線程中,從程序的第一行代碼到最后一句代碼順序執行。而異步編程的核心是使用多線程,通過讓不同的線程執行不同的任務,實現不同代碼的並行運行。

前台線程與后台線程

  關於多線程,早在.NET2.0時代,基礎類庫中就提供了Thread實現。默認情況下,實例化一個Thread創建的是前台線程,只要有前台線程在運行,應用程序的進程就一直處於運行狀態,以控制台應用程序為例,在Main方法中實例化一個Thread,這個Main方法就會等待Thread線程執行完畢才退出。而對於后台線程,應用程序將不考慮其是否執行完畢,只要應用程序的主線程和前台線程執行完畢就可以退出,退出后所有的后台線程將被自動終止。來看代碼應該更清楚一些:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主線程開始");

            //實例化Thread,默認創建前台線程
            Thread t1 = new Thread(DoRun1);
            t1.Start();

            //可以通過修改Thread的IsBackground,將其變為后台線程
            Thread t2 = new Thread(DoRun2) { IsBackground = true };
            t2.Start();

            Console.WriteLine("主線程結束");
        }

        static void DoRun1()
        {
            Thread.Sleep(500);
            Console.WriteLine("這是前台線程調用");
        }

        static void DoRun2()
        {
            Thread.Sleep(1500);
            Console.WriteLine("這是后台線程調用");
        }
    }
}
前台線程&后台線程

  運行上面的代碼,可以看到DoRun2方法的打印信息“這是后台線程調用”將不會被顯示出來,因為應用程序執行完主線程和前台線程后,就自動退出了,所有的后台線程將被自動終止。這里后台線程設置了等待1.5s,假如這個后台線程比前台線程或主線程提前執行完畢,對應的信息“這是后台線程調用”將可以被成功打印出來。

Task

  .NET 4.0推出了新一代的多線程模型Task。async/await特性是與Task緊密相關的,所以在了解async/await前必須充分了解Task的使用。這里將以一個簡單的Demo來看一下Task的使用,同時與Thread的創建方式做一下對比。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主線程啟動");
            
            //.NET 4.5引入了Task.Run靜態方法來啟動一個線程
            Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("Task1啟動"); });

            //Task啟動的是后台線程,假如要在主線程中等待后台線程執行完畢,可以調用Wait方法
            Task task = Task.Run(() => { Thread.Sleep(500); Console.WriteLine("Task2啟動"); });
            task.Wait();
            
            Console.WriteLine("主線程結束");
        }
    }
}
Task的使用

  首先,必須明確一點是Task啟動的線程是后台線程,不過可以通過在Main方法中調用task.Wait()方法,使應用程序等待task執行完畢。Task與Thread的一個重要區分點是:Task底層是使用線程池的,而Thread每次實例化都會創建一個新的線程。這里可以通過這段代碼做一次驗證:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        static void DoRun1()
        {
            Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId);
        }

        static void DoRun2()
        {
            Thread.Sleep(50);
            Console.WriteLine("Task調用Thread Id =" + Thread.CurrentThread.ManagedThreadId);
        }

        static void Main(string[] args)
        {
            for (int i = 0; i < 50; i++)
            {
                new Thread(DoRun1).Start();
            }

            for (int i = 0; i < 50; i++)
            {
                Task.Run(() => { DoRun2(); });
            }

            //讓應用程序不立即退出
            Console.Read();
        }
    }
}
Task底層使用線程池

  運行代碼,可以看到DoRun1()方法每次的Thread Id都是不同的,而DoRun2()方法的Thread Id是重復出現的。我們知道線程的創建和銷毀是一個開銷比較大的操作,Task.Run()每次執行將不會立即創建一個新線程,而是到CLR線程池查看是否有空閑的線程,有的話就取一個線程處理這個請求,處理完請求后再把線程放回線程池,這個線程也不會立即撤銷,而是設置為空閑狀態,可供線程池再次調度,從而減少開銷。

Task<TResult>

  Task<TResult>是Task的泛型版本,這兩個之間的最大不同是Task<TResult>可以有一個返回值,看一下代碼應該一目了然: 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主線程開始");

            Task<string> task = Task<string>.Run(() => { Thread.Sleep(1000); return Thread.CurrentThread.ManagedThreadId.ToString(); });
            Console.WriteLine(task.Result);

            Console.WriteLine("主線程結束");
        }
    }
}
Task<TResult>的使用

  Task<TResult>的實例對象有一個Result屬性,當在Main方法中調用task.Result的時候,將等待task執行完畢並得到返回值,這里的效果跟調用task.Wait()是一樣的,只是多了一個返回值。

async/await 特性

  經過前面的鋪墊,終於迎來了這篇文章的主角async/await,還是先通過代碼來感受一下這兩個特性的使用。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("-------主線程啟動-------");
            Task<int> task = GetLengthAsync();
            Console.WriteLine("Main方法做其他事情");
            Console.WriteLine("Task返回的值" + task.Result);
            Console.WriteLine("-------主線程結束-------");
        }

        static async Task<int> GetLengthAsync()
        {
            Console.WriteLine("GetLengthAsync Start");  
            string str = await GetStringAsync();
            Console.WriteLine("GetLengthAsync End");
            return str.Length;
        }

        static Task<string> GetStringAsync()
        {
            return Task<string>.Run(() => { Thread.Sleep(2000); return "finished"; });
        }
    }
}
async/await 用法

  首先來看一下async關鍵字。async用來修飾方法,表明這個方法是異步的,聲明的方法的返回類型必須為:void或Task或Task<TResult>。返回類型為Task的異步方法中無需使用return返回值,而返回類型為Task<TResult>的異步方法中必須使用return返回一個TResult的值,如上述Demo中的異步方法返回一個int。而返回類型可為void,則是為了和事件處理程序兼容,比如下面的示例:

public Form1()
{
    InitializeComponent();
    btnDo.Click += Down;
}

public async void Down(object sender, EventArgs e)
{
    btnDo.Enabled = false;
    string str = await Run();
    labText.Text = str;
    btnDo.Enabled = true;
}

public Task<string> Run()
{
    return Task.Run(() => { Thread.Sleep(5000); return DateTime.Now.ToString(); });
}
async修飾void

  再來看一下await關鍵字。await必須用來修飾Task或Task<TResult>,而且只能出現在已經用async關鍵字修飾的異步方法中。

  通常情況下,async/await必須成對出現才有意義,假如一個方法聲明為async,但卻沒有使用await關鍵字,則這個方法在執行的時候就被當作同步方法,這時編譯器也會拋出警告提示async修飾的方法中沒有使用await,將被作為同步方法使用。了解了關鍵字async\await的特點后,我們來看一下上述Demo在控制台會輸入什么吧。

  

  輸出的結果已經很明確地告訴我們整個執行流程了。GetLengthAsync異步方法剛開始是同步執行的,所以"GetLengthAsync Start"字符串會被打印出來,直到遇到第一個await關鍵字,真正的異步任務GetStringAsync開始執行,await相當於起到一個標記/喚醒點的作用,同時將控制權放回給Main方法,"Main方法做其他事情"字符串會被打印出來。之后由於Main方法需要訪問到task.Result,所以就會等待異步方法GetLengthAsync的執行,而GetLengthAsync又等待GetStringAsync的執行,一旦GetStringAsync執行完畢,就會回到await GetStringAsync這個點上執行往下執行,這時"GetLengthAsync End"字符串就會被打印出來。

  當然,我們也可以使用下面的方法完成上面控制台的輸出。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("-------主線程啟動-------");
            Task<int> task = GetLengthAsync();
            Console.WriteLine("Main方法做其他事情");
            Console.WriteLine("Task返回的值" + task.Result);
            Console.WriteLine("-------主線程結束-------");
        }

        static Task<int> GetLengthAsync()
        {
            Console.WriteLine("GetLengthAsync Start");
            Task<int> task = Task<int>.Run(() => { string str = GetStringAsync().Result; 
                Console.WriteLine("GetLengthAsync End"); 
                return str.Length; });           
            return task;
        }

        static Task<string> GetStringAsync()
        {
            return Task<string>.Run(() => { Thread.Sleep(2000); return "finished"; });
        }
    }
}
不使用async\await

  對比兩種方法,是不是async\await關鍵字的原理其實就是通過使用一個線程完成異步調用嗎?答案是否定的。async關鍵字表明可以在方法內部使用await關鍵字,方法在執行到await前都是同步執行的,運行到await處就會掛起,並返回到Main方法中,直到await標記的Task執行完畢,才喚醒回到await點上,繼續向下執行。更深入點的介紹可以查看文章末尾的參考文獻。

async/await 實際應用

  微軟已經對一些基礎類庫的方法提供了異步實現,接下來將實現一個例子來介紹一下async/await的實際應用。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using System.Net;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("開始獲取博客園首頁字符數量");
            Task<int> task1 = CountCharsAsync("http://www.cnblogs.com");
            Console.WriteLine("開始獲取百度首頁字符數量");
            Task<int> task2 = CountCharsAsync("http://www.baidu.com");

            Console.WriteLine("Main方法中做其他事情");

            Console.WriteLine("博客園:" + task1.Result);
            Console.WriteLine("百度:" + task2.Result);
        }

        static async Task<int> CountCharsAsync(string url)
        {
            WebClient wc = new WebClient();
            string result = await wc.DownloadStringTaskAsync(new Uri(url));
            return result.Length;
        }
    }
}
Demo

參考文獻:<IIIustrated C# 2012>     關於async/await的FAQ    《深入理解C#》


免責聲明!

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



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