{"id":4051,"date":"2026-05-13T21:24:02","date_gmt":"2026-05-13T21:24:02","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/05\/13\/process-api-improvements-in-net-11\/"},"modified":"2026-05-13T21:24:02","modified_gmt":"2026-05-13T21:24:02","slug":"process-api-improvements-in-net-11","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/05\/13\/process-api-improvements-in-net-11\/","title":{"rendered":"Process API Improvements in .NET 11"},"content":{"rendered":"<p>The <code>System.Diagnostics.Process<\/code> class is the primary way to create and interact with processes with .NET. We made the biggest update to it in years, with .NET 11. The changes add high-level APIs that make it easy to start a process and capture its output without deadlocks, give you full control over handle inheritance and standard handle redirection, introduce lifetime management features like <code>KillOnParentExit<\/code>, and include a lightweight <code>SafeProcessHandle<\/code>-based API surface that is more trimmer-friendly.<\/p>\n<h2>Summary<\/h2>\n<p>Here\u2019s a recap of new Process APIs in .NET 11:<\/p>\n<table>\n<thead>\n<tr>\n<th>Feature<\/th>\n<th>API<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>One-liner process execution<\/strong><\/td>\n<td><code>Process.RunAndCaptureText[Async]<\/code><\/td>\n<td>Starts a process, captures output\/error, waits for exit \u2013 all in one call.<\/td>\n<\/tr>\n<tr>\n<td><strong>One-liner process execution without capturing output<\/strong><\/td>\n<td><code>Process.Run[Async]<\/code><\/td>\n<td>Starts a process and waits for exit without capturing output.<\/td>\n<\/tr>\n<tr>\n<td><strong>Fire and forget<\/strong><\/td>\n<td><code>Process.StartAndForget<\/code><\/td>\n<td>Starts a process, returns its PID, and releases all resources immediately.<\/td>\n<\/tr>\n<tr>\n<td><strong>Deadlock-free output capture<\/strong><\/td>\n<td><code>Process.ReadAllText\/Bytes\/Lines[Async]<\/code><\/td>\n<td>Reads both stdout and stderr simultaneously using multiplexing, avoiding pipe buffer deadlocks.<\/td>\n<\/tr>\n<tr>\n<td><strong>Redirect to anything<\/strong><\/td>\n<td><code>ProcessStartInfo.Standard[Input\/Output\/Error]Handle<\/code><\/td>\n<td>Redirect standard handles to files, pipes, null, or any <code>SafeFileHandle<\/code>.<\/td>\n<\/tr>\n<tr>\n<td><strong>Controlled inheritance<\/strong><\/td>\n<td><code>ProcessStartInfo.InheritedHandles<\/code><\/td>\n<td>Specify exactly which handles child process inheritance, preventing accidental leaks.<\/td>\n<\/tr>\n<tr>\n<td><strong>Kill on parent exit<\/strong><\/td>\n<td><code>ProcessStartInfo.KillOnParentExit<\/code><\/td>\n<td>Ensures child processes are terminated when the parent exits (Windows and Linux).<\/td>\n<\/tr>\n<tr>\n<td><strong>Detached processes<\/strong><\/td>\n<td><code>ProcessStartInfo.StartDetached<\/code><\/td>\n<td>Start a process that survives parent exit, signal, or terminal close.<\/td>\n<\/tr>\n<tr>\n<td><strong>Lightweight process handle<\/strong><\/td>\n<td><code>SafeProcessHandle.Start\/WaitForExit\/Kill\/Signal<\/code><\/td>\n<td>Trimmer-friendly, lower-level API for starting and managing processes without <code>Process<\/code>.<\/td>\n<\/tr>\n<tr>\n<td><strong>Process exit details<\/strong><\/td>\n<td><code>ProcessExitStatus<\/code><\/td>\n<td>Reports exit code, terminating signal (Unix), and whether the process was killed due to timeout\/cancellation.<\/td>\n<\/tr>\n<tr>\n<td><strong>Null handle<\/strong><\/td>\n<td><code>File.OpenNullHandle()<\/code><\/td>\n<td>Opens a handle that discards writes and returns EOF on reads.<\/td>\n<\/tr>\n<tr>\n<td><strong>Anonymous pipes<\/strong><\/td>\n<td><code>SafeFileHandle.CreateAnonymousPipe<\/code><\/td>\n<td>Creates a connected pipe pair with optional async support.<\/td>\n<\/tr>\n<tr>\n<td><strong>Console handles<\/strong><\/td>\n<td><code>Console.OpenStandard[Input\/Output\/Error]Handle()<\/code><\/td>\n<td>Gets the underlying OS handle for standard streams.<\/td>\n<\/tr>\n<tr>\n<td><strong>Handle type detection<\/strong><\/td>\n<td><code>SafeFileHandle.Type<\/code><\/td>\n<td>Identifies whether a handle is a file, pipe, socket, etc.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Other improvements include:<\/p>\n<ul>\n<li><strong>Better scalability on Windows<\/strong>: <code>BeginOutputReadLine<\/code>\/<code>BeginErrorReadLine<\/code> no longer block thread pool threads: throughput improvement when starting multiple processes in parallel with redirected output and error.<\/li>\n<li><strong>Better trimmability<\/strong>: Up to 20% smaller NativeAOT binaries compared to .NET 10 when using <code>Process<\/code> and up to 32% smaller when using <code>SafeProcessHandle<\/code>.<\/li>\n<li><strong>Improved process creation on Apple platforms<\/strong>: Up to 100x faster process creation on Apple Silicon due to switch to <code>posix_spawn<\/code>.<\/li>\n<li><strong>Reduced memory allocations on Unix<\/strong>: 30\u201350% fewer allocations when starting processes on Unix.<\/li>\n<\/ul>\n<p>The rest of the blog post is a deep dive into each of these features.<\/p>\n<h2>Capturing process output without deadlocks<\/h2>\n<h3>Why capturing process output can hang your app<\/h3>\n<p>When redirecting standard output and error of the process, it\u2019s possible to run into deadlocks. Knowing why it\u2019s possible is crucial to understanding the changes we have made. Let\u2019s build a C# app that tries to read all output and error from a process.<\/p>\n<p>First of all, we need to redirect standard output and error of the process to be able to read it. This is done by setting <code>RedirectStandardOutput<\/code> and <code>RedirectStandardError<\/code> properties of <code>ProcessStartInfo<\/code> to <code>true<\/code>. Then before the process is started, two dedicated pipes are created (one for standard output and one for standard error), and the process is created with the write ends of these pipes (each pipe comes with a read and write end) as standard output and error. The child process just writes to its standard output and error as usual, but instead of going to the console, the data is written to the pipes.<\/p>\n<pre><code class=\"language-csharp\">ProcessStartInfo startInfo = new(\"dotnet\", \"--help\")\r\n{\r\n    RedirectStandardOutput = true,\r\n    RedirectStandardError = true\r\n};\r\n\r\nusing Process process = new() { StartInfo = startInfo };<\/code><\/pre>\n<p>Pipes have limited buffer size (usually 4KB on Windows and 64KB on Unix). When the producer (in our case, the child process) writes to the pipe, the data is stored in a buffer until it\u2019s read by the consumer (the parent process). If the producer writes more data than the size of the buffer and the consumer is not reading from the pipe at the same time, the producer will be blocked on write operation, waiting for the consumer to read from the pipe and free some space in the buffer.<\/p>\n<p>If the consumer is waiting for the producer to exit (e.g. by calling <code>WaitForExit<\/code>) without reading from the pipe, it will be blocked as soon as the producer fills the buffer:<\/p>\n<pre><code class=\"language-csharp\">process.Start();\r\nprocess.WaitForExit();\r\n\r\nstring output = process.StandardOutput.ReadToEnd();\r\nstring error = process.StandardError.ReadToEnd();<\/code><\/pre>\n<p>But does re-ordering the code help?<\/p>\n<pre><code class=\"language-csharp\">process.Start();\r\n\r\nstring output = process.StandardOutput.ReadToEnd();\r\nstring error = process.StandardError.ReadToEnd();\r\n\r\nprocess.WaitForExit();<\/code><\/pre>\n<p>Not really. <code>ReadToEnd<\/code> is a blocking call \u2013 it reads until the stream reaches EOF, which only happens when the child process closes its end of the pipe (typically on exit). So in the code above, we first block on standard output until the child exits, and only <em>then<\/em> start reading standard error. While we\u2019re waiting on standard output, nobody is draining standard error. If the child writes more to stderr than the pipe buffer can hold, the child blocks on its write \u2013 and we\u2019re stuck waiting for each other.<\/p>\n<p>The root cause is that we are reading the two streams <strong>sequentially<\/strong>, not simultaneously. To avoid this deadlock, we need to <strong>drain both standard output and error at the same time<\/strong>. So far, we had two options:<\/p>\n<p>The existing APIs are not optimal in terms of simplicity and performance. I\u2019ll show you a couple patterns.<\/p>\n<h4>Use asynchronous read operations on <code>StandardOutput<\/code> and <code>StandardError<\/code><\/h4>\n<p>The <code>Process<\/code> class exposes stream readers that you can read from, for example, with <code>ReadToEndAsync<\/code>.<\/p>\n<pre><code class=\"language-csharp\">process.Start();\r\n\r\n\/\/ Start both operations to ensure both streams are drained at the same time\r\nTask&lt;string&gt; outputTask = process.StandardOutput.ReadToEndAsync();\r\nTask&lt;string&gt; errorTask = process.StandardError.ReadToEndAsync();\r\n\r\n\/\/ Wait for both read operations to complete and process to exit\r\nawait Task.WhenAll(outputTask, errorTask, process.WaitForExitAsync());\r\n\r\nstring output = await outputTask;\r\nstring error = await errorTask;<\/code><\/pre>\n<h4>Use <code>OutputDataReceived<\/code> and <code>ErrorDataReceived<\/code> events<\/h4>\n<p>These <code>Process<\/code> class events are raised when a line is written to standard output and error respectively.<\/p>\n<pre><code class=\"language-csharp\">StringBuilder stdOut = new(), stdErr = new();\r\n\r\nprocess.OutputDataReceived += (sender, e) =&gt; stdOut.AppendLine(e.Data);\r\nprocess.ErrorDataReceived += (sender, e) =&gt; stdErr.AppendLine(e.Data);\r\n\r\nprocess.Start();\r\n\r\nprocess.BeginOutputReadLine();\r\nprocess.BeginErrorReadLine();\r\n\r\nprocess.WaitForExit();<\/code><\/pre>\n<h3>Process.ReadAllText and Process.ReadAllTextAsync<\/h3>\n<p>We have added <code>ReadAllText<\/code> and <code>ReadAllTextAsync<\/code> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126942\">PR<\/a>) methods to <code>Process<\/code> class. They drain standard output and error at the same time, helping us to avoid deadlocks. They decode the output using the encoding specified by the <code>ProcessStartInfo.Standard[Output\/Error]Encoding<\/code> (or the default when not specified), and return the result as strings (per-line needs are handled by an API we\u2019ll see shortly).<\/p>\n<pre><code class=\"language-csharp\">public class Process\r\n{\r\n    public (string StandardOutput, string StandardError) ReadAllText(TimeSpan? timeout = default);\r\n    public Task&lt;(string StandardOutput, string StandardError)&gt; ReadAllTextAsync(CancellationToken cancellationToken = default);\r\n}<\/code><\/pre>\n<p>So the code to read all output and error from the process becomes much simpler:<\/p>\n<pre><code class=\"language-csharp\">ProcessStartInfo startInfo = new(\"dotnet\", \"--help\")\r\n{\r\n    RedirectStandardOutput = true,\r\n    RedirectStandardError = true\r\n};\r\n\r\nusing Process process = new() { StartInfo = startInfo };\r\nprocess.Start();\r\n\r\n(string output, string error) = process.ReadAllText();\r\nprocess.WaitForExit();<\/code><\/pre>\n<h4>Process.RunAndCaptureText and Process.RunAndCaptureTextAsync<\/h4>\n<p>We expect that capturing output and error and just waiting for the process to exit (process can close standard handles and keep running) is very common. That is why we have <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127210\">introduced<\/a> <code>RunAndCaptureText<\/code> and <code>RunAndCaptureTextAsync<\/code> methods that combine starting the process, reading all output and error, and waiting for the process to exit in one method call.<\/p>\n<pre><code class=\"language-csharp\">namespace System.Diagnostics;\r\n\r\npublic sealed class ProcessExitStatus\r\n{\r\n    public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null);\r\n    public int ExitCode { get; }\r\n    public PosixSignal? Signal { get; }\r\n    public bool Canceled { get; }\r\n}\r\n\r\npublic sealed class ProcessTextOutput\r\n{\r\n    public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId);\r\n    public ProcessExitStatus ExitStatus { get; }\r\n    public string StandardOutput { get; }\r\n    public string StandardError { get; }\r\n    public int ProcessId { get; }\r\n}\r\n\r\npublic class Process\r\n{\r\n    public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default);\r\n    public static ProcessTextOutput RunAndCaptureText(string fileName, IList&lt;string&gt;? arguments = null, System.TimeSpan? timeout = default);\r\n\r\n    public static Task&lt;ProcessTextOutput&gt; RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default);\r\n    public static Task&lt;ProcessTextOutput&gt; RunAndCaptureTextAsync(string fileName, IList&lt;string&gt;? arguments = null, CancellationToken cancellationToken = default);\r\n}<\/code><\/pre>\n<p>Which makes the code to start a process and capture its output and error literally a one liner:<\/p>\n<pre><code class=\"language-csharp\">ProcessTextOutput output = Process.RunAndCaptureText(\"dotnet\", [\"--help\"]);<\/code><\/pre>\n<p>We also provided <code>Process.Run<\/code> and <code>Process.RunAsync<\/code> methods that don\u2019t capture output and error, but just wait for the process to exit, for cases when you don\u2019t care about the output.<\/p>\n<pre><code class=\"language-csharp\">ProcessExitStatus status = Process.Run(\"dotnet\", [\"build\", \"-c\", \"Release\"]);<\/code><\/pre>\n<h3>Process.ReadAllLines and Process.ReadAllLinesAsync<\/h3>\n<p>If you need to capture output and error as lines, you can use <code>ReadAllLines<\/code> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127106\">PR<\/a>) and <code>ReadAllLinesAsync<\/code> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126987\">PR<\/a>) methods, which are implemented in the same way as their <code>ReadAllText<\/code> counterparts, but return an enumerable of <code>ProcessOutputLine<\/code> instead of a single string.<\/p>\n<pre><code class=\"language-csharp\">namespace System.Diagnostics;\r\n\r\npublic readonly struct ProcessOutputLine\r\n{\r\n    public ProcessOutputLine(string content, bool standardError);\r\n    public string Content { get; }\r\n    public bool StandardError { get; }\r\n}\r\n\r\npublic class Process\r\n{\r\n    public IEnumerable&lt;ProcessOutputLine&gt; ReadAllLines(TimeSpan? timeout = default);\r\n    public IAsyncEnumerable&lt;ProcessOutputLine&gt; ReadAllLinesAsync(CancellationToken cancellationToken = default);\r\n}<\/code><\/pre>\n<p>Let\u2019s see how we can use it to read output and error from the process line by line as they are produced:<\/p>\n<pre><code class=\"language-csharp\">using Process process = Process.Start(\"dotnet\", \"--help\")!;\r\nawait foreach (ProcessOutputLine line in process.ReadAllLinesAsync())\r\n{\r\n    if (line.StandardError)\r\n        Console.ForegroundColor = ConsoleColor.Red;\r\n\r\n    Console.WriteLine(line.Content);\r\n    Console.ResetColor();\r\n}<\/code><\/pre>\n<h3>Process.ReadAllBytes and Process.ReadAllBytesAsync<\/h3>\n<p>If you need to capture output and error as bytes, you can use <code>ReadAllBytes<\/code> method, which is internally used by <code>ReadAllText<\/code> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126807\">PR<\/a>). It returns byte arrays instead of strings.<\/p>\n<pre><code class=\"language-csharp\">public class Process\r\n{\r\n    public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(TimeSpan? timeout = default);\r\n    public Task&lt;(byte[] StandardOutput, byte[] StandardError)&gt; ReadAllBytesAsync(CancellationToken cancellationToken = default);\r\n}<\/code><\/pre>\n<h3>Timeouts and cancellation<\/h3>\n<p>All the aforementioned methods that read from standard output and error support timeouts and cancellation. If the timeout is reached or the cancellation token is cancelled before the end of the stream is reached, the methods will throw <code>TimeoutException<\/code> or <code>OperationCanceledException<\/code> respectively. The high-level <code>RunAndCaptureText[Async]<\/code> and <code>Run[Async]<\/code> methods will also try to kill the process to avoid leaving it running.<\/p>\n<h3>Multiplexing and other optimizations under the hood<\/h3>\n<p>The new methods are not just simpler to use, but also faster. Behind the scenes, the synchronous <code>Process.RunAndCaptureText<\/code> and <code>Process.ReadAll[Bytes\/Text]<\/code> methods use multiplexing (<a href=\"https:\/\/man7.org\/linux\/man-pages\/man2\/poll.2.html\">poll<\/a> on Unix and <a href=\"https:\/\/learn.microsoft.com\/windows\/win32\/api\/synchapi\/nf-synchapi-waitformultipleobjects\">WaitForMultipleObjects<\/a> on Windows) to read from both standard output and error using a single thread. They also implement a bunch of other optimizations such as using <code>ArrayPool<\/code> to reduce memory allocations. The asynchronous <code>Process.RunAndCaptureTextAsync<\/code> and <code>Process.ReadAllTextAsync<\/code> methods use asynchronous I\/O operations without blocking any threads.<\/p>\n<pre><code class=\"language-csharp\">using BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Diagnostics;\r\nusing System.Text;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(CaptureOutputBenchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser, ThreadingDiagnoser]\r\npublic class CaptureOutputBenchmarks\r\n{\r\n    private readonly ProcessStartInfo _processStartInfo = CreateStartInfo();\r\n\r\n    private static ProcessStartInfo CreateStartInfo()\r\n    {\r\n        ProcessStartInfo startInfo = OperatingSystem.IsWindows()\r\n            ? new(\"cmd.exe\", \"\/c for \/L %i in (1,1,1000) do @echo Line %i\")\r\n            : new(\"sh\", [\"-c\", \"for i in $(seq 1 1000); do echo \"Line $i\"; done\"]);\r\n\r\n        startInfo.RedirectStandardOutput = true;\r\n        startInfo.RedirectStandardError = true;\r\n\r\n        return startInfo;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int Events()\r\n    {\r\n        using Process process = new();\r\n        process.StartInfo = _processStartInfo;\r\n\r\n        StringBuilder stdOut = new(), stdErr = new();\r\n\r\n        process.OutputDataReceived += (sender, e) =&gt; stdOut.AppendLine(e.Data);\r\n        process.ErrorDataReceived += (sender, e) =&gt; stdErr.AppendLine(e.Data);\r\n\r\n        process.Start();\r\n\r\n        process.BeginOutputReadLine();\r\n        process.BeginErrorReadLine();\r\n\r\n        process.WaitForExit();\r\n\r\n        \/\/ Other benchmarks materialize the output, so we do it here\r\n        \/\/ to ensure it's apples to apples comparison.\r\n        _ = stdOut.ToString();\r\n        _ = stdErr.ToString();\r\n\r\n        return process.ExitCode;\r\n    }\r\n\r\n    [Benchmark]\r\n    public async Task&lt;int&gt; ReadToEndAsync()\r\n    {\r\n        using Process process = Process.Start(_processStartInfo)!;\r\n\r\n        Task&lt;string&gt; readOutput = process.StandardOutput.ReadToEndAsync();\r\n        Task&lt;string&gt; readError = process.StandardError.ReadToEndAsync();\r\n\r\n        _ = await readOutput;\r\n        _ = await readError;\r\n\r\n        await process.WaitForExitAsync();\r\n\r\n        return process.ExitCode;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int RunAndCaptureText()\r\n    {\r\n        ProcessTextOutput processTextOutput = Process.RunAndCaptureText(_processStartInfo);\r\n\r\n        _ = processTextOutput.StandardOutput;\r\n        _ = processTextOutput.StandardError;\r\n\r\n        return processTextOutput.ExitStatus.ExitCode;\r\n    }\r\n\r\n    [Benchmark]\r\n    public async Task&lt;int&gt; RunAndCaptureTextAsync()\r\n    {\r\n        ProcessTextOutput processTextOutput = await Process.RunAndCaptureTextAsync(_processStartInfo);\r\n\r\n        _ = processTextOutput.StandardOutput;\r\n        _ = processTextOutput.StandardError;\r\n\r\n        return processTextOutput.ExitStatus.ExitCode;\r\n    }\r\n}<\/code><\/pre>\n<pre><code class=\"language-ini\">BenchmarkDotNet v0.16.0-nightly.20260505.517, Windows 11 (10.0.26200.8246\/25H2\/2025Update\/HudsonValley2)\r\nAMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores\r\nMemory: 63.86 GB Total, 44.02 GB Available\r\n.NET SDK 11.0.100-preview.5.26255.101\r\n  [Host]     : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3\r\n  DefaultJob : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Mean<\/th>\n<th>Completed Work Items<\/th>\n<th>Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Events (old)<\/td>\n<td>71.21 ms<\/td>\n<td>2006.0000<\/td>\n<td>612.58 KB<\/td>\n<\/tr>\n<tr>\n<td>ReadToEndAsync (old)<\/td>\n<td>70.33 ms<\/td>\n<td>2004.0000<\/td>\n<td>636.67 KB<\/td>\n<\/tr>\n<tr>\n<td>RunAndCaptureText (new)<\/td>\n<td>68.11 ms<\/td>\n<td>\u2013<\/td>\n<td>132.58 KB<\/td>\n<\/tr>\n<tr>\n<td>RunAndCaptureTextAsync (new)<\/td>\n<td>70.66 ms<\/td>\n<td>2004.0000<\/td>\n<td>534.09 KB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<pre><code class=\"language-log\">\/\/ * Legends *\r\n  Completed Work Items : The number of work items that have been processed in ThreadPool (per single operation)\r\n  Allocated            : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)<\/code><\/pre>\n<p>As you can see, on Windows the synchronous <code>RunAndCaptureText<\/code> method is about 2-3 ms faster than the old approaches and allocates about 4.5x less memory. It also doesn\u2019t use thread pool at all.<\/p>\n<pre><code class=\"language-ini\">BenchmarkDotNet v0.16.0-nightly.20260505.517, Linux Ubuntu 24.04.4 LTS (Noble Numbat)\r\nAMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores\r\nMemory: 31.27 GB Total, 29.69 GB Available\r\n.NET SDK 11.0.100-preview.5.26255.101\r\n  [Host]     : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3\r\n  DefaultJob : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Mean<\/th>\n<th>Completed Work Items<\/th>\n<th>Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Events (old)<\/td>\n<td>4.494 ms<\/td>\n<td>90.8359<\/td>\n<td>178.79 KB<\/td>\n<\/tr>\n<tr>\n<td>ReadToEndAsync (old)<\/td>\n<td>4.831 ms<\/td>\n<td>78.0313<\/td>\n<td>108.43 KB<\/td>\n<\/tr>\n<tr>\n<td>RunAndCaptureText (new)<\/td>\n<td>4.488 ms<\/td>\n<td>\u2013<\/td>\n<td>48.9 KB<\/td>\n<\/tr>\n<tr>\n<td>RunAndCaptureTextAsync (new)<\/td>\n<td>4.738 ms<\/td>\n<td>84.1641<\/td>\n<td>81.6 KB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>On Linux, the new methods allocate about 2-4x less memory and the synchronous method also does not use thread pool at all.<\/p>\n<h2>Handle inheritance<\/h2>\n<p>In case of pipes, the end of the file (EOF) is reached when <strong>all<\/strong> handles to its write end have been closed. <code>Process<\/code> closes its own copy of the write end of the pipe after starting the process, but in order for the pipe to be used by the child process, it must be inherited, which requires the pipe to be inheritable. When a process is started, it by default inherits all inheritable handles from the parent process, which opens the door for another two issues that can lead to deadlocks:<\/p>\n<ul>\n<li>when a sibling process started concurrently inherits the pipe handle and keeps it open,<\/li>\n<li>when a grandchild process inherits the pipe handle and keeps it open after the child process exits.<\/li>\n<\/ul>\n<p>From .NET perspective, there is no way to prevent this kind of accidental handle inheritance by the grandchild process, as the child process can do whatever it wants with the handles it inherits from the parent. The best we can do is to provide an API that allows the child process to inherit only selected handles. And this is exactly what we have done by introducing <code>ProcessStartInfo.InheritedHandles<\/code> property (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126318\">PR<\/a>) that allows you to specify which handles should be inherited by the child process.<\/p>\n<pre><code class=\"language-csharp\">public class ProcessStartInfo\r\n{\r\n    public IList&lt;SafeHandle&gt;? InheritedHandles { get; set; } = null;\r\n}<\/code><\/pre>\n<p>Due to backwards compatibility reasons, the new property is set to <code>null<\/code> by default, which means that the behavior is the same as before \u2013 all inheritable handles are inherited. If you set it to an empty list, only standard handles will be inherited. If you set it to a list of specific handles, those handles will be inherited along with the standard handles.<\/p>\n<p><strong>Request for feedback:<\/strong> we are considering extending all the new APIs that capture process output with an ability to stop when the process exits but the pipes remain open. Please let us know if you are interested in this feature.<\/p>\n<p><strong>Important: handles in this list should not have inheritance enabled beforehand.<\/strong> If they do, they could be unintentionally inherited by other processes started concurrently with different APIs, which may lead to security or resource management issues.<\/p>\n<p><strong>Note:<\/strong> as of today only <code>SafeFileHandle<\/code> and <code>SafePipeHandle<\/code> are supported, if you need more, please let us know.<\/p>\n<p><strong>Performance implications:<\/strong> if <code>InheritedHandles<\/code> is not <code>null<\/code>:<\/p>\n<p>In short: as long as you don\u2019t run on old Linux kernels (prior to 5.9), there should be no performance regression compared to the old behavior when inheriting all handles.<\/p>\n<ul>\n<li>On Windows we acquire only a reader lock when starting new process. It means that if you are starting multiple processes in parallel and they are all setting <code>InheritedHandles<\/code>, they won\u2019t be blocked on process creation as they would be if we used a global lock.<\/li>\n<li>On Unix, we use the best available sys-call to ensure that only the specified handles are inherited by the child process:\n<ul>\n<li>On Apple platforms, we always use <code>posix_spawn<\/code> with <code>POSIX_SPAWN_CLOEXEC_DEFAULT<\/code> flag, which is supported by all versions supported by current .NET.<\/li>\n<li>On Linux, we use <code>close_range<\/code> or <code>__NR_close_range<\/code> if they are available and enabled.<\/li>\n<li>On FreeBSD we use <code>close_range<\/code> (available since FreeBSD 12.2).<\/li>\n<li>On Illumos\/Solaris, we use <code>fdwalk<\/code>.<\/li>\n<li>If none of the above is available (or enabled), we fallback to iterating over all file descriptors and setting <code>FD_CLOEXEC<\/code> manually. This is expensive and can cause major performance regression. Mostly for Linux kernels prior to 5.9 (except RHEL 8.0 which <a href=\"https:\/\/docs.redhat.com\/en\/documentation\/red_hat_enterprise_linux\/8\/html\/8.4_release_notes\/new-features\">backported<\/a> it).<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>Let\u2019s benchmark the performance implications!<\/p>\n<pre><code class=\"language-csharp\">public class GlobalLock\r\n{\r\n    private ProcessStartInfo info;\r\n\r\n    [Params(true, false)]\r\n    public bool SetInheritedHandles { get; set; }\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        info = OperatingSystem.IsWindows()\r\n            ? new(\"cmd.exe\", [\"\/c\", \"exit 42\"])\r\n            : new(\"sh\", [\"-c\", \"exit 42\"]);\r\n\r\n        info.InheritedHandles = SetInheritedHandles ? [] : null;\r\n    }\r\n\r\n    [Benchmark]\r\n    public ParallelLoopResult Run() =&gt; Parallel.For(0, 1_000, (_, _) =&gt; _ = Process.Run(info));\r\n}<\/code><\/pre>\n<p>We can see that on this Windows machine, the throughput has doubled:<\/p>\n<pre><code class=\"language-ini\">BenchmarkDotNet v0.16.0-nightly.20260505.517, Windows 11 (10.0.26200.8246\/25H2\/2025Update\/HudsonValley2)\r\nAMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores\r\nMemory: 63.86 GB Total, 32.5 GB Available\r\n.NET SDK 11.0.100-preview.5.26255.101\r\n  [Host] : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3\r\n  Dry    : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3\r\n\r\nInvocationCount=1  IterationCount=10  LaunchCount=1\r\nUnrollFactor=1  WarmupCount=1<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>SetInheritedHandles<\/th>\n<th>Mean<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Run<\/td>\n<td>False<\/td>\n<td>4.014 s<\/td>\n<\/tr>\n<tr>\n<td>Run<\/td>\n<td>True<\/td>\n<td>1.958 s<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Redirecting standard handles<\/h2>\n<p>Another way to limit handle inheritance issues is to let the users redirect standard handles to any file handle they want, without the need to make it inheritable. It\u2019s now possible to redirect standard handles to any file handle (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125848\">PR<\/a>), which opens up new scenarios such as:<\/p>\n<ul>\n<li><strong>process piping<\/strong>,<\/li>\n<li>redirecting to a file,<\/li>\n<li>redirecting to a null handle (it reports <code>0<\/code> bytes read\/written for every operation):\n<ul>\n<li>starting process with no input,<\/li>\n<li>discarding output,<\/li>\n<\/ul>\n<\/li>\n<li>starting process with async handles (advanced, niche scenario),<\/li>\n<li>breaking inheritance chain by redirecting standard handles to other handles than parent process handles.<\/li>\n<\/ul>\n<p>The new API comes with a few new enablers (<code>File.OpenHandle<\/code> already existed) that make it easier to use:<\/p>\n<pre><code class=\"language-csharp\">namespace System.Diagnostics\r\n{\r\n    public class ProcessStartInfo\r\n    {\r\n        public SafeFileHandle? StandardInputHandle { get; set; }\r\n        public SafeFileHandle? StandardOutputHandle { get; set; }\r\n        public SafeFileHandle? StandardErrorHandle { get; set; }\r\n    }\r\n}\r\n\r\nnamespace Microsoft.Win32.SafeHandles\r\n{\r\n    public class SafeFileHandle\r\n    {\r\n        public static SafeFileHandle CreateAnonymousPipe(out SafeFileHandle readPipe, out SafeFileHandle writePipe, \r\n            bool asyncRead = false, bool asyncWrite = false);\r\n    }\r\n}\r\n\r\nnamespace System.IO\r\n{\r\n    public static class File\r\n    {\r\n        public static SafeFileHandle OpenHandle(string path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read, FileShare share = FileShare.Read, FileOptions options = FileOptions.None, long preallocationSize = 0);\r\n        public static SafeFileHandle OpenNullHandle();\r\n    }\r\n}\r\n\r\nnamespace System\r\n{\r\n    public static class Console\r\n    {\r\n        public static SafeFileHandle OpenStandardInputHandle();\r\n        public static SafeFileHandle OpenStandardOutputHandle();\r\n        public static SafeFileHandle OpenStandardErrorHandle();\r\n    }\r\n}<\/code><\/pre>\n<p>Let\u2019s pipe <code>ls \/usr\/bin<\/code> into <code>grep zip<\/code> and redirect the output to a file to find zip-related commands:<\/p>\n<pre><code class=\"language-cmd\">ls \/usr\/bin | grep zip &gt; output.txt<\/code><\/pre>\n<p>And now we\u2019re going to implement the same thing in C# with the new APIs.<\/p>\n<pre><code class=\"language-csharp\">SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readPipe, out SafeFileHandle writePipe);\r\n\r\nusing (readPipe)\r\nusing (writePipe)\r\nusing (SafeFileHandle outputFile = File.OpenHandle(\"output.txt\", FileMode.Create, FileAccess.Write))\r\n{\r\n    ProcessStartInfo producer = new(\"ls\", [\"\/usr\/bin\"])\r\n    {\r\n        StandardOutputHandle = writePipe\r\n    };\r\n\r\n    \/\/ Start consumer with input from the read end of the pipe, writing output to file\r\n    ProcessStartInfo consumer = new(\"grep\", [\"zip\"])\r\n    {\r\n        StandardInputHandle = readPipe,\r\n        StandardOutputHandle = outputFile,\r\n    };\r\n\r\n    using Process producerProcess = Process.Start(producer)!;\r\n    \/\/ The producer process has its own copy of the write end of the pipe, we need to dispose the parent copy.\r\n    writePipe.Dispose();\r\n\r\n    using Process consumerProcess = Process.Start(consumer)!;\r\n    \/\/ The consumer process has its own copy of the read end of the pipe, we need to dispose the parent copy.\r\n    readPipe.Dispose();\r\n\r\n    await producerProcess.WaitForExitAsync();\r\n    await consumerProcess.WaitForExitAsync();\r\n}<\/code><\/pre>\n<p><strong>Note:<\/strong> all of the new enablers create non-inheritable handles but <code>Process<\/code> knows how to make them inheritable when starting the process, so you don\u2019t have to worry about handle inheritance at all.<\/p>\n<h3>Other SafeFileHandle improvements<\/h3>\n<p>It\u2019s worth noting that <code>SafeFileHandle<\/code> class has also received some new features that make it easier to work with:<\/p>\n<ul>\n<li><code>Type<\/code> property to check if the handle type is a file, pipe, console, etc (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124561\">PR<\/a>).<\/li>\n<li><code>IsAsync<\/code> returns <code>true<\/code> on Unix only if the handle has <code>O_NONBLOCK<\/code> flag enabled.<\/li>\n<li>All read and write <code>RandomAccess<\/code> methods now support non-seekable handles (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125512\">PR<\/a>) such as pipes, which makes it possible to use them without the need to use <code>FileStream<\/code>.<\/li>\n<\/ul>\n<pre><code class=\"language-csharp\">namespace System.IO\r\n{\r\n    public enum FileHandleType\r\n    {\r\n        Unknown = 0,\r\n        RegularFile,\r\n        Pipe,\r\n        Socket,\r\n        CharacterDevice,\r\n        Directory,\r\n        SymbolicLink,\r\n        BlockDevice,\r\n    }\r\n}\r\n\r\nnamespace Microsoft.Win32.SafeHandles\r\n{\r\n    public class SafeFileHandle\r\n    {\r\n        public FileHandleType Type { get; }\r\n    }\r\n}<\/code><\/pre>\n<h2>Lifetime management<\/h2>\n<h3>Process.StartAndForget<\/h3>\n<p>There is a common misconception that when a process is disposed, it\u2019s also being killed. This is not the case, as <code>Process.Dispose<\/code> only releases the resources associated with the process, but does not kill it.<\/p>\n<p>To make it easier to start a process without the need to worry about disposing it, we have introduced <code>Process.StartAndForget<\/code> method that starts a process, returns its ID and releases all resources associated with it (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126078\">PR<\/a>).<\/p>\n<pre><code class=\"language-csharp\">public class Process\r\n{\r\n    public static int StartAndForget(ProcessStartInfo startInfo);\r\n    public static int StartAndForget(string fileName, IList&lt;string&gt;? arguments = null);\r\n}<\/code><\/pre>\n<p>The usage is straightforward:<\/p>\n<pre><code class=\"language-csharp\">int processId = Process.StartAndForget(\"notepad.exe\");<\/code><\/pre>\n<h3>ProcessStartInfo.KillOnParentExit<\/h3>\n<p>Processes started by the parent process are not automatically terminated when the parent process exits. This can lead to orphaned processes that keep running in the background, which is not desirable in many scenarios. To address this issue, we have introduced <code>ProcessStartInfo.KillOnParentExit<\/code> property that ensures that the child process is killed when the parent process exits (including forced terminations and crashes).<\/p>\n<pre><code class=\"language-csharp\">public class ProcessStartInfo\r\n{\r\n    [SupportedOSPlatform(\"windows\")] \/\/ introduced in .NET 11 Preview 4\r\n    [SupportedOSPlatform(\"linux\")] \/\/ introduced in .NET 11 Preview 5\r\n    [SupportedOSPlatform(\"android\")] \/\/ introduced in .NET 11 Preview 5\r\n    public bool KillOnParentExit { get; set; }\r\n}<\/code><\/pre>\n<p>This is achieved by using platform-specific features such as <code>JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE<\/code> on Windows (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126699\">PR<\/a>) and <code>PR_SET_PDEATHSIG<\/code> on Linux and Android (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127112\">PR<\/a>). In contrast to other APIs, the behavior is slightly different on different platforms:<\/p>\n<ul>\n<li>On Windows, we need to use <code>Job<\/code> object to ensure that the child process is killed when the parent process exits. Job objects are by default inherited by all child processes, so if the child process spawns another process (a grandchild), that grandchild will also be terminated when the parent process exits.<\/li>\n<li>On Linux and Android, we use <code>PR_SET_PDEATHSIG<\/code> to specify a <code>SIGKILL<\/code> that the kernel will send to the child process when the <strong>thread<\/strong> that created the process exits. Since both Thread Pool and user threads can be terminated at any time, we maintain a dedicated thread used only for spawning processes with <code>KillOnParentExit<\/code> enabled, to ensure that the child processes are killed when the parent process exits. So when there are multiple processes started with <code>KillOnParentExit<\/code>, a synchronization mechanism is used to ensure that the dedicated thread spawns one process at a time.<\/li>\n<\/ul>\n<p><strong>Request for feedback:<\/strong> we are considering extending the API to support killing child processes when the parent process exits on other Unix platforms as well. Since none of them provides a similar mechanism, we could handle only normal (<code>atexit<\/code>) and graceful terminations (<code>SIGTERM<\/code> etc). If you are interested in this feature, please let us know.<\/p>\n<h3>ProcessStartInfo.StartDetached<\/h3>\n<p><code>ProcessStartInfo.StartDetached<\/code> property allows you to start a process that is detached from the parent process, which means that it will keep running even if the parent process exits, gets signaled or terminal is closed. This is achieved by using platform-specific features such as <code>DETACHED_PROCESS<\/code> flag on Windows and <code>setsid<\/code> on Unix (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126632\">PR<\/a>).<\/p>\n<pre><code class=\"language-csharp\">public class ProcessStartInfo\r\n{\r\n    public bool StartDetached { get; set; }\r\n}<\/code><\/pre>\n<p>Moreover, if <code>StartDetached<\/code> is set to <code>true<\/code> and no redirection for standard handles is specified, standard handles will be redirected to <code>null<\/code> handle to avoid keeping parent standard handles open unnecessarily.<\/p>\n<h2>SafeProcessHandle<\/h2>\n<p>Sometimes <code>Process<\/code> doesn\u2019t cover your scenario \u2013 for example, you may need to P\/Invoke <code>CreateProcessAsUser<\/code> on Windows or use a custom <code>posix_spawn<\/code> configuration on Unix. In those cases, you already have an OS process handle, but so far <code>SafeProcessHandle<\/code> has not provided any public APIs other than constructor. We\u2019ve extended it with a set of focused APIs for the most common operations:<\/p>\n<pre><code class=\"language-csharp\">namespace Microsoft.Win32.SafeHandles\r\n{\r\n    public class SafeProcessHandle : SafeHandle\r\n    {\r\n        public int ProcessId { get; }\r\n        public void Kill();\r\n        public bool Signal(PosixSignal signal);\r\n        public static SafeProcessHandle Start(ProcessStartInfo startInfo);\r\n        public bool TryWaitForExit(System.TimeSpan timeout, [NotNullWhen(true)] out ProcessExitStatus? exitStatus);\r\n        public ProcessExitStatus WaitForExit();\r\n        public Task&lt;ProcessExitStatus&gt; WaitForExitAsync(CancellationToken cancellationToken = default);\r\n        public Task&lt;ProcessExitStatus&gt; WaitForExitOrKillOnCancellationAsync(CancellationToken cancellationToken);\r\n        public ProcessExitStatus WaitForExitOrKillOnTimeout(TimeSpan timeout);\r\n    }\r\n}<\/code><\/pre>\n<p>The <code>Process<\/code> class itself already exposes <code>SafeProcessHandle<\/code> via <code>Process.SafeHandle<\/code> property, so you can use the new APIs even if you are using <code>Process<\/code> class:<\/p>\n<pre><code class=\"language-csharp\">[UnsupportedOSPlatform(\"windows\")] \/\/ SIGTERM is not supported on Windows\r\nProcessExitStatus TerminateProcess(Process process)\r\n{\r\n    \/\/ First try to terminate the process gracefully with SIGTERM\r\n    process.SafeHandle.Signal(PosixSignal.SIGTERM);\r\n    if (process.SafeHandle.TryWaitForExit(TimeSpan.FromSeconds(3), out ProcessExitStatus? exitStatus))\r\n    {\r\n        return exitStatus;\r\n    }\r\n\r\n    \/\/ If the process is still running after the timeout, kill it forcefully with SIGKILL\r\n    process.SafeHandle.Signal(PosixSignal.SIGKILL);\r\n    return process.SafeHandle.WaitForExit();\r\n}<\/code><\/pre>\n<p>Or if you want to kill the process if it doesn\u2019t exit within a certain timeout:<\/p>\n<pre><code class=\"language-csharp\">using SafeProcessHandle processHandle = SafeProcessHandle.Start(new ProcessStartInfo(\"myapp.exe\"));\r\nProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMinutes(1));\r\nif (exitStatus.Canceled)\r\n{\r\n    Console.WriteLine(\"The process was killed after timeout.\");\r\n}<\/code><\/pre>\n<h3>Trimmability<\/h3>\n<p><code>SafeProcessHandle<\/code> also offers better trimmability. Let\u2019s publish a NativeAOT app that starts a process and waits for it to exit.<\/p>\n<p>Using <code>SafeProcessHandle<\/code>:<\/p>\n<pre><code class=\"language-csharp\">using SafeProcessHandle handle = SafeProcessHandle.Start(new ProcessStartInfo(\"whoami\"));\r\nhandle.WaitForExit();<\/code><\/pre>\n<p>And <code>Process<\/code>:<\/p>\n<pre><code class=\"language-csharp\">using Process process = Process.Start(new ProcessStartInfo(\"whoami\"))!;\r\nprocess.WaitForExit();<\/code><\/pre>\n<pre><code class=\"language-cmd\">dotnet publish -c Release -r win-x64 -p:PublishAot=true\r\ndotnet publish -c Release -r linux-x64 -p:PublishAot=true<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Type<\/th>\n<th>.NET Version<\/th>\n<th>OS<\/th>\n<th>Size (bytes)<\/th>\n<th>vs .NET 10 Process<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Process<\/td>\n<td>.NET 10<\/td>\n<td>Windows x64<\/td>\n<td>1 730 048<\/td>\n<td>baseline<\/td>\n<\/tr>\n<tr>\n<td>Process<\/td>\n<td>.NET 11<\/td>\n<td>Windows x64<\/td>\n<td>1 389 056<\/td>\n<td>-19.7%<\/td>\n<\/tr>\n<tr>\n<td>SafeProcessHandle<\/td>\n<td>.NET 11<\/td>\n<td>Windows x64<\/td>\n<td>1 178 624<\/td>\n<td>-31.9%<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>Process<\/td>\n<td>.NET 10<\/td>\n<td>Linux x64<\/td>\n<td>2 113 808<\/td>\n<td>baseline<\/td>\n<\/tr>\n<tr>\n<td>Process<\/td>\n<td>.NET 11<\/td>\n<td>Linux x64<\/td>\n<td>2 043 768<\/td>\n<td>-3.3%<\/td>\n<\/tr>\n<tr>\n<td>SafeProcessHandle<\/td>\n<td>.NET 11<\/td>\n<td>Linux x64<\/td>\n<td>1 816 504<\/td>\n<td>-14.1%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The size on disk improvements for <code>Process<\/code> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126338\">PR<\/a>) include a community <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126449\">contribution<\/a> from Red Hat. It\u2019s worth noting that <a href=\"https:\/\/github.com\/tmds\">Tom Deseyn<\/a> from Red Hat has contributed a LOT to this release by reviewing the Linux implementations of the new APIs, so a big thank you to him!<\/p>\n<h2>Notable performance improvements<\/h2>\n<h3>Improved scalability on Windows<\/h3>\n<p>So far, both <code>Process.BeginOutputReadLine<\/code> and <code>Process.BeginErrorReadLine<\/code> were creating a background task that was performing blocking read on the output\/error pipe. So for every process that was started with redirected output and error that used the <code>Begin[Output\/Error]ReadLine<\/code> methods, two thread pool threads were being blocked (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/81896\">#81896<\/a>). For years, we have believed that it\u2019s impossible to solve this issue on Windows, as there is no support for truly asynchronous I\/O operations on anonymous pipes.<\/p>\n<p>But when reading the <a href=\"https:\/\/learn.microsoft.com\/windows\/win32\/api\/namedpipeapi\/nf-namedpipeapi-createpipe\">documentation<\/a>, we have found out that:<\/p>\n<blockquote>\n<p>\u201cAnonymous pipes are implemented using a named pipe with a unique name. Therefore, you can often pass a handle to an anonymous pipe to a function that requires a handle to a named pipe.\u201d<\/p>\n<\/blockquote>\n<p>We knew that named pipes support truly asynchronous I\/O operations on Windows and combined with our previous experience from <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/file-io-improvements-in-dotnet-6\/\">File IO improvements in .NET 6<\/a> we knew that it\u2019s possible to open one end of named pipe for asynchronous IO and the other end for synchronous IO (99.99% of applications expect standard handles to be opened for synchronous IO).<\/p>\n<p>We studied the <code>CreatePipe<\/code> implementation and ensured (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125643\">PR<\/a>) that the new approach does not introduce any breaking changes. Starting from .NET 11 Preview 4, on Windows, when you start a process with redirected output and error, the pipes are created as named pipes with the read end opened for asynchronous IO and the write end opened for synchronous IO. This allows us to use truly asynchronous IO operations on the output and error pipes without blocking any threads.<\/p>\n<p>Last but not least, we have exposed the ability to create anonymous pipes using <code>SafeFileHandle.CreateAnonymousPipe<\/code> method, which creates a pair of connected pipes and returns their handles. This method is available on both Windows and Unix, and it abstracts away the platform-specific details of creating pipes.<\/p>\n<p>All of that translates into much better scalability when starting multiple processes in parallel with redirected output and error on Windows, as we are no longer blocking thread pool threads for every process.<\/p>\n<pre><code class=\"language-csharp\">public class BeginReadLineBenchmarks\r\n{\r\n    private static readonly ProcessStartInfo _processStartInfo = CreateStartInfo();\r\n\r\n    private static ProcessStartInfo CreateStartInfo()\r\n    {\r\n        ProcessStartInfo startInfo = OperatingSystem.IsWindows()\r\n            ? new(\"cmd.exe\", \"\/c for \/L %i in (1,1,1000) do @echo Line %i\")\r\n            : new(\"sh\", [\"-c\", \"for i in $(seq 1 1000); do echo \"Line $i\"; done\"]);\r\n\r\n        startInfo.RedirectStandardOutput = true;\r\n        startInfo.RedirectStandardError = true;\r\n\r\n        return startInfo;\r\n    }\r\n\r\n    [Benchmark]\r\n    public ParallelLoopResult Run() =&gt; Parallel.For(0, 300, static (_, _) =&gt; _ = Events());\r\n\r\n    private static int Events()\r\n    {\r\n        using Process process = new();\r\n        process.StartInfo = _processStartInfo;\r\n\r\n        StringBuilder stdOut = new(), stdErr = new();\r\n\r\n        process.OutputDataReceived += (sender, e) =&gt; stdOut.AppendLine(e.Data);\r\n        process.ErrorDataReceived += (sender, e) =&gt; stdErr.AppendLine(e.Data);\r\n\r\n        process.Start();\r\n\r\n        process.BeginOutputReadLine();\r\n        process.BeginErrorReadLine();\r\n\r\n        process.WaitForExit();\r\n\r\n        return process.ExitCode;\r\n    }\r\n}<\/code><\/pre>\n<pre><code class=\"language-ini\">BenchmarkDotNet v0.16.0-nightly.20260505.517, Windows 11 (10.0.26200.8246\/25H2\/2025Update\/HudsonValley2)\r\nAMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores\r\nMemory: 63.86 GB Total, 39.4 GB Available\r\n.NET SDK 11.0.100-preview.5.26255.101\r\n  [Host]    : .NET 10.0.7 (10.0.7, 10.0.726.21808), X64 RyuJIT x86-64-v3\r\n  .NET 10.0 : .NET 10.0.7 (10.0.7, 10.0.726.21808), X64 RyuJIT x86-64-v3\r\n  .NET 11.0 : .NET 11.0.0 (11.0.0-preview.5.26255.101, 11.0.26.25601), X64 RyuJIT x86-64-v3<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Job<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Run<\/td>\n<td>.NET 10.0<\/td>\n<td>.NET 10.0<\/td>\n<td>5.307 s<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Run<\/td>\n<td>.NET 11.0<\/td>\n<td>.NET 11.0<\/td>\n<td>2.936 s<\/td>\n<td>0.57<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>As you can see, for this particular micro-benchmark and machine, the throughput has improved by about 1.8x. The improvement will be even more significant when starting more processes in parallel, as we are no longer blocking thread pool threads for every process.<\/p>\n<h3>Improved process creation on apple platforms<\/h3>\n<p>In order to implement <code>ProcessStartInfo.InheritedHandles<\/code> on apple platforms (macOS, MacCatalyst), we had to switch from <code>fork<\/code> + <code>exec<\/code> to <code>posix_spawn<\/code> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126063\">PR<\/a>). To tell the long story short, it offers much better performance on apple platforms, especially on the arm64 architecture.<\/p>\n<p>When running following benchmarks:<\/p>\n<pre><code class=\"language-csharp\">[MemoryDiagnoser]\r\npublic class ProcessStartBenchmarks\r\n{\r\n    private static ProcessStartInfo s_startProcessStartInfo = new ProcessStartInfo()\r\n    {\r\n        FileName = \"whoami\", \/\/ exists on both Windows and Unix, and has very short output\r\n        RedirectStandardOutput = true \/\/ avoid visible output\r\n    };\r\n\r\n    private Process? _startedProcess;\r\n\r\n    [Benchmark]\r\n    public void Start()\r\n    {\r\n        _startedProcess = Process.Start(s_startProcessStartInfo)!;\r\n    }\r\n\r\n    [IterationCleanup(Target = nameof(Start))]\r\n    public void CleanupStart()\r\n    {\r\n        if (_startedProcess != null)\r\n        {\r\n            _startedProcess.WaitForExit();\r\n            _startedProcess.Dispose();\r\n            _startedProcess = null;\r\n        }\r\n    }\r\n\r\n    [Benchmark]\r\n    public void StartAndWaitForExit()\r\n    {\r\n        using (Process p = Process.Start(s_startProcessStartInfo)!)\r\n        {\r\n            p.WaitForExit();\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>We have <a href=\"https:\/\/github.com\/EgorBot\/Benchmarks\/issues\/73#issuecomment-4126314293\">observed<\/a> an impressive improvement of about <strong>98x<\/strong> on Apple Silicon and about 4.5x on x64 machines:<\/p>\n<pre><code class=\"language-ini\">BenchmarkDotNet v0.15.8, macOS Sequoia 15.4.1 (24E263) [Darwin 24.4.0]\r\nApple M4, 1 CPU, 10 logical and 10 physical cores\r\n.NET SDK 11.0.100-preview.3.26174.112\r\n  [Host]     : .NET 10.0.5 (10.0.5, 10.0.526.15411), Arm64 RyuJIT armv8.0-a<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Toolchain<\/th>\n<th>Mean<\/th>\n<th>Error<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>\/PR_126063\/corerun<\/td>\n<td>1,246.5 us<\/td>\n<td>5.26 us<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>\/main\/corerun<\/td>\n<td>8,945.9 us<\/td>\n<td>80.30 us<\/td>\n<td>7.18<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>Start<\/td>\n<td>\/PR_126063\/corerun<\/td>\n<td>122.0 us<\/td>\n<td>2.40 us<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Start<\/td>\n<td>\/main\/corerun<\/td>\n<td>12,043.2 us<\/td>\n<td>116.96 us<\/td>\n<td>98.86<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<pre><code class=\"language-ini\">BenchmarkDotNet v0.15.8, macOS Sequoia 15.2 (24C101) [Darwin 24.2.0]\r\nIntel Core i5-8500B CPU 3.00GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores\r\n.NET SDK 11.0.100-preview.3.26174.112\r\n  [Host]     : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Toolchain<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>PR #126063<\/td>\n<td>3,163.3 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>main<\/td>\n<td>4,981.3 \u03bcs<\/td>\n<td>1.58<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>Start<\/td>\n<td>PR #126063<\/td>\n<td>417.4 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Start<\/td>\n<td>main<\/td>\n<td>1,998.9 \u03bcs<\/td>\n<td>4.80<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Reduced memory allocations on Unix<\/h3>\n<p>We have also <a href=\"https:\/\/github.com\/EgorBot\/Benchmarks\/issues\/81\">reduced memory allocation<\/a> by 30-50% when starting process on Unix (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126201\">PR<\/a>).<\/p>\n<pre><code class=\"language-ini\">BenchmarkDotNet v0.15.8, macOS Sequoia 15.4.1 (24E263) [Darwin 24.4.0]\r\nApple M2, 1 CPU, 8 logical and 8 physical cores\r\n.NET SDK 11.0.100-preview.3.26178.103\r\n  [Host]     : .NET 10.0.5 (10.0.5, 10.0.526.15411), Arm64 RyuJIT armv8.0-a<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Toolchain<\/th>\n<th>Mean<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>\/bfe7a08\/corerun<\/td>\n<td>1,570.2 us<\/td>\n<td>15.83 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>\/bfe7a08~1\/corerun<\/td>\n<td>1,569.0 us<\/td>\n<td>23.92 KB<\/td>\n<td>1.51<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>Start<\/td>\n<td>\/bfe7a08\/corerun<\/td>\n<td>173.0 us<\/td>\n<td>15.83 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Start<\/td>\n<td>\/bfe7a08~1\/corerun<\/td>\n<td>176.5 us<\/td>\n<td>23.98 KB<\/td>\n<td>1.51<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Call to Action<\/h2>\n<p>All of these improvements are available today in <a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\/11.0\">.NET 11 Preview 4<\/a>. Give it a try, and let us know what you think \u2013 leave a comment here or file issues or feature requests at <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\">dotnet\/runtime<\/a>. We\u2019d love your feedback!<\/p>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/process-api-improvements-in-dotnet-11\/\">Process API Improvements in .NET 11<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>The System.Diagnostics.Process class is the primary way to create and interact with processes with .NET. We made the biggest update [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":94,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-4051","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4051","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=4051"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4051\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/94"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=4051"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=4051"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=4051"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}