非同步程式設計

全新的 Visual Studio 的非同步 CTP 使非同步程式設計更得心應手

Eric Lippert

 

假設有世界會像如果人們的工作時電腦程式相同的方式:

 

void ServeBreakfast(Customer diner)
{
  var order = ObtainOrder(diner);
  var ingredients = ObtainIngredients(order);
  var recipe = ObtainRecipe(order);
  var meal = recipe.Prepare(ingredients);
  diner.Give(meal);
}

每個副程式,當然分解進一步 正在準備餐可能牽涉到加熱至片頭烹飪 omelets 和 toasting 麵包。 若要執行這一類的工作,例如典型的電腦程式是人類,我們會仔細記下所有項目為階層式工作檢查清單中的序列,並 obsessively 確定 [每項工作已完成之前在下一個 embarking。

似乎合理的副程式為主的方法,取得訂單之前,您不能庫克蛋,但事實上它同時會浪費時間,讓應用程式會出現停止回應。 因為您想要被 toasting,雖然蛋是 frying,不在蛋完之後,越來越冷麵包,實在浪費時間。 因為另一個客戶到達時仍烹飪目前的順序,如果您想要花他的順序,不讓他等門口目前客戶載入之後其早餐出現停止回應。 Slavishly 下列檢查清單的伺服器並沒有任何能夠及時回應意外事件。

一個解決方法: 雇用更多的人員進行更多的執行緒

讓某人早餐富有範例中,但事實上,當然會有任何作用。 每次您到長時間執行副程式中傳出控制權,UI 執行緒上,UI 會變成完全沒有回應,直到副程式完成為止。 如何可能是很否則? 應用程式回應 UI 事件在 UI 執行緒上執行程式碼,該執行緒正在 obsessively 忙著處理其他項目。 只有在清單上的每個工作完成時,才將它取得的挫敗 
user 的佇列上命令的處理。 此問題常見的解決方案是使用並行存取要兩個或多個項目 」 在相同時間 」。(如果兩個執行緒在兩個獨立的處理器,它們可能真正執行一次。 在具有更多的執行緒,讓專屬於他們的處理器比世界裡,作業系統會模擬相同次並行定期排程為每個執行緒來控制處理器劃分使用時段。)

建立執行緒集區,並將每個新的用戶端指派特定的執行緒來處理它的要求可能是一個並行的解決方案。 在我們比方說,您可以雇用一群伺服器。 新的大來傳入時,會以新的大來指派閒置的伺服器。 然後每個伺服器個別會接受訂單、 尋找這些因素,烹飪食物和服務它的工作。

這種方法的困難之處是 UI 事件通常扺達的相同執行緒上,並且希望在該執行緒上進行完整。 大部分的 UI 元件在 UI 執行緒上建立要求,並且希望使用只能在該執行緒上進行通訊。 讓新的執行緒專屬於每個 UI 相關的工作不太可能無法正常運作。

若要解決這個問題,可以有單一的前景執行緒聆聽 UI 的事件,不執行任何動作,但 「 取得訂單 」,伺服它們陣列至一或多個背景工作執行緒。。 在此類推法中,沒有使用者互動與客戶和廚房的 cooks 實際執行要求的工作只能有一個伺服器。 在 UI 執行緒和背景工作執行緒就負責協調他們的通訊。 Cooks 不談直接到 diners,但設法食物取得還是服務。

當然已解決 」 及時的 UI 事件回應 」 的問題,但它並不會解析缺乏效率。背景工作執行緒上執行的程式碼繼續等待同步至完全庫克在烤麵包之前蛋。 該問題無法解決甚至新增了依序多個並行: 您可以讓每個順序,一個用於蛋,一個用於吐司的兩個 cooks。 但是,可能會得到非常耗資源。 只要多少 cooks 是您將會需要,與他們必須協調它們的工作時,會發生什麼事?

這種並行介紹許多已知的問題。 首先,執行緒是非常可能重量級 預設情況下一個執行緒會使用其堆疊和許多其他系統資源的虛擬記憶體的百萬個位元組。 第二,UI 物件通常的"affinitized"至 UI 執行緒,而且無法呼叫從背景工作執行緒。背景工作執行緒與 UI 執行緒必須讓 UI 執行緒可以傳送所需的資訊從 UI 項目上背景工作,而背景工作可以傳送更新至 UI 執行緒,而不是 UI 項目的直接一些複雜的排列方式出現。 這種安排是困難的程式碼,而且容易產生競爭情況、 死結 (死結) 和其他執行緒的問題。 我們都需要您在單一執行緒的世界裡倒塌 fictions 的第三個,多-例如讀取和寫入的記憶體可預測且一致的順序發生-已不可靠。 這會導致很難重現錯誤的最差的類型。

它只是好像有誤需要使用大鎚的執行緒為基礎的並行建置簡單的程式保持回應更靈敏且有效率地執行。 設法真正的人員會設法解決複雜的問題,同時又能回應事件。 在真實世界中,您不需要配置每個資料表的一個等候者] 或 [每份訂單服務好幾十個客戶所要求的兩個 cooks 所有暫止一次。 解決上述問題會太多的 cooks。 那里了是更好的解決方案,不需要這麼多的並行存取。

方案 2: 開發與 DoEvents 注意不足長

一般非並行 「 解決方案 」 問題的長期執行作業期間的 UI 一堆是自由 sprinkle 程式周圍的魔法字 Application.DoEvents,直到問題解決了。 當然,這是為了顧及現實的解決方案,但不是很井 engineered:

void ServeBreakfast(Customer diner)
{
  var order = ObtainOrder(diner);
  Application.DoEvents();
  var ingredients = ObtainIngredients(order);
  Application.DoEvents();
  var recipe = ObtainRecipe(order);
  Application.DoEvents();
  var meal = recipe.Prepare(ingredients);
  Application.DoEvents();
  diner.Give(meal);
}

使用 DoEvents 基本上,表示 「 請參閱任何有趣的內容發生在我忙著處理的最後一個項目。。 如果發生,我需要回應、 記得我做什麼不僅指現在,處理新的情況下,然後回到我先前離開的地方。它可讓程式運作起來有注意不足混雜: 出現的任何新立即取得注意。 聽起來好像方案來加強回應,而且有時候甚至可 — 但是有一些問題,這個方法。

首先,DoEvents 適合時的延遲因迴圈必須執行許多次,但每個個別的迴圈執行短。 藉由檢查擱置事件迴圈的每個幾次,您可以保持回應性,即使整個迴圈需要長時間執行。 不過,該模式通常不是回應問題的原因。 頻率問題被因花不少時間,例如嘗試同步透過高延遲網路存取檔案的一個原本就長期執行作業。 可能是在我們的範例長時間執行準備餐及工作將會幫助 DoEvents 沒有地方。 可能是位置 DoEvents 會幫助,但是它是在您不需要的程式碼的方法。

第二,呼叫 DoEvents 會嘗試完整服務程式所有 較新的事件 之前完成先前的事件相關聯的工作。 想像一下,是否沒有人可以取得直到他餐之後每一位客戶都是有他餐! 如果越來越多的客戶保持抵達,第一個客戶可能會永遠到不了他餐耗竭所導致。 事實上,它可能會發生沒有客戶取得其提供。 與先前的事件相關的工作完成可推入任意目前與未來服務較新的事件持續發生中斷之前的事件所進行的工作。

第三,DoEvents 會造成未預期的重新進入的真實風險。 也就是同時提供一位客戶您檢查看看是否有任何新的有趣 UI 事件並不小心在即使他已經正在處理,啟動 [一次,提供相同的大來。 大部分的開發人員沒有設計程式碼來偵測這種類型的重新進入。您仍可得到一些很奇怪的程式狀態中確實當演算法不想要遞迴最後會透過 DoEvents 意外地呼叫它自己。

簡單來說,DoEvents 應該只能用來修正回應問題在最簡單的情況下。它不是很好的解決方案,以管理複雜的程式中的 UI 回應。

解決三個方法: 將您的檢查清單內到外回呼

非並行性質 DoEvents 技術是很吸引人,但顯然不完全對正確的方案,複雜的程式。 更好的作法是細分為一系列的檢查清單上的每一個都可以完成不夠快速應用程式可以顯示為回應事件的簡短工作項目。

這個主意是什麼新 分割成小部分的複雜問題就是為什麼我們一開始就有的副程式。 有趣的變化,代替干預執行檢查清單,來判斷項目已完成,以及所需進行接下來,關閉,而且只會傳回給呼叫者的所有項目完成時,控制項就是提供每個新的工作,必須放在後面的工作清單。 必須放在特定的工作完成後的工作稱為 「 接續 」 的工作。

當工作完成時,它就可以檢視註解的接續,並完成該處。 它可以安排註解的接續,稍後再執行。 如果註解的接續要求計算出來的上一個工作的資訊,請的上一個工作可以叫用作為接續上呼叫就做為引數傳遞該資訊。

使用這個方法,總工作的本文是基本上分成零散的部分,每個可執行快速。 系統會顯示回應因為暫止事件可以偵測並處理之間的任何兩個小部分的工作執行。 但也分成小的組件和排入佇列,稍後再執行任何與這些新的事件相關的活動,因為我們不需要新的工作,讓舊的工作無法完成耗竭 「 」 問題。 長時間執行的新任務不會立即處理過,但它們佇列中等待最終處理。

這個想法棒,但是它是完全不清楚如何實作這種解決方案。 不可或缺的困難度會決定如何識別每個小的工作單位,其註解的接續為何。 也就是說,哪些工作必須進入下一步]。

在傳統的非同步程式碼,這通常是註冊"回呼 」 函式。 假設我們有 「 準備 」,會顯示下一個步驟將回呼函式的非同步版本-也就是提供餐:

void ServeBreakfast(Diner diner)
{
  var order = ObtainOrder(diner);
  var ingredients = ObtainIngredients(order);
  var recipe = ObtainRecipe(order);
  recipe.PrepareAsync(ingredients, meal =>
    {
      diner.Give(meal);
    });
}

現在傳回 ServeBreakfast 立即 PrepareAsync 傳回值之後呼叫 ServeBreakfast 的任何程式碼就可用來服務就會發生其他事件。 PrepareAsync 沒有任何 「 真正 」 工作本身。相反地,快速的話無論是為了確保餐會在未來做好準備。 此外,PrepareAsync 也可確保回呼方法會在被叫用與準備餐做為引數餐準備工作完成之後的某個時間。 因此,大來將最後會服務,雖然她可能會有短暫等候如果有需要注意的準備工作結束時間與餐的服務之間的事件。

請注意, 這一定是牽涉到第二個執行緒。 PrepareAsync 可能會導致餐一些準備工作,若要在個別執行緒上執行,或可能會導致一系列簡短餐準備佇列等候 UI 執行緒,以便稍後執行相關聯的工作。 其實並不重要。 所有我們知道是 PrepareAsync 設法保證兩件事: 餐將不會封鎖 UI 執行緒與高延遲的操作,以準備好,並準備要求的餐的工作完成之後,某種方式將叫用回呼。

但假設任何的方法,以取得訂單,取得這些因素,取得此配方,或準備餐可能會變慢的 UI 的一個。 如果有兩種方法的非同步版本,我們可以解決這個較大的問題。 所產生的程式外觀? 請記住,每一種方法必須要賦予該怎麼辦完成的工作單元告知的回呼:

void ServeBreakfast(Diner diner)
{
  ObtainOrderAsync(diner, order =>
  {
    ObtainIngredientsAsync(order, ingredients =>
    {
      ObtainRecipeAsync(order, recipe =>
      {
        recipe.PrepareAsync(ingredients, meal =>
        {
          diner.Give(meal);
        })})})});
}

這可能會看起來像是短短的狀況,但它的任何相較於真正的方式不正確程式時,取得它們重新撰寫使用回呼基礎非同步。 請考慮您如何處理進行迴圈非同步的或處理的例外狀況,請試著最後區塊或其他非一般的形式的控制流程的方式。 所得到的本質上開啟您的程式內到外。程式碼現在強調如何所有回呼都連接在一起,並不該程式的邏輯工作流程應該是什麼。

解決四個方法: 請解決的問題以工作為基礎的非同步編譯器

回撥為基礎的非同步不會保留 UI 執行緒的回應,並浪費在同步等待完成長時間執行工作的時間降至最低。 但是,修復看起來比疾病差。 價格您支付回應速度和效能是您必須撰寫程式碼,強調如何機制的非同步工作時遮蔽的意義和用途的程式碼。

C# 和 Visual Basic 的未來版本而是讓您撰寫的編譯器來建置必要機制讓您在幕後進行足夠的提示時,強調其意義與用途的程式碼。 方案包含兩個部分: 一型別系統,而另一個在 語言

CLR 4 發行定義的型別工作 <T> — workhorse 類型的工作平行程式庫 (TPL),來代表概念 「 即將以後產生的結果型別 t 的某些工作 」。非泛型的任務類型會以 「 未來將會完成,但沒有傳回結果的工作 」 的概念。

精確型別 t 的結果要如何在未來產生是實作詳細資料的特定工作。分散在伺服工作可能陣列另一台電腦完全,另一個執行緒,這台機器,處理程序,或可能是工作只是讀取先前快取的結果,可以從目前的執行緒廉價地存取。 分散在伺服 TPL 工作通常陣列與背景工作執行緒從執行緒集區中目前的處理序,但該實作詳細資料不是 「 <T>。 」 工作的基礎型別。而是工作的 <T> 可以表示任何高延遲的操作會產生 t。

方案的下半部是新的語言所等候的關鍵字。 一般方法呼叫表示"記住自己在做什麼、 執行這個方法,直到完全完成,然後選取您先前離開的地方,現在知道方法的結果。Await 運算式,相反地,表示 「 評估這個運算式,以取得物件,表示將在未來產生結果的工作。 註冊目前方法的其餘部分,為該任務的註解的接續相關聯的回呼。 一旦此工作會產生並註冊回呼, 立即我呼叫端,控制才傳回。"

更精美讀取我們重寫成的新樣式的一些範例:

async void ServeBreakfast(Diner diner)
{
  var order = await ObtainOrderAsync(diner);
  var ingredients = await ObtainIngredientsAsync(order);
  var recipe = await ObtainRecipeAsync(order);
  var meal = await recipe.PrepareAsync(ingredients);
  diner.Give(meal);
}

在這個草圖中,每個非同步的版本會傳回 <Order>,工作 < <Ingredient> 清單 > 工作等等。 每次發生的著您呢 ! 時,目前正在執行的方法訂閱方法的其餘部分為目前的工作已完成,而且立即回傳時的項目。 某種方式,將會完成本身每項工作-藉著被排程要執行事件為目前的執行緒,或因為它使用的 I/O 完成執行緒或背景工作執行緒,而其註解的接續,若要選取先前離開的地方] 執行其餘的方法也將會再原因。

請注意方法已經標記為使用新的非同步關鍵字。這是只是一個標記,讓它知道編譯器在這種內容中,關鍵字等候是被視為工作流程將控制權傳回給其呼叫者的位置,並挑選一次完成相關的任務是一個點。 也請注意我已經示範這份文件中的範例使用 C# 程式碼。Visual Basic 會有類似的功能,以類似的語法。 這些功能的 C# 和 Visual Basic 的設計是經常受到 F# 非同步工作流程,F # 已經有段時間的功能。

讓您了解更多

這個簡短的介紹只是促使,然後將新的非同步功能,在 C# 和 Visual Basic 的表面而已。 如需在幕後的運作方式,以及如何理解非同步程式碼的效能特性的詳細說明,請參閱 Mads 的 Torgersen 和史 Toub 我同事此問題中的 [小幫手] 文件]。

若要取得您的手上試用版本,請在這項功能,以及與範例、 白皮書和社群論壇問題]、 [討論區] 和 [建設性的意見反應,請移至 msdn.com/async。 這些語言功能和支援這些程式庫是仍在開發。設計小組很喜歡您的意見反應,盡量。

Eric Lippert 是主要的開發人員 C# 編譯器小組在 Microsoft。

感謝到下列的技術專家來檢閱這份文件: Mads Torgersen 史 Toub Lucian Wischik