November 2015

Volume 30 Number 12

非同期プログラミング - スタートアップから非同期に

Mark Sowul

最新バージョンの Microsoft .NET Framework では、async キーワードと await キーワードを使用すれば、応答性に優れ、パフォーマンスが高いアプリケーションをかつてないほど簡単に開発できます。これらのキーワードは .NET 開発者のソフトウェア開発方法を変えたといっても過言ではありません。かつての非同期コードは不可解に入り組んだ入れ子形式のコールバックが必要でしたが、今ではシーケンシャルな同期コードとほとんど変わらないほど簡単に記述 (理解) できるようになりました。

非同期メソッドの作成と使用に関する資料は多数公開されているため、その基本はよくご存知だと思います。あまり詳しくないとお考えの方は、Visual Studio ドキュメントのこちらのページ (https://msdn.microsoft.com/library/vstudio/hh191443) がお勧めです。

非同期に関するドキュメントの大半は、非同期メソッドを既存のコードに挿入するだけでは不十分で、呼び出し側自体を非同期にしなければならないと記されています。マイクロソフト言語チームの開発者、Lucian Wischik によれば、「非同期はゾンビ ウイルスみたいなもの」だそうです。では、async void に頼らずに、アプリケーションそのものをはじめから非同期にするにはどうすればよいでしょう。そこで今回は、Windows フォームと Windows Presentation Foundation (WPF) 向けの既定の UI スタートアップ コードにさまざまなリファクタリングを行い、UI の定型コードをオブジェクト指向設計に変換し、async/await のサポートを追加する一連の手順を紹介します。その中で、「async void」の使用が適切な場合とそうでない場合についても説明します。

今回、説明の中心に据えるのは Windows フォームです。WPF ではここで説明する以上の変更が必要になり、若干複雑になります。説明の各手順では、最初に Windows フォーム アプリケーションでの変更点を説明してから、WPF 版の相違点を示します。ここではコードの基本的な変更点を取り上げますが、両環境の完全な例 (および中間の改訂) については付属のオンライン コード ダウンロードをご利用ください。

最初の手順

Windows フォームや WPF のアプリケーションをターゲットにする Visual Studio テンプレートは、スタートアップ中に async を使用するようには考えられていません (つまり、通常スタートアップ プロセスをカスタマイズすることは考えられていません)。C# はオブジェクト指向言語を目指しているため、すべてのコードをクラスに収める必要があります。しかし、既定のスタートアップ コードは、Main メソッド以外の静的メソッドにロジックを含める構成になっていて、通常はメイン フォームのかなり複雑なコンストラクターの中に含めることになります (メイン フォームのコンストラクター内部でデータベースにアクセスするのが正しいとは思えませんが、そのようなコードをよく目にします)。こうした状況は常に問題になっていますが、今回非同期にするとなると、アプリケーションの初期化自体を非同期にする明確なチャンスがないことになります。

まず、Visual Studio で Windows フォーム アプリケーション テンプレートを使用して新しいプロジェクトを作成します。図 1 は、Program.cs に含められる既定のスタートアップ コードです。

図 1 Windows フォーム既定のスタートアップ コード

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 に必要なデータを読み込むまで UI の表示を遅らせるにはどうすればよいでしょう。WPF では、最初にスタートアップ プロセスを編集可能なコードにする必要があります。この作業を行い、WPF と Windows フォームが同じスタート地点に立ったら、この後の手順はどちらもあまり変わりません。

Visual Studio で新しい WPF アプリケーションを作成後、Program という新しいクラスを作成します。そのコードを 図 2 に示します。既定のスタートアップ シーケンスを置き換えるには、プロジェクト プロパティを開き、[スタートアップ オブジェクト] を [App] から新しく作成した [Program] に変更します。

図 2 Windows フォームと等価な 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();
  }
}

図 2 の InitializeComponent の呼び出しで [定義へ移動] を使用すると、App をスタートアップ オブジェクトとして使用している場合と同様、対応する Main コードがコンパイラで生成されるのを確認できます (これはこの段階で「ブラック ボックス」を開く方法です)。

オブジェクト指向のスタートアップを目指して

