2015 年 11 月

第 30 卷,第 12 期

本文章是由機器翻譯。

同步程式設計 - 從頭開始同步

Mark Sowul

最新版本的 Microsoft.NET Framework 簡化比以往撰寫回應、 高效能的應用程式透過 async 和 await 關鍵字 — 它是說它們已變更的方式沒有誇張我們.NET 開發人員撰寫的軟體。用來要求山寨 web 的巢狀回呼的非同步程式碼可以現在是撰寫 (及了解!) 幾乎輕鬆地為循序的同步程式碼。

沒有足夠材料已經建立和使用非同步方法,因此我會假設您熟悉基本概念。如果您不是,Visual Studio 文件頁面上的 msdn.com/async 可以讓您加速。

大部分的非同步處理的相關文件會警告您就無法將非同步方法插入現有的程式碼。呼叫端本身必須非同步處理。Lucian Wischik,Microsoft 語言小組的開發人員的"Async 就像是廢止病毒。 」 因此您要如何建置非同步到您的應用程式,直接從一開始就非常網狀架構而不需使用非同步 void? 我為各位示範數個預設 UI 啟動程式碼、 Windows Form 和 Windows Presentation Foundation (WPF),轉換物件導向設計的 UI 重複使用並加入支援非同步/等候重整的過程。過程中,我也將說明當它並不合理使用"async void"。

在本文中主要的逐步解說著重在 Windows Form。WPF 需要可以取得令人分心的額外變更。在每個步驟中,我想先解釋與 Windows Form 應用程式變更,然後討論的 WPF 版本所需的任何差異。我在本文中示範基本程式碼的所有變更但您可以看到已完成的範例 (及中繼修訂) 隨附的線上程式碼下載中這兩種環境。

第一步

Windows Form 和 WPF 應用程式的 Visual Studio 範本並不真正的能見度在啟動期間使用 async (或一般自訂啟動程序)。雖然 C# 致力於是物件導向語言 — 所有程式碼必須在類別 — 預設啟動程式碼會將邏輯放在與主要的靜態方法或過於複雜主要表單的建構函式中向開發人員。(不對它不是個不錯的主意 MainForm 建構函式內資料庫的存取。而且沒錯,我就看過它完成。) 這種情況下一直有問題,但現在使用 async,這也表示沒有清楚的機會可以讓應用程式以非同步方式初始化本身。

