いいね、ストック、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
以外が指定されていた場合
→ OnExplicitShutdown
はApplication.Current.Shutdown();
が明示的に呼ばれないと終了しないモード
・WaitForExitAsync()やキャンセルトークンなどを実行しなかった場合
参考
実装
非同期で実装した場合は高確率で起こる
→適切に実装出来ていれば問題ないらしい。
多分、Projectが複雑化するほどリソースの開放が間に合わないのだと思う。再現が困難。
Thread th1 = null!;
Process process = null!; // Processオブジェクトをフィールドとして定義
public MainWindow()
{
InitializeComponent();
// Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
// MainWindowを閉じたときにアプリケーション全体を終了するように設定 ゾンビプロセス化しないために必要
//
}
private void testButton_Click(object sender, RoutedEventArgs e)
{
th1 = new Thread(() => RunFfmpegAsync());
th1.IsBackground = true;
//フォアグラウンドで動作。ゾンビプロセスの主な原因
th1.Start();
}
RunFfmpeg(同期バージョン:非実用的)
メインプロセスが終了しなくなる。
条件不明。例外を放置してるとなることがある?
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だとゾンビ化する。
・非同期処理が長すぎる+タスクがキャンセルされない状態でも発生するらしい
参考
非同期外部プロセス起動で標準出力を受け取る際の注意点
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)した方が良い
→ あまり関係ない模様。
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でもゾンビ化する。