非同期処理とApplication.Shutdown()


はじめに

本記事はC# WPF.NetFlameworkを対象とした記事となります。非同期処理を実装するシステムにて、適切に非同期処理を破棄する終了処理のベストプラクティスを模索した時の記録(2020/9/13時点)を個人用にまとめます。

非同期処理について

foreground / background thread 概要

マネージドスレッドは以下の二つに分類されます。

種類 説明
foreground thread マネージド実行環境を維持できる。
background thread マネージド実行環境を維持できない。つまりすべてのフォアグラウンドが停止すると、システムによって全てのバックグラウンドスレッドが停止させられる。

これはUIスレッドで非同期処理を実行した場合、非同期処理実行中にUIスレッドを停止したときの動作の違いを意味します。「background thread」はUIスレッド終了に伴い終了しますが、「foreground thread」は終了しません。

foreground / background thread 判定方法

次に特定の非同期処理がどちらに該当するのかを確認します。

方法としては、実際に非同期処理実行中にUIスレッドを停止してみて確認することもできるかもしれませんが、Thread.IsBackgroundというプロパティを参照するのが最も容易です。
しかし、Threadオブジェクトを生成せずに非同期処理を実行する場合は、非同期処理内で以下の記述をすることでThread.IsBackgroundの規定値を確認することができます。

Console.WriteLine(Thread.CurrentThread.IsBackground);

ちなみにThreadオブジェクトを生成した際の規定値はFalse、Task.Run()にて非同期処理を実行する場合はTureが規定値となります。

Applicationの終了について

ここまでUIスレッドと非同期スレットとの関係性についての注意事項を述べましたが、本題であるApplication終了時の関係性について触れていきます。先に結論を言うと、Applicationが終了するとマネージドスレッドの種類に関係なく、非同期スレッドは終了します。では、Applicationの終了について詳しく見ていきます。

Window.Close()

基本的には、WPFアプリケーションテンプレートで作成したMainWindow.xaml.csにてClose()メソッドを呼び出すことでApplicationは終了します。ただしApp.xamlもしくはApp.xaml.csを編集した場合は、それに限りません。

厳密にいうとApplication.MainWindowに設定されたWindowインスタンスがClose()されたときに、Application.ShutdownModeOnLastWindowCloseに設定されていたかどうかによります。

Application.ShutdownMode

Application.ShutdownModeが規定値であるOnLastWindowCloseに設定されていた場合、Application.MainWindowに設定されたWindowインスタンスのClose()時にApplicationは終了されます。しかしOnExplicitShutdownに設定されていた場合は、Close()=Applicationの終了とはなりません。
※結果的にAppicationが終了することはありますが、Applicationが生きたままとなる危険性があります。

したがって後者の場合は、Close()ではなく明示的にApplication.Shutdown()メソッドを呼び出す必要があります。

Application.MainWindow

ここでApplication.MainWindowにWindowインスタンスを設定する方法を見ていきます。Application.MainWindowにセットするには、App.xamlで行う方法とApp.xaml.csで行う方法の2通りあります。

テンプレートからWPFアプリケーションを作成した場合はApp.xamlのApplicationのStartupUriにて記述されています。

App.xaml
<Application x:Class="HelloWorld.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

続いて、App.xaml.csでMainWindowを設定する方法を提示します。

App.xaml
<Application x:Class="HelloWorld.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="AppStartup">
    <Application.Resources>

    </Application.Resources>
</Application>
App.xaml.cs
public partial class App : Application
{
    void AppStartup(object sender, StartupEventArgs e)
    {
        MainWindow window = new MainWindow();
        window.Show();
    }
}

コチラの方法ではApp.xamlStartupUriにて直接xamlを指定せずに、App.xaml.csのStartupイベント上にてWindowインスタンスを生成します。  
  

ここで補足になりますが、上記ではApplication.MainWindowに明示的にセットしていません。しかし、実はApplication.Mainwindowに生成したMainWindowsインスタンスがセットされています。これは、最初にWindowsインスタンスを生成した際に自動でそのインスタンスがApplication.MainWindowにセットされる仕様のため、上記のような記述になっています。

したがって同イベント内でWindowインスタンスを2つ生成した場合は、最初に生成したWindowインスタンスがClose()した段階でApplicationが終了処理に入るので、以降の処理は実行されないことに注意してください。
App.xaml.cs
public partial class App : Application
{
    void App_Startup(object sender, StartupEventArgs e)
    {
        // Windowsインスタンスを生成した際に自動でそのインスタンスが
        // Application.MainWindowにセットされる。
        Window firstWindow = new MainWindow();
        Window secondWindow = new MainWindow();

        firstWindow.ShowDialog();
        // 移行の処理は実行されません。
        secondWindow.ShowDialog();
    }
}

まとめ

以上より、非同期処理を実装したアプリケーションは、以下に留意して実装することを結論としました。

  • 非同期処理は理由のない限り、「background thread」で処理させる。
  • マネージドスレッドの種類を問わず、Applicationを終了させることで非同期スレッドも終了させることができる。
  • Application.ShutdownModeAppication.MainWindowが編集されることを考慮して、基本的にはClose()ではなく明示的にApplication.ShutDown()を使用してApplication終了処理を記述する。

※ガベージコレクションについては話がややこしくなるので、ココでは触れない。