若要開始,我會建立新的專案與 Visual Studio 中的 Windows Form 應用程式範本。[圖 1 Program.cs 中顯示其預設啟動程式碼。

[圖 1 預設的 Windows Form 啟始程式碼

static class Program
{
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main()
  {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
  }
}

您不使用 WPF 一樣簡單。預設 WPF 啟動是相當不透明,並甚至尋找任何程式碼來自訂很困難。一些初始化程式碼置於 Application.OnStartup,但如何將您延遲顯示 UI 直到您已經載入必要的資料嗎? 我需要使用 WPF 第一件事是將啟動程序公開為可編輯的程式碼。我會相同的起始點做為 Windows Form 的 WPF 與再發行項的每個步驟都很相似。

在 Visual Studio 中建立新的 WPF 應用程式之後, 我建立一個新的類別,使用中的程式碼呼叫程式 [圖 2。若要取代預設的啟動順序,開啟專案屬性並將啟始物件從"App"變更為新建立之 「 程式。 」

[圖 2 對等的 Windows Presentation Foundation 啟始程式碼

static class Program
{
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main()
  {
    App app = new App();
    // This applies the XAML, e.g. StartupUri, Application.Resources
    app.InitializeComponent();
    // Shows the Window specified by StartupUri
    app.Run();
  }
}

如果您使用 「 移至定義 」 在呼叫中的 InitializeComponent [圖 2, ,您會看到編譯器會產生對應的主要程式碼做為當您使用應用程式做為啟始物件 (這是我要怎樣可以開啟 「 黑箱 」 為您這裡)。

向物件導向的啟動

首先,我將小型重整的預設啟動程式碼推送物件導向的方向: 我會將從主要的邏輯並將它移至類別。若要這麼做,我想將程式的非靜態類別 (我說過,預設值推入您在錯誤方向) 並指定其建構函式。然後我會將安裝程式碼移至建構函式,並新增將會執行我的表單的開始方法。

我稱為新版 Program1,以及您可以看到它在 [圖 3。這個基本架構顯示的概念核心: 若要執行程式,主要現在建立的物件和在其上呼叫方法就像對任何物件導向的一般案例。

[圖 3 Program1、 物件導向的啟動開頭

[STAThread]
static void Main()
{
  Program1 p = new Program1();
  p.Start();
}
 
private readonly Form1 m_mainForm;
private Program1()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  m_mainForm = new Form1();
}
 
public void Start()
{
  Application.Run(m_mainForm);
}

減少從表單應用程式

不過,對 Application.Run 採用表單執行個體 (在我的 Start 方法中 end) 的呼叫會帶來幾個問題。其中一個是一般架構考量: 我不喜歡它繫結到顯示該表單的我的應用程式存留期。這會是許多應用程式中的 [確定] 但不是應該啟動除了或許工作列或通知區域中的圖示時顯示任何 UI 背景中執行的應用程式。我知道我看過一些,當使用者在啟動之前沒有什麼區別簡短閃爍螢幕。我的做法是其啟動程式碼會遵循類似的程序,然後他們隱藏本身儘速完成表單後載入。無可否認地,該特定的問題不需要在這裡,解決但分隔會以非同步方式初始化非常重要。

不要 Application.Run(m_mainForm),我可以使用不接受引數的執行中的多載: 啟動 UI 基礎結構而不需中斷任何特定的表單。這分離表示我自己; 顯示表單這也表示關閉表單會不會再結束應用程式中,因此我必須明確地連接的太中, 所示 [圖 4。我也會使用這個機會來新增初始化我第一次攔截程序。"Initialize"是我在我的表單類別來存放我需要初始化它,例如從資料庫或網站擷取資料的任何邏輯建立的方法。

[圖 4 Program2,訊息迴圈現在是個別從主表單

private Program2()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  m_mainForm = new Form1();
  m_mainForm.FormClosed += m_mainForm_FormClosed;
}
 
void m_mainForm_FormClosed(object sender, FormClosedEventArgs e)
{
  Application.ExitThread();
}
 
public void Start()
{
  m_mainForm.Initialize();
  m_mainForm.Show();
  Application.Run();
}

在 WPF 版本中,應用程式的 StartupUri 判斷呼叫哪些視窗以顯示當執行;您會看到它在 App.xaml 標記檔案中定義。Unsurprisingly,OnLastWindowClose 的應用程式預設 ShutdownMode 設定關閉應用程式時所有 WPF 視窗已都關閉,這是如何存留期間取得繫結在一起。(請注意這不同於 Windows Form。在 Windows Form 中如果您的主視窗會開啟子視窗並關閉只是第一個視窗就會結束應用程式。在 WPF 中,它將不會結束除非您關閉這兩個視窗。)

若要完成在 WPF 中相同的區隔,我先移除 StartupUri App.xaml。相反地,我建立自己的視窗、 將它初始化並顯示 App.Run 的呼叫之前:

public void Start()
{
  MainWindow mainForm = new MainWindow();
  mainForm.Initialize();
  mainForm.Show();
  m_app.Run();
}

當我建立應用程式時,我會設定應用程式。ShutdownMode.OnExplicitShutdown,以減少從 windows 應用程式存留期的 ShutdownMode:

m_app = new App();
m_app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
m_app.InitializeComponent();

若要完成該明確關閉,我將 MainWindow.Closed 附加事件處理常式。

當然,WPF 會更好的區隔方面的顧慮,因此可以初始化檢視模型而不是視窗本身的更有意義: 我將建立 MainViewModel 類別並建立我 Initialize 方法。同樣地,關閉應用程式要求也應該經過檢視模型,因此我要新增 「 CloseRequested 」 事件和對應的"RequestClose"方法來檢視模型。產生的 WPF 版本 Program2 會列在 [圖 5 (主要是不變,因此我此處不顯示)。

[圖 5 Program2 類別 Windows Presentation Foundation 版本

private readonly App m_app;
private Program2()
{
  m_app = new App();
  m_app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
  m_app.InitializeComponent();
}
 
public void Start()
{
  MainViewModel viewModel = new MainViewModel();
  viewModel.CloseRequested += viewModel_CloseRequested;
  viewModel.Initialize();
 
  MainWindow mainForm = new MainWindow();
  mainForm.Closed += (sender, e) =>
  {
    viewModel.RequestClose();
  };
     
  mainForm.DataContext = viewModel;
  mainForm.Show();
  m_app.Run();
}
 
void viewModel_CloseRequested(object sender, EventArgs e)
{
  m_app.Shutdown();
}

取出裝載環境

既然我已經有獨立 Application.Run 從我的表單,我想要處理另一個架構考量。現在以滑鼠右鍵、 程式類別中深內嵌應用程式。我要 「 abstract 出"這個裝載環境 』。我要從我的程式類別中移除所有應用程式上的各種 Windows Form 方法離開只將相關程式本身的功能與 Program3 中所示 [圖 6。一個最後的部分是比較不那麼直接關閉表單並關閉應用程式之間的連結是加入 program 類別上的事件。請注意如何 Program3 做為類別具有與應用程式沒有互動。

[圖 6 Program3,現在您輕鬆地隨插即用在其他地方

private readonly Form1 m_mainForm;
private Program3()
{
  m_mainForm = new Form1();
  m_mainForm.FormClosed += m_mainForm_FormClosed;
}
 
public void Start()
{
  m_mainForm.Initialize();
  m_mainForm.Show();
}
 
public event EventHandler<EventArgs> ExitRequested;
void m_mainForm_FormClosed(object sender, FormClosedEventArgs e)
{
  OnExitRequested(EventArgs.Empty);
}
 
protected virtual void OnExitRequested(EventArgs e)
{
  if (ExitRequested != null)
    ExitRequested(this, e);
}

區隔的裝載環境有幾個優點。第一,它讓測試更為容易 (您現在可以測試 Program3,有限範圍)。它也方便重複使用程式碼在其他位置、 可能是內嵌於較大的應用程式或 「 啟動 」 畫面。

低耦合的 Main 示 [圖 7— 我已經移動應用程式邏輯上一步。這種設計可讓您更容易整合 WPF 和 Windows Form 或可能是逐漸取代 Windows Form 與 WPF。範圍以外的這篇文章,但您可以在隨附的線上程式碼中找到的混合應用程式範例。如先前的重整,這些都是很棒但不是一定是很重要: "手邊的工作、 「 相關性,是它會進行非同步版本流程更自然地,您很快就會看到。

[圖 7 Main,現在能夠裝載任意的程式

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  Program3 p = new Program3();
  p.ExitRequested += p_ExitRequested;
  p.Start();
 
  Application.Run();
}
 
static void p_ExitRequested(object sender, EventArgs e)
{
  Application.ExitThread();
}

長時間等候非同步

現在,最後,保證物超所值。我可以讓方法非同步的這讓我使用 await 及進行非同步初始化的邏輯開始。根據慣例,我改名開始 StartAsync,並對 InitializeAsync 進行初始化。我也已變更其傳回型別為非同步工作:

public async Task StartAsync()
{
  await m_mainForm.InitializeAsync();
  m_mainForm.Show();
}

若要使用它,主要的變更就像這樣:

static void Main()
{
  ...
  p.ExitRequested += p_ExitRequested;
  Task programStart = p.StartAsync();
 
  Application.Run();
}

為了說明其運作方式 — 並解決細微但很重要的問題 — 我要探索的詳細資料怎麼使用非同步/等候。

Await 真正的意義: 請考慮我提出 StartAsync 方法。請務必了解 (通常),當非同步方法到達 await 關鍵字,它會傳回。就像任何方法傳回時繼續執行的執行緒。在此情況下,StartAsync 方法達到"await m_mainForm.InitializeAsync"並傳回至 Main 就會繼續呼叫 Application.Run。這會導致有點即使循序發生之後 m_mainForm.Show Application.Run 是可能 m_mainForm.Show 前, 要執行的反直覺式結果。Async 和 await 進行非同步程式設計更為容易,但仍然不方便。

這就是為什麼非同步方法傳回的作業。它是完成的工作表示非同步方法"傳回"直覺式的方面來說,也就是當所有的程式碼已執行。如果是 StartAsync,表示 InitializeAsync 和 m_mainForm.Show 已完成。而這是使用非同步 void 的第一個問題: 沒有工作物件沒有任何方法呼叫者的非同步 void 方法知道何時結束執行。

如何及何時真的其餘程式碼可以執行,如果執行緒已移和 StartAsync 已傳回給其呼叫者? 這是 Application.Run 會傳入。Application.Run 是無限的迴圈,等候要執行的工作 — 主要處理使用者介面事件。比方說,當您將滑鼠移到視窗中,或按下按鈕時,Application.Run 訊息迴圈將取消佇列事件和分派適當的程式碼在回應中,並再等候下一個事件的。它不是嚴格限制的 ui,不過: 請考慮 Control.Invoke,UI 執行緒執行的函式。Application.Run 太處理這些要求。

在此情況下完成 InitializeAsync StartAsync 方法的其餘部分將會張貼至該訊息迴圈中。當您使用 await,Application.Run 會執行其餘的方法在 UI 執行緒上就如同您已寫入使用 Control.Invoke 的回呼。(接續是否應該會顯示在 UI 執行緒是由控制 ConfigureAwait。您可以進一步了解在 Stephen cleary 在探討的 2013 年 3 月發行項中非同步的最佳作法在程式設計 msdn.com/magazine/jj991977)。

這也是如此 Application.Run 分開 m_mainForm 的重要原因。Application.Run 正在顯示: 它必須執行才能處理程式碼之後"await,",甚至之前您其實只列出任何 UI。例如,如果您嘗試從主要並放回 StartAsync 移動 Application.Run,程式將只立即結束: 一旦執行叫用 「 await InitializeAsync",控制權會回到主要並沒有執行多個程式碼,因此這是主要的結尾。

這也會說明為什麼非同步的使用會有從下往上啟動。通用但短期 antipattern 為呼叫 Task.Wait 而不是 await,因為呼叫端不是非同步方法,但很可能是它會鎖死立即。問題是 UI 執行緒將會封鎖等候該呼叫並無法處理接續。沒有接續工作將不會完成,所以等候呼叫永遠不會傳回 — 死結!

Await 和 Application.Run、 雞和短期問題: 我先前提到時發生一個微妙的問題。我所述,當您呼叫 await、 預設行為是 UI 執行緒,也就是我需要什麼這裡上繼續執行。不過,設定基礎結構的不是第一次呼叫時因為尚未尚未執行適當的程式碼的 await!

Synchronizationcontext.current 通常是以這種行為的索引鍵: 當呼叫 await 時,基礎結構會擷取值,並用它來張貼接續。這就是它在 UI 執行緒上就會繼續。同步處理內容是由 Windows Forms 或 WPF 時設定它開始執行訊息迴圈。內部 StartAsync 尚未尚未發生: 如果您檢查 SynchronizationContext.Current StartAsync 開始,您會看到為 null。如果沒有同步處理內容,await 會反而張貼到執行緒集區的接續和不那 UI 執行緒,因為它不會運作。

WPF 版本將會停止回應被允許,但其實,Windows Form 版本將"意外"運作。根據預設,Windows Form 設定同步處理內容的第一個控制項建立時 — 在此情況下,當我建構的 m_mainForm (WindowsFormsSynchronizationContext.AutoInstall 時控制這個行為)。因為"await InitializeAsync"就會發生在建立表單之後,我會是 [確定]。我將 await 呼叫 之前 建立 m_mainForm,不過,我會有同樣的問題。解決方法是設定同步處理內容自己剛開始,如下所示:

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  SynchronizationContext.SetSynchronizationContext(
    new WindowsFormsSynchronizationContext());
 
  Program4 p = new Program4();
  ... as before
}

WPF、 對等的呼叫是:

SynchronizationContext.SetSynchronizationContext(
  new DispatcherSynchronizationContext());

處理例外狀況

接近完美! 但仍有延遲的另一個問題的應用程式根目錄: 如果 InitializeAsync 引發例外狀況,程式便不會進行處理。ProgramStart 工作物件將會包含例外狀況資訊,但沒有正在執行與它和我的應用程式將處於一種 purgatory 因此。如果我無法"await StartAsync,"我無法攔截例外狀況在 Main 中,但是我不能使用 await,因為主要不是非同步。

這說明非同步 void 的第二個問題: 任何方法正確地攔截因為呼叫端沒有存取權的工作物件非同步 void 方法所擲回的例外狀況。(因此當 應該 使用非同步 void 嗎? 非同步 void 應限制為事件處理常式通常就會說的一般指引。我之前有提到 2013 年 3 月文章討論這太;建議您閱讀它充分利用非同步/等候。)

在正常的情況下 TaskScheduler.UnobservedException 應付引發不連續處理的例外狀況的工作。問題在於,它具有不保證執行。在此情況下,就幾乎肯定不會: 時便會完成這類工作的工作排程器偵測到未觀察到的例外狀況。最終處理就會發生只有當記憶體回收行程執行。它需要滿足更多記憶體的要求時,才會執行記憶體回收行程。

您可能會看到這得: 在此情況下,例外狀況會導致坐在不進行任何動作,因此它不會要求更多記憶體,讓記憶體回收行程將不會執行的應用程式。結果是應用程式將會停止回應。事實上,這就是為什麼如果您沒有指定同步處理內容的 WPF 版本懸置: WPF 視窗建構函式會擲回例外狀況因為在非 UI 執行緒上建立一個視窗,然後進入未處理的例外狀況。最後奉勸,就是要應付 programStart 項工作中,並加入將會發生錯誤時執行的接續。在本例中是合理結束如果應用程式無法初始化本身。

我無法使用因為它不是非同步,但是我可以建立新的非同步方法僅供公開 (及處理) 目的 Main 中等候非同步啟動期間擲回任何例外狀況: 它將包含只使用 try/catch await 周圍。因為這個方法會處理所有例外狀況並不擲回任何新的是另一個有限的情況下非同步 void 合理的位置:

private static async void HandleExceptions(Task task)
{
  try
  {
    await task;
  }
  catch (Exception ex)
  {
    ...log the exception, show an error to the user, etc.
    Application.Exit();
  }
}

主要使用它,如下所示:

Task programStart = p.StartAsync();
HandleExceptions(programStart);
Application.Run();

當然,通常是比較麻煩的問題 (如果非同步/等候使事情更簡單,您可以想像如何硬以前)。前面所說的 通常, 非同步方法到達以等候它傳回呼叫和該方法的其餘部分執行做為接續時。在某些情況下,不過工作才能完成同步。如果是這樣,執行的程式碼不會取得分解,也就是效能優勢。如果發生這種情況這裡,不過這表示該 HandleExceptions 方法會執行完整且會傳回,並 Application.Run 會遵循它: 在此情況下,如果例外狀況現在 Application.Exit 的呼叫會發生 之前 Application.Run,和它的呼叫將不會有任何影響。

我想要做為強制執行做為接續 HandleExceptions: 我需要先確定該我"落入"Application.Run 之前進行任何動作。如此一來,如果例外狀況,我知道已經執行 Application.Run Application.Exit 會正確地中斷它。Task.Yield 這樣寫的: 它會強制目前的非同步程式碼路徑來產生給其呼叫者,然後繼續做為接續。

以下是 HandleExceptions 更正:

private static async void HandleExceptions(Task task)
{
  try
  {
    // Force this to yield to the caller, so Application.Run will be executing
    await Task.Yield();
    await task;
  }
  ...as before

在此情況下,當我呼叫"await Task.Yield",HandleExceptions 會傳回和 Application.Run 會執行。然後將目前 SynchronizationContext,這表示它將會收取 Application.Run 來做為接續公佈 HandleExceptions 的其餘部分。

順帶一提,我認為 Task.Yield 是很好的辦法就測試的了解非同步/等候: 如果您了解 Task.Yield 使用,然後您可能有詳實的了解非同步/等候的運作方式。

保證物超所值

現在一切都運作,就有一些有趣的時間: 我要顯示而不需執行的不同執行緒上加入回應的啟動顯示畫面是多麼的容易。有趣的或不、 啟動顯示畫面是如果您的應用程式不會"start"立即相當重要: 如果使用者啟動應用程式並不會看到幾秒發生的任何項目,這是不良的使用者經驗。

啟動另一個執行緒為啟動顯示畫面效率不佳,而且也是笨重 — 您必須正確地在執行緒之間的所有呼叫封送都處理。提供進度資訊在啟動顯示畫面是因此困難,並甚至關閉它需要呼叫來叫用或同等權限。此外,當最後不會關閉啟動顯示畫面,通常它不會正確焦點放主表單,因為您無法設定啟動顯示畫面和主表單之間擁有權如果它們位於不同的執行緒。比較中顯示的非同步版本的簡單性, [圖 8

[圖 8 StartAsync 中加入啟動顯示畫面

public async Task StartAsync()
{
  using (SplashScreen splashScreen = new SplashScreen())
  {
    // If user closes splash screen, quit; that would also
    // be a good opportunity to set a cancellation token
    splashScreen.FormClosed += m_mainForm_FormClosed;
    splashScreen.Show();
 
    m_mainForm = new Form1();
    m_mainForm.FormClosed += m_mainForm_FormClosed;
    await m_mainForm.InitializeAsync();
 
    // This ensures the activation works so when the
    // splash screen goes away, the main form is activated
    splashScreen.Owner = m_mainForm;
    m_mainForm.Show();
 
    splashScreen.FormClosed -= m_mainForm_FormClosed;
    splashScreen.Close();
  }
}

總結

我已經示範如何將物件導向設計套用到您的應用程式啟動程式碼 — 無論 Windows Forms 或 WPF — 因此可以輕鬆支援非同步初始化。我也說明了如何克服一些細微的問題可能是來自非同步啟動程序。至於實際上讓您初始化非同步,我很害怕您在自己的但您會發現在一些指引 msdn.com/async

啟用使用 async 和 await 是個開端。現在該程式是較為物件導向、 其他功能變得更容易實作。我可以在 Program 類別上呼叫適當的方法來處理命令列引數。我可以讓使用者登入之前顯示主視窗。我可以啟動通知區域中的應用程式而不會顯示在啟動任何視窗。如往常般物件導向設計提供機會來擴充和重複使用程式碼中的功能。


Mark Sowul可能實際上為 (因此同仁 speculate) 以 C# 撰寫的軟體模擬。開始之後的忠實的.NET 開發人員、 Sowul 分享他豐富的.NET 和 Microsoft SQL Server 中透過他紐約諮商,SolSoft 解決方案架構和效能的專業知識。與他連絡 mark@solsoftsolutions.com, ,他偶爾的電子郵件在軟體 insights 上註冊 eepurl.com/_K7YD

感謝以下的微軟技術專家對本文的審閱: Stephen cleary 在探討與 James McCaffrey