Start Debugging

.NET 11 がデッドロックなしのプロセス出力キャプチャを追加

.NET 11 Preview 4 では、stdout と stderr を並行して読み取る新しい System.Diagnostics.Process API、起動とキャプチャを 1 行で行うヘルパー、そして KillOnParentExit が登場します。

Adam Sitnik 氏が、.NET 11 Preview 4 に入った System.Diagnostics.Process の一連の改善を発表しました。注目すべきは、.NET 開発者なら誰しも一度は踏むデッドロックの罠が、ついに BCL レベルで解決されたことです。さらに「プロセスを起動して出力をキャプチャし、終了を待つ」というお決まりのボイラープレートが、たった 1 回の呼び出しにまとまります。

古いパターンがデッドロックする理由

古典的な書き方は一見安全に見えますが、そうではありません。

var psi = new ProcessStartInfo("git", "log")
{
    RedirectStandardOutput = true,
    RedirectStandardError = true,
};
using var p = Process.Start(psi)!;
string stdout = p.StandardOutput.ReadToEnd();
string stderr = p.StandardError.ReadToEnd();
p.WaitForExit();

親プロセスが stderr.ReadToEnd() に到達する前に、子プロセスがパイプバッファ以上の量を stderr に書き込むと、子はいっぱいになったパイプで停止し、親は stdout のクローズを待ったまま停止します。両者は永遠に待ち続けます。これまでの公式の回避策は、OutputDataReceived を使うイベントベースの API、手動の StringBuilder、そして各ストリームに 1 つずつ用意した ManualResetEvent でした。動きはしますが、初回で正しく書ける人はいません。

新しいワンライナー

.NET 11 では Process.RunAndCaptureTextProcess.RunAndCaptureTextAsync が追加され、両方のパイプを内部で多重化します。

ProcessTextOutput result = await Process.RunAndCaptureTextAsync(
    "git",
    ["log", "--oneline", "-n", "20"]);

if (result.ExitStatus.ExitCode != 0)
    throw new InvalidOperationException(result.StandardError);

Console.WriteLine(result.StandardOutput);

ProcessTextOutput はキャプチャした stdout、stderr、プロセス ID、そして終了コードに加え Unix では終了シグナルも報告する ProcessExitStatus を保持します。すでに動作中の Process を持っている呼び出し元向けの Process.ReadAllText、各行に StandardError フラグを付けて IEnumerable<ProcessOutputLine> をストリーミングする ReadAllLines/Async、バイナリツール向けの ReadAllBytes/Async も用意されています。ブログによれば、同期版の RunAndCaptureText は Windows でイベントベースの同等ループに比べてメモリ割り当てが約 4.5 倍少ないとのことです。

ライフタイムと切り離し

長年の落とし穴 2 つも、正式なサポートを得ました。ProcessStartInfo.KillOnParentExit は Windows、Linux、Android で子プロセスを親のライフタイムに紐付けるため、クラッシュした CLI ツールが孤児ワーカーを残すことはなくなります。ProcessStartInfo.StartDetached はその逆で、子プロセスは親プロセスと、それを起動した端末よりも長く生き続けます。Process.StartAndForget は PID だけを返して即座に親側のハンドルを解放するため、ファイア・アンド・フォーゲットの起動にぴったりです。

int pid = Process.StartAndForget("notepad.exe");

ハンドル配線

より低レイヤーのシナリオでは、ProcessStartInfo.StandardInputHandleStandardOutputHandleStandardErrorHandle が任意の SafeFileHandle を受け取れます。新しい SafeFileHandle.CreateAnonymousPipe から取得したパイプや、File.OpenNullHandle の破棄シンクも含まれます。ProcessStartInfo.InheritedHandles を使えば、フォーク境界を越えるハンドルをホワイトリストで厳密に指定でき、Windows でのファイルロックリークの実際の原因のひとつを塞ぐことができます。

.NET 11 Preview 4 で試して、OutputDataReceived ハンドラーを削除し始めましょう。

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る