程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 淺談Async和Await如何簡化異步編程(幾個實例讓你徹底明白)

淺談Async和Await如何簡化異步編程(幾個實例讓你徹底明白)

編輯:C#入門知識

淺談Async和Await如何簡化異步編程(幾個實例讓你徹底明白)。本站提示廣大學習愛好者:(淺談Async和Await如何簡化異步編程(幾個實例讓你徹底明白))文章只能為提供參考,不一定能成為您想要的結果。以下是淺談Async和Await如何簡化異步編程(幾個實例讓你徹底明白)正文


引言

C#5.0中async和await兩個關鍵字,這兩個關鍵字簡化了異步編程,之所以簡化了,還是由於編譯器給我們做了更多的任務,上面就詳細看看編譯器究竟在面前幫我們做了哪些復雜的任務的。

同步代碼存在的問題

關於同步的代碼,大家一定都不生疏,由於我們往常寫的代碼大局部都是同步的,但是同步代碼卻存在一個很嚴重的問題,例如我們向一個Web服務器收回一個懇求時,假如我們收回懇求的代碼是同步完成的話,這時分我們的使用順序就會處於等候形態,直到發出一個呼應信息為止,但是在這個等候的形態,關於用戶不能操作任何的UI界面以及也沒有任何的音訊,假如我們試圖去操作界面時,此時我們就會看到”使用順序為呼應”的信息(在使用順序的窗口旁),置信大家在往常運用桌面軟件或許訪問web的時分,一定都遇到過這樣相似的狀況的,關於這個,大家一定會覺得看上去十分不舒適。惹起這個緣由正是由於代碼的完成是同步完成的,所以在沒有失掉一個呼應音訊之前,界面就成了一個”卡死”形態了,所以這關於用戶來說一定是不可承受的,由於假如我要從服務器上下載一個很大的文件時,此時我們甚至不能對窗體停止封閉的操作的。為了詳細闡明同步代碼存在的問題(形成界面開端),上面經過一個順序讓大家更抽象地看下問題所在:

// 單擊事情
    private void btnClick_Click(object sender, EventArgs e)
    {
      this.btnClick.Enabled = false;

      long length = AccessWeb();
      this.btnClick.Enabled = true;
      // 這裡可以做一些不依賴回復的操作
      OtherWork();

      this.richTextBox1.Text += String.Format("\n 回復的字節長度為: {0}.\r\n", length);
      txbMainThreadID.Text = Thread.CurrentThread.ManagedThreadId.ToString();
    }

    private long AccessWeb()
    {
      MemoryStream content = new MemoryStream();

      // 對MSDN發起一個Web懇求
      HttpWebRequest webRequest = WebRequest.Create("http://msdn.microsoft.com/zh-cn/") as HttpWebRequest;
      if (webRequest != null)
      {
        // 前往回復後果
        using (WebResponse response = webRequest.GetResponse())
        {
          using (Stream responseStream = response.GetResponseStream())
          {
            responseStream.CopyTo(content);
          }
        }
      }

      txbAsynMethodID.Text = Thread.CurrentThread.ManagedThreadId.ToString();
      return content.Length;
    }

運轉順序後,當我們點擊窗體的 “點擊我”按鈕之後,在失掉服務器呼應之前,我們不能對窗體停止任何的操作,包括挪動窗體,封閉窗體等,詳細運轉後果如下:

傳統的異步編程來改善順序的呼應

下面局部我們曾經看到同步辦法所帶來的實踐問題了,為理解決相似的問題,.NET Framework很早就提供了對異步編程的支持,上面就用.NET 1.0中提出的異步編程模型(APM)來處理下面的問題,詳細代碼如下(正文的局部經過取得GUI線程的同步上文對象,然後同步伐用同步上下文對象的post辦法把要調用的辦法交給GUI線程去處置,由於控件原本就是由GUI線程創立的,然後由它自己執行訪問控件的操作就不存在跨線程的問題了,順序中運用的是調用RichTextBox控件的Invoke方式來異步回調訪問控件的辦法,其實面前的原來和正文局部是一樣的,調用RichTextBox控件的Invoke辦法可以取得創立RichTextBox控件的線程信息(也就是前一種方式的同步上下文),然後讓Invoke回調的辦法在該線程上運轉):

private void btnClick_Click(object sender, EventArgs e)
    {
      this.richTextBox1.Clear();
      btnClick.Enabled = false;
      AsyncMethodCaller caller = new AsyncMethodCaller(TestMethod);
      IAsyncResult result = caller.BeginInvoke(GetResult, null);

      //// 捕獲調用線程的同步上下文派生對象
      //sc= SynchronizationContext.Current;
    }

    # region 運用APM完成異步編程
    // 同步辦法
    private string TestMethod()
    {    
      // 模仿做一些耗時的操作
      // 實踐項目中能夠是讀取一個大文件或許從近程服務器中獲取數據等。
      for (int i = 0; i < 10; i++)
      {
        Thread.Sleep(200);
      }

      return "點擊我按鈕事情完成";
    }

    // 回調辦法
    private void GetResult(IAsyncResult result)
    {
      AsyncMethodCaller caller = (AsyncMethodCaller)((AsyncResult)result).AsyncDelegate;
      // 調用EndInvoke去等候異步伐用完成並且取得前往值
      // 假如異步伐用尚未完成,則 EndInvoke 會不斷阻止調用線程,直到異步伐用完成
      string resultvalue = caller.EndInvoke(result);
      //sc.Post(ShowState,resultvalue);
      richTextBox1.Invoke(showStateCallback, resultvalue);
    }

    // 顯示後果到richTextBox
    private void ShowState(object result)
    {
      richTextBox1.Text = result.ToString();
      btnClick.Enabled = true;
    }

    // 顯示後果到richTextBox
    //private void ShowState(string result)
    //{
    //  richTextBox1.Text = result;
    //  btnClick.Enabled = true;
    //}
    #endregion

運轉的後果為:

C# 5.0 提供的async和await使異步編程更復雜

下面局部演示了運用傳統的異步編程模型(APM)來處理同步代碼所存在的問題,但是在.NET 2.0,.NET 4.0和.NET 4.5中,微軟都有推出新的方式來處理同步代碼的問題,他們辨別為基於事情的異步形式,基於義務的異步形式和提供async和await關鍵字來對異步編程支持。關於前兩種異步編程形式,在我後面的文章中都有引見,大家可以檢查相關文章停止詳細地理解,本局部就C# 5.0中的async和await這兩個關鍵字如何完成異步編程的問題來給大家引見下。上面經過代碼來理解下如何運用async和await關鍵字來完成異步編程,並且大家也可以參看後面的博客來比照了解運用async和await是異步編程更復雜。

private async void btnClick_Click(object sender, EventArgs e)
    {
      long length = await AccessWebAsync();

      // 這裡可以做一些不依賴回復的操作
      OtherWork();

      this.richTextBox1.Text += String.Format("\n 回復的字節長度為: {0}.\r\n", length);
      txbMainThreadID.Text = Thread.CurrentThread.ManagedThreadId.ToString();
    }

    // 運用C# 5.0中提供的async 和await關鍵字來定義異步辦法
    // 從代碼中可以看出C#5.0 中定義異步辦法好像定義同步辦法一樣復雜。
    // 運用async 和await定義異步辦法不會創立新線程,
    // 它運轉在現有線程上執行多個義務.
    // 此時不知道大家有沒有一個疑問的?在現有線程上(即UI線程上)運轉一個耗時的操作時,
    // 為什麼不會梗塞UI線程的呢?
    // 這個問題的答案就是 當編譯器看到await關鍵字時,線程會
    private async Task<long> AccessWebAsync()
    {
      MemoryStream content = new MemoryStream();

      // 對MSDN發起一個Web懇求
      HttpWebRequest webRequest = WebRequest.Create("http://msdn.microsoft.com/zh-cn/") as HttpWebRequest;
      if (webRequest != null)
      {
        // 前往回復後果
        using (WebResponse response = await webRequest.GetResponseAsync())
        {
          using (Stream responseStream = response.GetResponseStream())
          {
            await responseStream.CopyToAsync(content);
          }
        }
      }

      txbAsynMethodID.Text = Thread.CurrentThread.ManagedThreadId.ToString() ;
      return content.Length;
    }

    private void OtherWork()
    {
      this.richTextBox1.Text += "\r\n等候服務器回復中.................\n";
    }

運轉後果如下:

async和await關鍵字分析

我們比照下下面運用async和await關鍵字來完成異步編程的代碼和在第二局部的同步代碼,有沒有發現運用async和await關鍵字的異步完成和同步代碼的完成很像,只是異步完成中多了async和await關鍵字和調用的辦法都多了async後綴而已。正是由於他們的完成很像,所以我在第四局部才命名為運用async和await使異步編程更復雜,好像我們在寫同步代碼一樣,並且代碼的coding思緒也是和同步代碼一樣,這樣就防止思索在APM中委托的回調等復雜的問題,以及在EAP中思索各種事情的定義。

從代碼局部我們可以看出async和await的運用的確很復雜,我們就如在寫同步代碼普通,但是我很想知道編譯器究竟給我們做了怎樣的處置的?並且從運轉後果可以發現,運轉異步辦法的線程和GUI線程的ID是一樣的,也就是說異步辦法的運轉在GUI線程上,所以就不必像APM中那樣思索跨線程訪問的問題了(由於經過委托的BeginInvoke辦法來停止回調辦法時,回調辦法是在線程池線程上執行的)。上面就用反射工具看看編譯器把我們的源碼編譯成什麼樣子的:

