7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【WPF】アプリケーションのメインプロセスが終了しないケースを深堀する記事

Last updated at Posted at 2025-06-15

いいね、ストック、Badボタン、クレーム、編集リクエストで言いがかりをつけるなどが励みになります。

(最近いいねが入ったので追記 25/6/29)

自前のアプリ制作でなぜかメインプロセスが残る問題(いわゆるゾンビプロセス Zombie Process)が解決できずに難儀していたので、掘り下げてみることにした。

ファイルの読み書き等処理等は自動でGCされるわけではないので明示的なDisposeかUsingディレクティブで囲む必要がある。

これをしないとアプリケーションをCloseしてもメモリ上にメインプロセスの.exeが残り続ける場合がある(つまり実際にはアプリケーションが終了しない)

→この場合、Application.ShutDown()など他の終了方法も通用しない

今回はこのことを実証しつつ、幾つかCloseしてもプロセスが終了しないケースを挙げてみます。

参考URL

ゾンビプロセスを回避する方法(対象はC#のアプリケーション)

git

Visualstudio 2022 .net9
それほど難しいコードは使用していないため、比較C古いTargetFrameWotkでも動作します(たぶん)。

git close https://github.com/Sheephuman/ZombiProcessTest.git

まずは比較的単純なケースから再現してみることにする。

TimerクラスがNewされ、適切にDisposeされなかった場合


  private System.Timers.Timer _timer = null!;
  private Thread _backgroundThread = null!;
  private bool _isRunning = true;
  
   private void StartBackgroundWork()
   {
       // バックグラウンドスレッドを開始
       _backgroundThread = new Thread(() =>
       {
           while (_isRunning)
           {
               Console.WriteLine("Background thread running...");
               Thread.Sleep(1000);
           }
       });
       _backgroundThread.IsBackground = false; // 意図的にフォアグラウンドスレッドにする
       _backgroundThread.Start();

       // タイマーを開始
       _timer = new System.Timers.Timer(500);
       _timer.Elapsed += TimerElapsed;
       _timer.AutoReset = true;
       _timer.Start();
   }

   private void TimerElapsed(object sender, ElapsedEventArgs e)
   {
       // タイマーイベントで何か処理(例: ログ出力)
       Console.WriteLine("Timer ticked...");
   }

Test実行ボタン

 private async void testButton_Click(object sender, RoutedEventArgs e)
 {
      private void testButton_Click(object sender, RoutedEventArgs e)
  {
      try
      {
          StartBackgroundWork();
      }
      catch (Exception ex)
      {
          MessageBox.Show(ex.Message);
      }
  }



終了時の処理をコメントアウト

  private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
  {
      // 意図的にリソース解放をしない
      // _isRunning = false;
      // _timer?.Stop();
      // _timer?.Dispose();
      //_backgroundThread?.Join();
  }

実行結果

明示的な開放をしないので、アプリケーションが終了後もメインプロセスがゾンビとして残る

ffmpegのような外部プロセスを呼び出すケースにおいて、適切な終了処理あるいは実装を行わなかった場合

$\color{Gray}{\tiny \textsf{※再現にやたらと苦労させられた}}$

非同期メソッド内で同期的な処理がある場合
 → このケースのみ、MainWindowがCloseされてもアプリケーションのメインプロセスが閉じない

非同期処理メソッドがAsync Void型の場合はゾンビ化

ShutdownMode.OnMainWindowClose以外が指定されていた場合
→ OnExplicitShutdownApplication.Current.Shutdown();が明示的に呼ばれないと終了しないモード

・WaitForExitAsync()やキャンセルトークンなどを実行しなかった場合
 

参考

実装

非同期で実装した場合は高確率で起こる
→適切に実装出来ていれば問題ないらしい。
多分、Projectが複雑化するほどリソースの開放が間に合わないのだと思う。再現が困難。

コンストラクタ・フィールド変数
 Thread th1 = null!;
Process process = null!; // Processオブジェクトをフィールドとして定義

  public MainWindow()
  {
      InitializeComponent();


           // Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
            // MainWindowを閉じたときにアプリケーション全体を終了するように設定 ゾンビプロセス化しないために必要
            //
  }

testButton
 private void testButton_Click(object sender, RoutedEventArgs e)
 {
     th1 = new Thread(() => RunFfmpegAsync());
     th1.IsBackground = true;
     //フォアグラウンドで動作。ゾンビプロセスの主な原因

     
     th1.Start();
 }

RunFfmpeg(同期バージョン:非実用的)

メインプロセスが終了しなくなる。
条件不明。例外を放置してるとなることがある?

RunFfmpegAsync(非同期バージョン)
  
          private async Task RunFfmpegAsync()
     {

         var startInfo = new ProcessStartInfo
         {
             FileName = "ffmpeg.exe",
             Arguments = "-i test.mp4 -y output.mp4",
             UseShellExecute = false,
             RedirectStandardInput = true,
             RedirectStandardError = true,
             CreateNoWindow = true
         };

        //他のButton呼び出しで利用するためusing句を外す。TextBoxに表示されない原因だった
         process = new Process();

         process.StartInfo = startInfo;
         process.EnableRaisingEvents = true;

         // 標準出力とエラー出力のコールバックを設定


         ///実験結果 非同期メソッド内でここのasync・awaitを不使用にするとゾンビプロセス化する
         process.ErrorDataReceived += async (s, ev) =>
         {
             if (ev.Data != null)
             {


                 await Dispatcher.InvokeAsync(() =>
                 {

                     OutputTextBox.AppendText(ev.Data + Environment.NewLine);
                     OutputTextBox.ScrollToEnd(); // テキストボックスをスクロールして最新の出力を表示
                 });
                 _cts.Cancel(); // キャンセルトークンをキャンセル
                 await Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする
             }
         };

         // Exitedイベント(問題を引き起こす可能性のある実装)
         process.Exited += (s, ev) =>
         {
             // WaitForExitを呼ばない(バッファが残る可能性)
             // 終了を待つ(キャンセルトークンを使用)
             ///ゾンビプロセス化しない
             //_cts.Cancel();



             Dispatcher.InvokeAsync(() => MessageBox.Show("ffmpeg process exited."));
         };

         // プロセス開始
         process.Start();

         process.BeginErrorReadLine();




         try
         {
             await process.WaitForExitAsync(); // キャンセルトークンを使用して非同期に待機


         }
         catch
         {
             // 例外を無視(問題を悪化させる)
         }
     }

RunFfmpegAsync(非同期バージョン)

実用的。
ShutdownMode.OnExplicitShutdownなどの条件次第でゾンビプロセス化する。
非同期メソッド内で同期的な処理があるとゾンビプロセス化する(コメント参照)

Async Voidだとゾンビ化する。
・非同期処理が長すぎる+タスクがキャンセルされない状態でも発生するらしい

参考
非同期外部プロセス起動で標準出力を受け取る際の注意点

RunFfmpegAsync(非同期バージョン)
  public MainWindow()
  {
      InitializeComponent();

      //  Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
      // MainWindowを閉じたときにアプリケーション全体を終了するように設定 
      //defalutなので指定は不要
      
      
      Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
      //ShutDownメソッドを呼ばないとゾンビプロセス化する
      }



       //Task型を返さないとゾンビ化する
        private async void RunFfmpegAsync()
        {

            var startInfo = new ProcessStartInfo
            {
                FileName = "ffmpeg.exe",
                Arguments = "-i test.mp4 -y output.mp4",
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            };

            process = new Process();

            process.StartInfo = startInfo;
            process.EnableRaisingEvents = true;

            // 標準出力とエラー出力のコールバックを設定



            process.ErrorDataReceived += (s, ev) =>
            {
                if (ev.Data != null)
                {


                    Dispatcher.InvokeAsync(() =>
                   {

                       OutputTextBox.AppendText(ev.Data + Environment.NewLine);
                       OutputTextBox.ScrollToEnd(); // テキストボックスをスクロールして最新の出力を表示
                   });
                    _cts.Cancel(); // キャンセルトークンをキャンセル
                    Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする
                }
            };



StopButton

qキーの送信
恐らく破棄(Dispose)した方が良い
→ あまり関係ない模様。

StopButton
  private void StopButton_Click(object sender, RoutedEventArgs e)
  {
      try
      {
          StreamWriter inputWriter = process.StandardInput;


          inputWriter.WriteLine("q");
      }

      catch (Exception ex)
      {
          MessageBox.Show($"Error stopping ffmpeg: {ex.Message}");
      }
      //inputWriter.Dispose();
      //解放させない
  }

実行結果

あとがき

Uploadeめんどい。
いいねがついた時点で更新。
Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
で安定してゾンビ化。
Async Voidでもゾンビ化する。

7
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?