まず、既定のスタートアップ コードに簡単なリファクタリングを行い、コードをオブジェクト指向に近づけます。つまり、Main のロジックをクラスに移します。そのため、Program を非静的クラスにし (前述のとおり、既定のコードでは誤った方向に向かっています)、そのクラスをコンストラクターに渡します。次に、セットアップ コードをコンストラクターに移し、フォームを実行する Start メソッドを追加します。

この新しいバージョンには「Program1」という名前を付けます (図 3 参照)。このスケルトンが考え方の中核を示します。つまり、プログラムを実行するために、Main でオブジェクトを作成し、そのオブジェクトのメソッドを呼び出すようにします。これが、典型的なオブジェクト指向のシナリオです。

図 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 メソッド内) にいくつか問題があります。1 つは全般的なアーキテクチャの問題です。このように、アプリケーションの有効期間とフォームの表示が結び付くことは望ましくありません。このことが問題にはならないアプリケーションはたくさんあります。しかし、起動時にバックグラウンドで実行されるため、タスク バーや通知エリアのアイコンを除き、起動時に UI が表示されてはいけないアプリケーションもあります。起動すると画面が一瞬表示され、その後消滅するようなアプリケーションを目にしたことがありますが、こうしたアプリケーションのスタートアップ コードもフォームと結び付いていて、フォームの読み込みが終わりしだい自らを非表示にしていると考えられます。このような特定の問題をここで解決するつもりはありませんが、初期化を非同期にする場合は、フォームとアプリケーションを切り離すことが非常に重要です。

Application.Run(m_mainForm) の代わりに、引数を受け取らない Run のオーバーロードを使用します。これが、どの特定のフォームにも結び付かない UI インフラストラクチャの出発点です。このように切り離すと、フォームを自身で表示する必要があります。フォームを閉じてもアプリケーションは終了しなくなります。そのため、この 2 つを明示的にコーディングしたのが図 4 のコードです。また、この機会を利用して、初期化のための最初のフックを追加します。"Initialize" はフォーム クラスに作成したメソッドで、初期化に必要なすべてのロジックを保持します。データベースや Web サイトからのデータの取得などはここで行います。

図 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 によって Run が呼び出されるときに表示するウィンドウが決まります。StartupUri は App.xaml マークアップ ファイルで定義を確認できます。当然ながら、Application 既定の ShutdownMode 設定である OnLastWindowClose により、すべての WPF ウィンドウが閉じるとアプリケーションも終了します。WPF ではアプリケーションの有効期間とウィンドウの表示がこのように結び付けられています (Windows フォームとは異なることに注意してください。Windows フォームでは、メイン ウィンドウから子ウィンドウを開き、その後メイン ウィンドウを閉じるだけでアプリケーションも終了します。WPF では、両方のウィンドウを閉じるまでアプリケーションは終了しません)。

WPF でも Windows フォームと同じ分離を実現するには、まず App.xaml から StartupUri を削除します。代わりに、自身でウィンドウを作成および初期化して、ウィンドウを表示してから App.Run を呼び出します。

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

アプリケーションを作成するときに、app.ShutdownMode を ShutdownMode.OnExplicitShutdown に設定し、アプリケーションとウィンドウの有効期間を分離します。

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

明示的なシャットダウンを実現するには、MainWindow.Closed のイベント ハンドラーをアタッチします。

当然、分離の問題については WPF の方が適切に対処します。そのため、ウィンドウ自体ではなくビュー モデルを初期化する方が適切です。ここでは、MainViewModel クラスを作成し、そこに初期化メソッドを作成します。同様に、アプリケーションを終了する要求も、ビュー モデルで処理します。そのため、"CloseRequested" イベントと対応する "RequestClose" メソッドをビュー モデルに追加します。WPF 版の Program2 のリストを図 5 に示します (Main は変更していないためここには掲載していません)。

図 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 を切り離したところで、アーキテクチャの別の考慮事項に対処します。現在、Application は Program クラスの深部に埋め込まれています。このホスティング環境を「抽象化」します。図 6 の Program3 に示すように、Application のさまざまな Windows フォーム関連メソッドをすべて Program クラスから取り除き、プログラム自体に関連する機能のみを残します。最後に、フォームを閉じることとアプリケーションの終了との直接的なつながりが弱くなるように、Program クラスにイベントを追加します。クラスとしての Program3 と Application との間のやり取りをなくします。

