いいね、ストック、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
以外が指定されていた場合
→ OnExplicitShutdown
はApplication.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めんどい。
いいねがついた時点で更新。
Views: 0