// 編譯器為按鈕Click事情生成的代碼
private void btnClick_Click(object sender, EventArgs e)
{
  <btnClick_Click>d__0 d__;
  d__.<>4__this = this;
  d__.sender = sender;
  d__.e = e;
  d__.<>t__builder = AsyncVoidMethodBuilder.Create();
  d__.<>1__state = -1;
  d__.<>t__builder.Start<<btnClick_Click>d__0>(ref d__);
}

看到下面的代碼,作為順序員的我想說——編譯器你怎樣可以這樣呢?怎樣可以恣意竄改我的代碼呢?這樣不是進犯我的版權了嗎?你要改最最少應該通知我一聲吧,假如我的源碼看到它在編譯器中的完成是下面那樣的,我置信我的源碼會說——難道我中了人間上最狠毒的改頭換面腳嗎? 好吧,為了讓大家更好天文清編譯器面前究竟做了什麼事情,上面就順著下面的代碼摸瓜,我也來展現耍一套還我漂漂拳來協助大家找到編譯器代碼和源碼的對應關系。我的剖析思緒為:

1、提出問題——我的click事情的源碼到哪裡去了呢?

從編譯器代碼我們可以看到,後面的7句代碼都是對某個類停止賦值的操作,最真正起作用的就是最後Start辦法的調用。這裡又發生了幾個疑問——d__0是什麼類型? 該類型中的<>t__builder字段類型的Start辦法究竟是做什麼用的? 有了這兩個疑問,我們就點擊d__0(反射工具可以讓我們直接點擊檢查)來看看它是什麼類型   

// <btnClick_Click>d__0類型的定義,從上面代碼可以看出它是一個構造體
// 該類型是編譯器生成的一個嵌入類型
// 看到該類型的完成有沒有讓你聯想到什麼?
private struct <btnClick_Click>d__0 : IAsyncStateMachine
{
  // Fields
  public int <>1__state;
  public Form1 <>4__this;
  public AsyncVoidMethodBuilder <>t__builder;
  private object <>t__stack;
  private TaskAwaiter<long> <>u__$awaiter2;
  public long <length>5__1;
  public EventArgs e;
  public object sender;

  // Methods
  private void MoveNext()
  {
    try
    {
      TaskAwaiter<long> CS$0$0001;
      bool <>t__doFinallyBodies = true;
      switch (this.<>1__state)
      {
        case -3:
          goto Label_010E;

        case 0:
          break;

        default:
            // 獲取用於等候Task(義務)的等候者。你要知道某個義務能否完成,我們就需求一個等候者對象對該義務停止一個監控,所以微軟就定義了一個等候者對象的
            // 從這裡可以看出,其實async和await關鍵字面前的完成原理是基於義務的異步編程形式(TAP)
          // 這裡代碼是在線程池線程上運轉的
          CS$0$0001 = this.<>4__this.AccessWebAsync().GetAwaiter();
            // 假如義務完成就調轉到Label_007A局部的代碼
          if (CS$0$0001.IsCompleted)
          {
            goto Label_007A;
          }
           
          // 設置形態為0為了加入回調辦法。
          this.<>1__state = 0;
          this.<>u__$awaiter2 = CS$0$0001;
            // 這個代碼是做什麼用的呢?讓我們帶著問題看上面的剖析
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<long>, Form1.<btnClick_Click>d__0>(ref CS$0$0001, ref this);
          <>t__doFinallyBodies = false;
            // 前往到調用線程,即GUI線程,這也是該辦法不會梗塞GUI線程的緣由,不論義務能否完成都前往到GUI線程
          return;
      }
      // 當義務完成時,不會執行上面的代碼,會直接執行Label_007A中代碼
      CS$0$0001 = this.<>u__$awaiter2;
      this.<>u__$awaiter2 = new TaskAwaiter<long>();
      // 為了使再次回調MoveNext代碼
      this.<>1__state = -1;
    Label_007A:
      // 上面代碼是在GUI線程上執行的
      CS$0$0001 = new TaskAwaiter<long>();
      long CS$0$0003 = CS$0$0001.GetResult();
      this.<length>5__1 = CS$0$0003;
        // 我們源碼中的代碼這裡的
      this.<>4__this.OtherWork();
      this.<>4__this.richTextBox1.Text = this.<>4__this.richTextBox1.Text + string.Format("\n 回復的字節長度為: {0}.\r\n", this.<length>5__1);
      this.<>4__this.txbMainThreadID.Text = Thread.CurrentThread.ManagedThreadId.ToString();
    }
    catch (Exception <>t__ex)
    {
      this.<>1__state = -2;
      this.<>t__builder.SetException(<>t__ex);
      return;
    }
  Label_010E:
    this.<>1__state = -2;
    this.<>t__builder.SetResult();
  }

  [DebuggerHidden]
  private void SetStateMachine(IAsyncStateMachine param0)
  {
    this.<>t__builder.SetStateMachine(param0);
  }
}

假如你看過我的迭代器專題的話,置信你一定可以聯想到該構造體就是一個迭代器的一個完成,其次要辦法就是MoveNext辦法。

從下面的代碼的正文應該可以協助我們處理在第一步提到的第一個問題,即<btnClick_Click>d__0是什麼類型,上面就剖析下第二個問題,從<btnClick_Click>d__0構造體的代碼中可以發現<>t__builder的類型是AsyncVoidMethodBuilder類型,上面就看看它的Start辦法的解釋——運轉關聯形態機的生成器,即調用該辦法就可以開端運轉形態機,運轉形態機指的就是執行MoveNext辦法(MoveNext辦法中有我們源碼中一切代碼,這樣就把編譯器生成的Click辦法與我們的源碼關聯起來了)。

從下面代碼正文中可以發現,當該MoveNext被調用時會立刻還回到GUI線程中,同時也有這樣的疑問——剛開端調用MoveNext辦法時,義務一定是還沒有被完成的,但是我們輸入我們源碼中的代碼,必需等候義務完成(由於義務完成才干調轉到Label_007A中的代碼),此時我們應該需求回調MoveNext辦法來反省義務能否完成,(就如迭代器中的,我們需求運用foreach語句不斷調用MoveNext辦法),但是我們在代碼卻沒有找到回調的任何代碼啊?

關於這個疑問,回調MoveNext辦法一定是存在的,只是初次看下面代碼的冤家還沒有找到相似的語句而已,下面代碼正文中我提到了一個問題——這個代碼是做什麼用的呢?讓我們帶著問題看上面的剖析,其實正文上面的代碼就是起到回調MoveNext辦法的作用,AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted<TAwaiter, TStateMachine> 辦法就是調度形態機去執行MoveNext辦法,從而也就處理了回調MoveNext的疑問了。

置信大家從下面的解釋中可以找到源碼與編譯器代碼之間的對應關系了吧, 但是我在剖析完下面的之後,又有一個疑問——當義務完成時,是如何加入MoveNext辦法的呢?總不能讓其不斷回調吧,從下面的代碼的正文可以看出,當義務執行完成之後,會把<>1__state設置為0,當下次再回調MoveNext辦法時就會直接加入辦法,但是義務沒完成之前,異樣也會把<>1__state設置為0,但是Switch局部前面的代碼又把<>1__state設置為-1,這樣就保證了在義務沒完成之前,MoveNext辦法可以被反復回調,當義務完成之後,<>1__state設置為-1的代碼將不會執行,而是調轉到Label_007A局部。

經過下面的剖析之後,置信大家也可以耍出一套還我漂漂拳去剖析異步辦法AccessWebAsync(),其剖析思緒是和btnClick_Click的剖析思緒是一樣的.這裡就不反復啰嗦了。

剖析完之後,上面再分享下幾個關於async和await常問的問題

問題一:是不是寫了async關鍵字的辦法就代表該辦法是異步辦法,不會梗塞線程呢?

答: 不是的,關於只標識async關鍵字的(指在辦法內沒有呈現await關鍵字)的辦法,調用線程會把該辦法當成同步辦法一樣執行,所以但是會梗塞GUI線程,只要當async和await關鍵字同時呈現,該辦法才被轉換為異步辦法處置。  

問題二:“async”關鍵字會招致調用辦法用線程池線程運轉嗎?

答: 不會,被async關鍵字標識的辦法不會影響辦法是同步還是異步運轉並完成,而是,它使辦法可被聯系成多個片段,其中一些片段能夠異步運轉,這樣這個辦法能夠異步完成。這些片段界線就呈現在辦法外部顯示運用”await”關鍵字的地位處。所以,假如在標志了”async”的辦法中沒有顯示運用”await”,那麼該辦法只要一個片段,並且將以同步方式運轉並完成。在await關鍵字呈現的後面局部代碼和前面局部代碼都是同步執行的(即在調用線程上執行的,也就是GUI線程,所以不存在跨線程訪問控件的問題),await關鍵處的代碼片段是在線程池線程上執行。總結為——運用async和await關鍵字完成的異步辦法,此時的異步辦法被分紅了多個代碼片段去執行的,而不是像之前的異步編程模型(APM)和EAP那樣,運用線程池線程去執行一整個辦法。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支持。  

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved