C#異步編程


什么是異步編程

什么是異步編程呢?舉個簡單的例子:

using System.Net.Http;
using System.Threading.Tasks;
using static System.Console;

namespace Core
{
    class Async
    {
        static void Main()
        {
            Start();
            End();
        }

        static void Wait()=>WriteLine("waiting...");
        static void End()=>WriteLine("end...");
        static int Start()
        {
            WriteLine("start...");
            HttpClient client = new HttpClient();
            Waiting();
            var result = client.GetStringAsync("https://www.visualstudio.com/");
            string str = result.Result;
            return str.Length;
        }
    }
}

上面這段代碼中,Main方法中的代碼是按照自上而下的順序執行的。網絡狀況不佳時,Start()方法是比較耗時(注意,這里在Start方法中調用了異步方法GetStringAsync,但該方法在此處是以同步方式執行的,具體原因下文會進行說明),在Start()方法執行完畢之前,整個程序處於阻塞狀態。而異步編程可以很好的解決這個問題,一句簡單的話來概括異步編程就是,程序無須按照代碼順序自上而下的執行

async/await

C#5.0新增了async和await關鍵字,使用這兩個關鍵字可以大大簡化異步編程

使用 async 關鍵字可將方法、lambda 表達式匿名方法標記為異步,即,方法中應該包含一個或多個await表達式,但async關鍵字本身不會創建異步操作。

public async Task Asy()
{
  //do something...
}

這里需要注意一點,若使用async關鍵字標記的方法中沒有使用await關鍵字(編譯器會給出警告但不報錯),那么該方法將會以同步方式執行。

定義異步方法的幾點要求

定義一個異步方法應滿足以下幾點:

  • 使用async關鍵字來修飾方法
  • 在異步方法中使用await關鍵字(不使用編譯器會給出警告但不報錯),否則異步方法會以同步方式執行
  • 盡量不使用void作為返回類型,若希望異步方法返回void類型,請使用Task
  • 異步方法名稱以Async結尾
  • 異步方法中不能聲明使用ref或out關鍵字修飾的變量

下面定義一個異步方法StartAsync()

static async Task<int> StartAsync()
{
    HttpClient client = new HttpClient();
    var str = await client.GetStringAsync("https://www.visualstudio.com/");
    return str.Length;
}

異步方法的返回類型

  • Task<T>
    如果在調用匿名方法時使用了await關鍵字,且匿名方法的返回類型是Task<T>,那么我們得到的返回類型是T。若未使用await關鍵字,則返回類型是Task。未使用await,調用GetStringAsync方法時result是Task類型。

  

從上圖我們可以看到調用GetStringAsync方法時未使用await關鍵字,result是Task類型,我們可以通過GetType()方法來獲取result的詳細類型信息:

從上圖可以看到result的類型全名是System.Threading.Tasks.Task

 


 

從上圖我們可以看到使用await關鍵字時,result是string類型,而匿名方法GetStringAsync的返回類型是Task<string>

  • Task
    如果在調用匿名方法時使用了await關鍵字,且匿名方法的返回類型是Task,那么我們得到的返回類型是void。若為使用await關鍵字,則得到的返回類型是Task。

  • void
    不建議使用void作為異步方法的返回值。
    因為使用Task或Task<TResult>任務作為返回值,其屬性攜帶有關其狀態和歷史記錄的信息,如任務是否完成、異步方法是否導致異常或已取消以及最終結果是什么。而await運算符可訪問這些屬性。

異步方法執行流程


異步程序執行流程


上圖是微軟官方提供的講解異步程序執行流程的圖示,並附有解釋說明:

The numbers in the diagram correspond to the following steps.

  1. An event handler calls and awaits the AccessTheWebAsync async method.
  2. AccessTheWebAsync creates an HttpClient instance and calls the GetStringAsyncasynchronous method to download the contents of a website as a string.
  3. Something happens in GetStringAsync that suspends its progress. Perhaps it must wait for a website to download or some other blocking activity. To avoid blocking resources, GetStringAsync yields control to its caller, AccessTheWebAsync.
    GetStringAsync returns a Task<TResult> where TResult is a string, and AccessTheWebAsync assigns the task to thegetStringTask variable. The task represents the ongoing process for the call to GetStringAsync, with a commitment to produce an actual string value when the work is complete.
  4. Because getStringTask hasn't been awaited yet, AccessTheWebAsync can continue with other work that doesn't depend on the final result from GetStringAsync. That work is represented by a call to the synchronous method DoIndependentWork.
  5. DoIndependentWork is a synchronous method that does its work and returns to its caller.
  6. AccessTheWebAsync has run out of work that it can do without a result from getStringTask. AccessTheWebAsync next wants to calculate and return the length of the downloaded string, but the method can't calculate that value until the method has the string.
    Therefore, AccessTheWebAsync uses an await operator to suspend its progress and to yield control to the method that called AccessTheWebAsync. AccessTheWebAsync returns a Task<int> to the caller. The task represents a promise to produce an integer result that's the length of the downloaded string.

    Note
    If GetStringAsync (and therefore getStringTask) is complete before AccessTheWebAsync awaits it, control remains inAccessTheWebAsync. The expense of suspending and then returning to AccessTheWebAsync would be wasted if the called asynchronous process (getStringTask) has already completed and AccessTheWebSync doesn't have to wait for the final result.
    Inside the caller (the event handler in this example), the processing pattern continues. The caller might do other work that doesn't depend on the result from AccessTheWebAsync before awaiting that result, or the caller might await immediately. The event handler is waiting for AccessTheWebAsync, and AccessTheWebAsync is waiting for GetStringAsync.

  7. GetStringAsync completes and produces a string result. The string result isn't returned by the call to GetStringAsync in the way that you might expect. (Remember that the method already returned a task in step Instead, the string result is stored in the task that represents the completion of the method, getStringTask. The await operator retrieves the result from getStringTask. The assignment statement assigns the retrieved result to urlContents.
  8. When AccessTheWebAsync has the string result, the method can calculate the length of the string. Then the work ofAccessTheWebAsync is also complete, and the waiting event handler can resume. In the full example at the end of the topic, you can confirm that the event handler retrieves and prints the value of the length result.
    If you are new to asynchronous programming, take a minute to consider the difference between synchronous and asynchronous behavior. A synchronous method returns when its work is complete (step 5), but an async method returns a task value when its work is suspended (steps 3 and 6). When the async method eventually completes its work, the task is marked as completed and the result, if any, is stored in the task.

解釋雖是英文,但並沒有太難的單詞,是可以看懂其意思的。通過上面的說明,我們可以知道:
在遇到awiat關鍵字之前,程序是按照代碼順序自上而下以同步方式執行的。
在遇到await關鍵字之后,系統做了以下工作:

  1. 異步方法將被掛起
  2. 將控制權返回給調用者
  3. 使用線程池中的線程(而非額外創建新的線程)來計算await表達式的結果,所以await不會造成程序的阻塞
  4. 完成對await表達式的計算之后,若await表達式后面還有代碼則由執行await表達式的線程(不是調用方所在的線程)繼續執行這些代碼

使用一段代碼來進行驗證:

static void Main()
{
    Task<int> task = StartAsync();
    Thread.Sleep(5000);
    End();
}

static async Task<int> StartAsync()
{
    WriteLine("start...");
    HttpClient client = new HttpClient();
    var result = client.GetStringAsync("https://www.visualstudio.com/");
    string str = await result;
    return str.Length;
}

執行代碼


從上圖左側的調用棧中可以看到,在遇到await關鍵字之前,異步方法StartAsync自上而下同步執行。注意,這里異步方法GetStringAsync方法是被掛起的,不會造成程序的阻塞,控制權回到調用者StartAsync中,仔細看英文解釋中的第3步。
然后在Debug Console中輸入System.Threading.Thread.Current查看當前工作線程信息,以及System.Threading.Thread.CurrentThread.IsThreadPoolThread查看當前線程是否在線程池中。


 

從上圖我們看到,當前線程Id是1,不在線程池中。繼續執行程序:


遇到await關鍵字后,異步方法StartAsync被掛起,控制權也回到了調用者Main方法中。



從上圖我們可以看到異步方法StartAsync中的result變量的Status屬性值是WaitingForActivation,Result屬性值是Not yet computed

代碼繼續執行,將Main方法所在線程接掛起5秒,系統使用線程池中的線程計算await表達式的值:


從上圖我們可以看到,程序已經成功計算出await表達式的值,變量result的Status屬性值變成了RanToCompletion。完成對await表達式的計算之后,程序繼續執行后面的代碼(return str.Length)。

再看此時的工作線程信息:

我們看到,當前線程Id是5且存在於線程池中。

從這里我們可以得知異步是借助於多線程來實現的

Task

Task類擁有執行異步方法的兩個方法:Task.Run(),Task.Run<T>Task.Run以及Task.Run<T>使用線程池中的線程來執行代碼,它和使用await關鍵字的區別是:Task.Run直接使用線程池中的線程,而使用await的異步方法是在遇到await關鍵字后才使用多線程。

Thread

線程是前面所說的異步(async/await)和任務(Task)的基礎。和線程緊密相關的另外一個概念是進程,這里不多贅述。

 

ThreadPool

線程也是對象,頻繁的創建和銷毀線程比較影響性能,.NET提供線程池使得我們能夠復用線程對象從而避免頻繁創建和銷毀線程。

結語

自己創建線程比較麻煩但能夠更好的控制程序的運行,使用async/await關鍵字來編碼顯得較為簡潔,但對程序的控制力度會有所降低。

參考文章:

Asynchronous Programming with async and await (C#)
async
await
走進異步編程的世界 - 開始接觸 async/await
C#執行異步操作的幾種方式比較和總結
thread task parallel plinq async await多線程 任務及異步編程
走進異步編程的世界 - 在 GUI 中執行異步操作

Async/Await - Best Practices in Asynchronous Programming

版權聲明

本文為作者原創,版權歸作者雪飛鴻所有。 轉載必須保留文章的完整性,且在頁面明顯位置處標明原文鏈接

如有問題, 請發送郵件和作者聯系。


免責聲明!

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



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