図 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);
}

ホスティング環境を分離するメリットはいくつかあります。1 つは、テストが容易になります (限られた範囲で Program3 をテストできるようになります)。また、他の場所でのコードの再利用が容易になります。たとえば、大きなアプリケーションや起動画面に埋め込むこともできます。

切り離した Main を図 7 に示します。Application ロジックをそこに戻しています。この設計ではに WPF と Windows フォームの統合が容易になります。つまり、Windows フォームを 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();
}

いよいよ非同期に

前置きが長くなりましたが、これで Start メソッドを非同期にできるようになりました。ここでは、await を使用して初期化ロジックを非同期にします。コーディングの規約に従って、Start を StartAsync に、Initialize を InitializeAsync にそれぞれ名前を変更します。また、戻り値の型を async Task に変更します。

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

これを利用するために、Main も変更します。

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

しくみを理解し、さらには繊細で重要な問題を解決する方法を説明するために、async/await によって何が行われるかを詳しく見ていきます。

await の真の意味を理解するために、前述の StartAsync メソッドを考えます。重要なのは、(通常は) 非同期メソッドが await キーワードに到達すると、そこで呼び出し元に復帰することを理解することです。すべてのメソッドからの復帰と同様、実行スレッドは継続します。今回の場合でいえば、StartAsync メソッドが "await m_mainForm.InitializeAsync" に到達して Main に復帰すると、Main の実行が継続し、Application.Run を呼び出します。これにより、Application.Run が m_mainForm.Show の前に実行される可能性があります。直感的には受け入れにくいかもしれません。順番としては m_mainForm.Show の後に Application.Run が呼び出されることになっています。async と await によって確かに非同期プログミングは容易になりますが、それでも決してわかりやすくはありません。

これが非同期メソッドから Task を返す理由です。直感的に受け入れられる非同期メソッドからの「復帰」を表すのが、Task の完了です。つまり、非同期メソッドのすべてのコードが実行されたことを表します。StartAsync の場合は、InitializeAsync と m_mainForm.Show の両方が完了したことを意味します。そしてこれが async void の使用に伴う最初の問題です。Task オブジェクトが返されないため、呼び出し元にメソッドの完了を伝える方法がありません。

スレッドの実行が継続され、StartAsync が既に呼び出し元に復帰しているとすると、メソッドの残りのコードはいつ、どのように実行されるのでしょう。ここで Application.Run の出番です。Application.Run は無限ループしながら、処理を待機します。主な処理対象は UI イベントです。たとえば、マウスをウィンドウの上に移動したり、ボタンをクリックすると、Application.Run メッセージ ループはそのイベントをキューから取り出し、応答に適切なコードにディスパッチします。その後、次のイベントが発生するのを待機します。ただし、厳密には待機するのは UI イベントだけではありません。Control.Invoke を考えてみます。Control.Invoke は UI スレッドで関数を実行します。Application.Run はこうした要求も処理します。

今回の場合、InitializeAsync が完了したら、StartAsync メソッドの残りのコードがこのメッセージ ループにポストされます。await を使用すると、Application.Run はメソッドの残りのコードを、Control.Invoke を使用してコールバックを記述したかのように UI スレッドで実行します (処理を UI スレッドで続行するかどうかは ConfigureAwait で制御します。これについての詳細は、Stephen Cleary の 2013 年 3 月号の「非同期プログラミングのベスト プラクティス」(msdn.com/magazine/jj991977) を参照してください)。

m_mainForm と Application.Run を分離することが非常に重要だった理由がここにあります。すべてを取り仕切るのが Application.Run です。"await" の後のコードを処理するには、実際に UI が表示される前であっても、Application.Run を実行する必要があります。たとえば、Application.Run を Main から StartAsync に戻してみると、プログラムはすぐに終了してしまいます。実行が "await InitializeAsync" に到達したら、制御が Main に戻り、それ以上実行するコードがないため、Main は終了します。

このことから逆に考えれば、async を使用する理由がわかります。よく使われてもすぐに行き詰るアンチパターンが、await の代わりに Task.Wait を呼び出す方法です。これは呼び出し側が非同期メソッドではないため、多くの場合、すぐにデッドロックが発生します。問題は、Wait を呼び出すことで UI スレッドがブロックされ、処理を継続できなくなることです。処理を継続できなければタスクは完了しません。したがって Wait 呼び出しは決して復帰しません。デッドロックの発生です。

await と Application.Run は、ニワトリが先か卵が先かという問題です。前半で繊細な問題があると記しました。await を呼び出すと、既定の動作では UI スレッドで実行が継続されると説明しました。ここではその UI スレッドが必要です。しかし、最初に await を呼び出す時点では、適切なコードがまだ実行されていないため、そのためのインフラストラクチャがセットアップされていません。

この動作の鍵を握るのが SynchronizationContext.Current です。await を呼び出すと、インフラストラクチャは SynchronizationContext.Current の値をキャプチャし、その値を使用して継続をポストします。その結果、UI スレッドでの実行が継続されます。この同期コンテキストは、メッセージ ループが開始されるときに Windows フォームまたは WPF によってセットアップされます。StartAsync 内部では、まだセットアップは行われていません。StartAsync の先頭で SynchronizationContext.Current を調べれば、null になっていることがわかります。同期コンテキストがない場合、await は代わりにスレッド プールに継続をポストします。スレッド プールは UI スレッドにはならないため、機能しなくなります。

WPF 版はそもそもまったく動作しません。結局のところ、Windows フォーム バージョンが動作するのは「偶然」です。既定では、Windows フォームが同期コンテキストをセットアップするのは、最初の制御が作成される時点です。つまり今回の場合は、m_mainForm が構築される時点です (この動作は WindowsFormsSynchronizationContext.AutoInstall によって制御されます)。"await InitializeAsync" が実行されるのはフォームの作成後であるため問題ありません。ただし、m_mainForm 作成の前に await の呼び出しを置くと、同じ問題が生じることになります。この解決策は、冒頭で同期コンテキストを独自にセットアップすることです。

[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 の Task オブジェクトに保持されますが、このオブジェクトは何も実行しません。そのため、出口が見えないような状況でアプリケーションが停止します。"await StartAsync" を使用できれば、Main での例外をキャッチできます。しかし、Main は非同期ではないため、await を使用できません。

これが async void にまつわる 2 つ目の問題です。呼び出し側はこの Task オブジェクトにアクセスできないため、async void メソッドからスローされた例外を正しくキャッチする方法がありません (それでは async void を使用するときはあるのでしょうか。一般的なガイダンスでは、async void は主にイベント ハンドラーに限定すべきだと示されています。前述の 2013 年 3 月号の記事でもそのように説明しています。async/await を最大限に活用するためにこの記事を一読することをお勧めします)。 

通常の状況では、処理されない例外が発生するタスクは TaskScheduler.UnobservedException で対処します。問題は、TaskScheduler.UnobservedException の実行が保証されていないことです。今回はほぼ確実に実行されません。タスク スケジューラで監視していない例外が検出されるのは、そのタスクが最終処理されるときです。最終処理はガベージ コレクターが実行されるときのみ行われます。ガベージ コレクターが実行されるのは、多くのメモリが要求され、ガベージ コレクターでその要求に対応する必要がある場合のみです。

結末が見えてきました。今回の場合、例外が発生してもアプリケーションは何もしません。多くのメモリを要求することもないので、ガベージ コレクションも実行されません。その結果、アプリケーションは停止します。実際、同期コンテキストを指定しなければ WPF 版が停止するのはこれが原因です。ウィンドウが UI 以外のスレッドで作成されるため、WPF ウィンドウ コンストラクターは例外をスローしますが、その例外は処理されないまま放置されます。最後に、programStart タスクに対処し、エラーの場合に実行する継続を追加します。今回の場合、アプリケーションがアプリケーション自体を初期化できないのであれば、終了するのは当然です。

Main は非同期ではないので、Main 内部で await は使用できません。ただし、非同期スタートアップ中にスローされたすべての例外を公開 (または処理) することだけを目的に新しい非同期メソッドを作成することはできます。このメソッドは、await を囲む try/catch ブロックのみで構成します。このメソッドではすべての例外を処理し、新しい例外をスローすることはないため、async void を使用できるもう 1 つの限定的なケースです。

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();
  }
}

