火曜日, 6月 17, 2025
- Advertisment -
ホームニューステックニュース【WPF】アプリケーションのメインプロセスが終了しないケースを深堀する記事 #C# - Qiita

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



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

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

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

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

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

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

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

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

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

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

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


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

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

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

非同期メソッド内で同期的な処理がある場合
 → このケースのみ、MainWindowがCloseされてもアプリケーションのメインプロセスが閉じない
ShutdownMode.OnMainWindowClose以外が指定されていた場合
→ OnExplicitShutdownApplication.Current.Shutdown();が明示的に呼ばれないと終了しないモード
 →このモードでさえ、非同期処理内で同期処理があったり、キャンセルトークンが通知されていないと利かないケースがあるのが判明しています(多分に推測交じり)

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

参考

非同期で実装した場合は高確率で起こる(UIスレッドがブロックされるので基本的にしない。というか出力が表示されない)

コンストラクタ・フィールド変数

 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 void RunFfmpegAsync()
        {

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

            using (process = new Process())
            {
                process.StartInfo = startInfo;
                process.EnableRaisingEvents = true;

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



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

                        // バッファに追加(スレッドセーフでない)
                        await Dispatcher.InvokeAsync(() =>
                           {

                               OutputTextBox.AppendText(ev.Data + Environment.NewLine);
                               OutputTextBox.ScrollToEnd(); // テキストボックスをスクロールして最新の出力を表示
                           });

                        await Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする
                    }
                };

                // Exitedイベント(問題を引き起こす可能性のある実装)
                process.Exited += async (s, ev) =>
                {
                    // WaitForExitを呼ばない(バッファが残る可能性)

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

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

                process.BeginErrorReadLine();



                // 終了を待つ(キャンセルトークンを使用)
                try
                {


                    //process.WaitForExitAsync();
                }
                catch
                {
                    // 例外を無視(問題を悪化させる)
                }
            }
            // usingブロックを抜けた後、プロセスが完全に解放されない可能性
        }


RunFfmpegAsync(非同期バージョン)

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

RunFfmpegAsync(非同期バージョン)

  public MainWindow()
  {
      InitializeComponent();

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



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

        using (process = new Process())
        {
            process.StartInfo = startInfo;
            process.EnableRaisingEvents = true;

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


         ///実験結果 ErrorDataReceived内でasync・awaitを不使用にするとゾンビプロセス化する
         ///警告も特に発生しない
             process.ErrorDataReceived += (s, ev) =>
  {
      if (ev.Data != null)
      {
         ////本来はasync・awaitを入れる
          Dispatcher.InvokeAsync(() =>
         {

             OutputTextBox.AppendText(ev.Data + Environment.NewLine);
             OutputTextBox.ScrollToEnd(); // テキストボックスをスクロールして最新の出力を表示
         });

          Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする 
        //本来はawaitを入れる
      }
  };
                    await Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする
                }
            };

            // Exitedイベント(問題を引き起こす可能性のある実装)
            process.Exited += async (s, ev) =>
            {
                // WaitForExitを呼ばない(バッファが残る可能性)

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

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

            process.BeginErrorReadLine();



            // 終了を待つ(キャンセルトークンを使用)
            try
            {


                await process.WaitForExitAsync();
            }
            catch
            {
                // 例外を無視(問題を悪化させる)
            }
        }
    }


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めんどい。
いいねがついた時点で更新。





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -