1. 程式人生 > >C#異步編程

C#異步編程

led state 文章 suspend 什麽 tps ssi 會有 because

什麽是異步編程

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

技術分享圖片
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

版權聲明

C#異步編程