Main ではこれを次のように使用します。

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

例によって繊細な問題が存在していることは言うまでもありません (async/await のおかげでものごとが容易になっているとしたら、以前がどれほど困難だったか想像できるでしょう)。通常非同期メソッドが await の呼び出しに到達すると、その非同期メソッドから復帰し、メソッドの残りの部分が継続として実行されます。ただし場合によっては、タスクが同期的に完了することがあります。その場合、コードの実行が停止することはありません。これにはパフォーマンス上のメリットがあります。ですが、今回それが起こったとしたら、HandleExceptions メソッド全体が実行されてから戻り、Application.Run が続いて実行されることになります。つまり、その場合に例外が発生したら、今度は Application.Run の呼び出しの前に Application.Exit が呼び出されることになり、Application.Exit の効果がなくなります。

HandleExceptions を強制的に継続として実行します。何をおいてもまず、確実に Application.Run まで実行を進めなければなりません。そうすれば、例外が発生したとしても、既に Application.Run が実行されており、Application.Exit が正しく Application.Run に割り込みます。Task.Yield がこれを行います。つまり、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 が実行されます。次に HandleExceptions の残りの部分が継続として現在の SynchronizationContext にポストされます。つまり、Application.Run で処理されます。

ちなみに、Task.Yield は async/await の理解度を測る優れたリトマス試験になると考えています。Task.Yield の使用方法を理解できれば、おそらく async/await のしくみについても確実に理解できます。

成果

すべてが動作するようになったところで、少し面白いことを試してみましょう。別のスレッドで実行しなくても、応答性の高いスプラッシュ スクリーンを追加するのがどれほど簡単かを示します。面白いかどうかは別にして、すぐに「始まらない」アプリケーションにはスプラッシュ スクリーンが非常に重要になります。ユーザーがアプリケーションを起動し、数秒間何も画面に表示されなければ、ユーザー エクスペリエンスとしては不適切です。

スプラッシュ スクリーンためだけに別のスレッドを開始するのは非効率かつ不適切です。スレッド間で呼び出しをすべて適切にマーシャリングする必要が生じます。したがって、スプラッシュ スクリーンで進行情報を表示するのが難しく、スプラッシュ スクリーンを閉じる場合でも Invoke の類を呼び出さなければなりません。さらに、最終的にスプラッシュ スクリーンを閉じたときに、多くの場合、フォーカスがメイン フォームに正しく移動しません。スプラッシュ スクリーンとメイン フォームが別のスレッドにあると、両スレッド間で所有権を移動できないためです。これと比べた非同期バージョンの簡潔さを示しているのが図 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 フォームでも、WPF でも、初期化で非同期を簡単にサポートできるよう、オブジェクト指向設計をアプリケーションのスタートアップ コードに追加する方法を説明しました。また、非同期のスタートアップ プロセスで発生する可能性のある繊細な問題を克服する方法も示しました。実際に初期化を非同期にする方法については、ガイダンス (msdn.com/async) を参考にしてください。

async と await を使用できるようになることは第一歩にすぎません。Program がオブジェクト指向に近づけば、他の機能の実装もわかりやすくなります。コマンドライン引数を処理するには、Program クラスで適切なメソッドを呼び出します。メイン ウィンドウが表示される前にユーザーにログインを求めることができます。スタートアップでウィンドウを表示しないで、通知領域でアプリケーションを開始することもできます。いつものように、オブジェクト指向の設計によってコードの機能拡張や再利用が簡単になります。


Mark Sowulは実際には C# で開発されたソフトウェア シミュレーションなのかもしれません (仲間内ではそう推測されています)。最初から熱心な .NET 開発者である Sowul は、.NET と Microsoft SQL Server のアーキテクチャとパフォーマンスに関する豊富な専門知識を、自身が New York で設立したコンサルティング企業の SolSoft Solutions を通じて共有しています。連絡先は、mark@solsoftsolutions.com (英語のみ) です。ソフトウェアの洞察に関する不定期の電子メールへの登録は、eepurl.com/_K7YD (英語) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Stephen Cleary および James McCaffrey に心より感謝いたします。