他のプログラムの実行の終了を非同期で待つ - C#

他のプログラムの実行の終了を非同期で待つコードを紹介します。

概要

こちらの方法で、他のアプリの終了を待つことができますが、終了を待つあいだメインのアプリケーションはロックされたままとなります。
この記事では非同期でアプリの終了を待つ方法を紹介します。

非同期でアプリケーションの終了を待つにはProcessクラスのインスタンスを作成し、EnableRaisingEvents プロパティをTrueに設定し、Exited イベントにイベントハンドラを割り当てることでプロセス終了時にExitedイベントハンドラが呼び出されます。
呼び出されたExitedイベントハンドラはメインスレッドでの実行ではない(システムのスレッド プールのスレッドで呼び出される)ため、メインフォームのコントロールにアクセスする場合はInvokeメソッドを使い同期をとる必要があります。詳細はこちらの記事を参照してください。

プログラム例

Windows Formアプリケーションを作成します。

UI

下図のフォームを作成します。今回のデモで利用するボタンは1つのみです。

コード

下記コードを記述します。(button4のクリックイベントハンドラーを実装しています。)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;

namespace ExecApplication
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
    }

    public delegate void ExitEventHandler();
    public event ExitEventHandler evt;

    private void button4_Click(object sender, EventArgs e)
    {
      evt += proc_Exited_message;

      Process proc = new Process();
      proc.StartInfo.FileName="notepad.exe";
      proc.StartInfo.Arguments = @"C:\Windows\system.ini";
      proc.EnableRaisingEvents = true;
      proc.Exited += new EventHandler(proc_Exited);
      proc.Start();
    }

    void proc_Exited(object sender, EventArgs e)
    {
      this.Invoke(evt);
    }

    void proc_Exited_message()
    {
      textBox1.Text += "notepad.exeが終了しました\r\n";
    }
  }
}

解説

ボタンをクリックするとメモ帳を起動します。メモ帳が起動中でもメインのアプリケーションのスレッドはブロックされません。 メモ帳が終了するとメインのアプリケーションのテキストボックスに「notepad.exeが終了しました」のメッセージが表示されます。

はじめにUIを操作する、proc_Exited_message のコールバックのデリゲートを作成します。
デリゲートの宣言とデリゲートの変数を準備します。
    public delegate void ExitEventHandler();
    public event ExitEventHandler evt;

呼び出されるメソッドをデリゲート変数に代入してデリゲートを作成します。
  evt += proc_Exited_message;

Processオブジェクトを作成します。StartInfoプロパティに実行するアプリケーションを指定します。
  Process proc = new Process();
  proc.StartInfo.FileName="notepad.exe";

EnableRaisingEventsプロパティを true に設定しイベントが発生する状態にします。 Exitedイベントに proc_Exited メソッドの呼び出しを追加します。アプリケーションが終了すると、proc_Exited() メソッドが呼び出されます。
  proc.EnableRaisingEvents = true;
  proc.Exited += new EventHandler(proc_Exited);

proc_Exited() メソッドで Invokeメソッドを呼び出し、先に作成したExitEventHandler デリゲートを呼び出します。 この処理で、proc_Exited_message() メソッドがスレッドセーフで呼び出されます。
proc_Exitedメソッドは非同期で呼び出されるため、スレッドセーフではありません。このメソッド内でTextBoxにアクセスすると、 誤動作の原因となります。
    void proc_Exited(object sender, EventArgs e)
    {
      this.Invoke(evt);
    }

proc_Exited_message メソッドで、テキストボックスにアプリケーションが終了した旨のメッセージを表示します。
    public void proc_Exited_message()
    {
      textBox1.Text += "notepad.exeが終了しました\r\n";
    }

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


コードを実装したボタン(今回の例では[button4])をクリックします。ボタンをクリックするとメモ帳のアプリケーションが起動します。


メモ帳のウィンドウを閉じ、メモ帳を終了すると、アプリケーションのウィンドウにメモ帳が終了した旨のメッセージが表示されます。


今回実装した方法では、メインスレッドがブロックされないため、メモ帳が起動した状態でも、 呼び出し元のアプリケーションのウィンドウは操作できます。


proc_Exited メソッドでテキストボックスにアクセスした場合

proc_ExitedメソッドからInvokeメソッドでproc_Exited_messageを呼び出す処理が冗長に思えますが、 フォームのウィンドウコントロールを操作するメソッドでは同期呼び出しする必要があります。
proc_Exitedメソッドを以下のコードに変更して実行すると、下記の例外が発生します。


エラーメッセージ
System.InvalidOperationException
HResult=0x80131509
Message=有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'textBox1' がアクセスされました。

フォームのコントロールを操作する場合は、Invokeを利用してスレッドセーフで操作するメソッドを呼び出す必要があります。

別の記述方法

クラスのメンバ変数でデリゲート変数に代入して呼び出すのは冗長な場合、次の記述でも動作します。
  private void button5_Click(object sender, EventArgs e)
  {
    Process proc = new Process();
    proc.StartInfo.FileName = "notepad.exe";
    proc.EnableRaisingEvents = true;
    proc.Exited += new EventHandler(proc_Exited2);
    proc.Start();
  }

proc_Exited2 メソッドは以下の記述が利用できます。
ExitEventHandler デリゲートの変数をローカル変数に宣言して代入する方法です。
  void proc_Exited2(object sender, EventArgs e)
  {
    //方法1
    ExitEventHandler eeh = proc_Exited_message;
    this.Invoke(eeh);
  }

ExitEventHandler デリゲートを変数に代入せず直接デリゲートを作成する方法です。
  void proc_Exited2(object sender, EventArgs e)
  {
    //方法2
    this.Invoke(new ExitEventHandler(proc_Exited_message) );
  }

デリゲートを利用せず、Action<T>を利用する方法です。
  void proc_Exited2(object sender, EventArgs e)
  {
    //方法3
    this.Invoke(new Action(proc_Exited_message) );
  }

また、今回紹介した方法とは別の方法として、awaitを利用する方法もあります。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2010-08-15
iPentec all rights reserverd.