Start Debugging

Process.Run vs Process.Start in .NET 11: was sollten Sie verwenden?

Verwenden Sie Process.Run, wenn Sie ein Tool starten und nur wissen wollen, wie es beendet wurde: ein Aufruf, ein Timeout, der den Kindprozess beendet, und ein ProcessExitStatus mit dem Signal. Bleiben Sie bei Process.Start, wenn Sie das Process-Objekt brauchen: Schreiben nach stdin, Lesen der Ausgabe während sie eintrifft, UseShellExecute oder das Beenden eines ganzen Prozessbaums.

Verwenden Sie Process.Run (oder Process.RunAsync), wann immer Sie einen Prozess starten, ihn zu Ende laufen lassen und nur wissen müssen, wie er beendet wurde. Es ist ein einziger Aufruf, er kann kein Process-Handle verlieren, sein Timeout oder CancellationToken beendet den Kindprozess tatsächlich, und er gibt einen ProcessExitStatus zurück, der Ihnen sagt, ob der Kindprozess normal endete, durch ein Signal beendet wurde oder von Ihnen beendet wurde. Bleiben Sie bei Process.Start, wenn Sie das laufende Process-Objekt brauchen: Schreiben nach stdin, zeilenweises Lesen der Ausgabe während der Prozess läuft, UseShellExecute zum Öffnen einer URL oder eines Dokuments, das Exited-Ereignis oder Kill(entireProcessTree: true) für Tools, die eigene Worker starten. Die Performance entscheidet es nicht: Beide lagen auf .NET 11 RC 1 bei etwa 740 Mikrosekunden pro Prozessstart. Alles Folgende wurde gegen .NET 11 RC 1 (SDK 11.0.100-rc.1.26425.128, Laufzeit 11.0.0-rc.1.26425.128, C# 15) auf macOS 26.6 mit einem Apple M4 geprüft.

Die beiden APIs im Vergleich

Verhalten (.NET 11 RC 1)Process.Run / RunAsyncProcess.Start + WaitForExit
Verfügbar ab.NET 11+jede .NET-Version
RückgabeProcessExitStatusProcess (muss disposed werden)
Standard für stdin/stdout/stderrvom Elternprozess geerbt (oder null mit silent: true)vom Elternprozess geerbt
RedirectStandardOutput = trueInvalidOperationExceptionja
Nach stdin schreiben während der Laufzeitneinja
UseShellExecute = trueInvalidOperationExceptionja
Timeoutbeendet den Kindprozess, Canceled = trueWaitForExit(TimeSpan) gibt false zurück, Kindprozess läuft weiter
Abbruch (async)beendet den Kindprozess, keine ExceptionWaitForExitAsync(ct) wirft, Kindprozess läuft weiter
Beendendes SignalProcessExitStatus.Signalaus ExitCode raten (137, 143)
Enkelprozesse bei Timeout beendennein, nur der direkte KindprozessKill(entireProcessTree: true)
Prozess-IDwird nicht zurückgegebenProcess.Id
/usr/bin/true starten, synchron737.0 us, 16.56 KB739.4 us, 16.84 KB

Die ersten fünf Zeilen entscheiden die meisten Fälle. Wenn Ihr Aufrufer StandardInput, BeginOutputReadLine oder UseShellExecute anfasst, kommt Process.Run überhaupt nicht in Frage. Wenn er nichts davon tut, ist Process.Run kürzer und hat sicherere Standardwerte.

Was Process.Run tatsächlich tut

Process.Run kam in .NET 11 Preview 4 zusammen mit RunAndCaptureText und StartAndForget als Teil der Überarbeitung der Process API, über die ich geschrieben habe, als die Deadlock-freie Ausgabeerfassung erschien. Die Oberfläche in RC 1 sieht so aus:

// .NET 11 RC 1, System.Diagnostics.Process
public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default);
public static ProcessExitStatus Run(string fileName, IEnumerable<string>? arguments = null,
    bool silent = false, TimeSpan? timeout = default);

public static Task<ProcessExitStatus> RunAsync(ProcessStartInfo startInfo,
    CancellationToken cancellationToken = default);
public static Task<ProcessExitStatus> RunAsync(string fileName, IEnumerable<string>? arguments = null,
    bool silent = false, CancellationToken cancellationToken = default);

Zwei Details weichen vom Blogbeitrag zu Preview 4 ab. Preview 7 änderte arguments von IList<string>? zu IEnumerable<string>? (dotnet/runtime#130630), und die fileName-Überladungen erhielten ein silent-Flag, das stdin, stdout und stderr auf das Null-Gerät umleitet.

Die Implementierung in Process.Scenarios.cs ist kurz genug, um sie genau zusammenzufassen. Sie erzeugt nie ein Process-Objekt. Sie ruft SafeProcessHandle.Start(startInfo) auf, dann entweder WaitForExit() oder WaitForExitOrKillOnTimeout(timeout), und gibt das Handle vor der Rückkehr frei. RunAsync macht dasselbe mit WaitForExitOrKillOnCancellationAsync. Deshalb kann es kein Handle verlieren, und deshalb lehnt es auch alles ab, was ein Process zur Steuerung braucht: Die RedirectStandard*-Flags ergeben nur Sinn, wenn jemand Process.StandardOutput hält und leert.

ProcessExitStatus hat drei Eigenschaften: ExitCode, Canceled und ein nullbares PosixSignal? Signal. Derselbe Typ kommt auch von den neuen Process.WaitForExitStatus-Methoden in RC 1 zurück.

Dieselbe Aufgabe auf beide Arten

Hier der Alltagsfall: dotnet build ausführen, die Ausgabe zur Konsole durchlassen, bei einem Fehler fehlschlagen und nach fünf Minuten aufgeben.

// .NET 11 RC 1, C# 15 -- before: Process.Start
using System.Diagnostics;

using Process build = Process.Start("dotnet", ["build", "-c", "Release"]);

if (!build.WaitForExit(TimeSpan.FromMinutes(5)))
{
    build.Kill(entireProcessTree: true);
    build.WaitForExit();
    throw new TimeoutException("dotnet build took longer than 5 minutes");
}

if (build.ExitCode != 0)
    throw new InvalidOperationException($"dotnet build failed with {build.ExitCode}");
// .NET 11 RC 1, C# 15 -- after: Process.Run
using System.Diagnostics;

ProcessExitStatus status = Process.Run(
    "dotnet", ["build", "-c", "Release"],
    timeout: TimeSpan.FromMinutes(5));

if (status.Canceled)
    throw new TimeoutException("dotnet build took longer than 5 minutes");

if (status.ExitCode != 0)
    throw new InvalidOperationException($"dotnet build failed with {status.ExitCode}");

Die Process.Start-Version ist korrekt, aber nur, weil sie zwei Dinge berücksichtigt, die regelmäßig vergessen werden: WaitForExit(TimeSpan) beendet bei einem Timeout nichts, und der Process muss disposed werden. Bei der Process.Run-Version lässt sich beides nicht falsch machen. Beachten Sie aber, dass ich entireProcessTree: true in der ersten Version absichtlich behalten habe. Mehr dazu bei den Fallstricken.

Timeouts und Abbruch verhalten sich unterschiedlich

Das ist der Unterschied, der alle trifft, die per Suchen und Ersetzen migrieren. Ich habe beide gegen sleep 10 mit einem Budget von 500 ms auf .NET 11 RC 1 laufen lassen:

// .NET 11 RC 1, C# 15, macOS 26.6
using System.Diagnostics;

using var cts = new CancellationTokenSource(500);
ProcessExitStatus s = await Process.RunAsync("sleep", ["10"], cancellationToken: cts.Token);
Console.WriteLine($"ExitCode={s.ExitCode} Canceled={s.Canceled} Signal={s.Signal}");
// ExitCode=137 Canceled=True Signal=SIGKILL   (returned after 505 ms, no exception)

using var cts2 = new CancellationTokenSource(500);
using Process p = Process.Start("sleep", ["10"]);
try { await p.WaitForExitAsync(cts2.Token); }
catch (TaskCanceledException) { Console.WriteLine($"HasExited={p.HasExited}"); }
// HasExited=False   (the child is still running)

Drei Erkenntnisse aus dieser Ausgabe:

  1. RunAsync wirft beim Abbruch nicht. Es beendet den Kindprozess mit SIGKILL (oder TerminateProcess unter Windows), wartet auf dessen Ende und gibt einen Status mit Canceled = true zurück. Code, der es in catch (OperationCanceledException) einschließt, betritt den catch-Block nie. Prüfen Sie stattdessen status.Canceled.
  2. Process.WaitForExitAsync(ct) bricht nur das Warten ab. Es wirft TaskCanceledException und lässt den Prozess am Leben. Wenn Sie ihn beenden wollten, rufen Sie Kill() selbst auf.
  3. Signal beendet das Raten anhand des Exit-Codes. Unter Unix meldet ein durch SIGKILL beendeter Prozess den Exit-Code 137 und ein durch SIGTERM beendeter 143, und ein Prozess kann auch einfach exit 137 ausführen. ProcessExitStatus.Signal sagt Ihnen, was davon passiert ist. Innerhalb eines Process.Run-Aufrufs sagt Canceled Ihnen, ob das Beenden von Ihnen kam.

Das synchrone Run nimmt ein TimeSpan? timeout und kein Token; RunAsync nimmt ein Token und kein Timeout. Für ein asynchrones Timeout verwenden Sie new CancellationTokenSource(TimeSpan) wie oben. Wenn der Abbruch aus einer ASP.NET Core-Anfrage oder einem Hosted Service kommt, gelten die Muster aus wie Sie einen lang laufenden Task ohne Deadlock abbrechen unverändert.

Wann Process.Run die richtige Wahl ist

// .NET 11 RC 1, C# 15
using System.Diagnostics;
using Microsoft.Win32.SafeHandles;

using SafeFileHandle log = File.OpenHandle("build.log", FileMode.Create, FileAccess.Write);

ProcessExitStatus status = Process.Run(new ProcessStartInfo("dotnet", ["build"])
{
    StandardOutputHandle = log,
    StandardErrorHandle = log,
    WorkingDirectory = "/src/app",
});

Wenn Sie die Ausgabe im Speicher statt in einer Datei brauchen, lassen Sie beide APIs weg und rufen Sie Process.RunAndCaptureText auf, das stdout und stderr parallel leert, sodass der klassische Redirect-Deadlock nicht auftreten kann.

Wann Sie bei Process.Start bleiben sollten

Der Benchmark

Die Performance ist kein Grund, sich für eines der beiden zu entscheiden, und ich habe sie gemessen, damit Sie nicht rätseln müssen. Umgebung: .NET 11 RC 1 (11.0.0-rc.1.26425.128), BenchmarkDotNet 0.15.8 mit der In-Process-Emit-Toolchain (0.15.8 erkennt net11.0 für Out-of-Process-Läufe noch nicht), 3 Warmup- und 15 Messiterationen, macOS 26.6.2, Apple M4 mit 10 Kernen. Jeder Benchmark startet /usr/bin/true und wartet darauf.

// .NET 11 RC 1, BenchmarkDotNet 0.15.8
[MemoryDiagnoser]
[InProcess, WarmupCount(3), IterationCount(15)]
public class SpawnBenchmarks
{
    private const string Exe = "/usr/bin/true";

    [Benchmark(Baseline = true)]
    public int Start_WaitForExit()
    {
        using Process p = Process.Start(Exe);
        p.WaitForExit();
        return p.ExitCode;
    }

    [Benchmark]
    public int Run() => Process.Run(Exe).ExitCode;

    [Benchmark]
    public async Task<int> Start_WaitForExitAsync()
    {
        using Process p = Process.Start(Exe);
        await p.WaitForExitAsync();
        return p.ExitCode;
    }

    [Benchmark]
    public async Task<int> RunAsync() => (await Process.RunAsync(Exe)).ExitCode;
}
MethodMeanStdDevAllocated
Start_WaitForExit739.4 us6.26 us16.84 KB
Run737.0 us2.76 us16.56 KB
Start_WaitForExitAsync768.5 us13.89 us17.89 KB
RunAsync762.6 us2.56 us17.47 KB

Der Unterschied liegt im Rauschen. Fast die gesamten Kosten entstehen dadurch, dass das Betriebssystem den Prozess erzeugt, und beide APIs teilen denselben SafeProcessHandle-Startpfad. Der Verzicht auf das Process-Objekt spart etwa 300 Bytes. Die großen Verbesserungen in .NET 11 (laut den Messungen des .NET-Teams 98-mal schnellerer Prozessstart auf Apple Silicon) gelten für beide APIs gleichermaßen, weil sie unterhalb dieser Schicht liegen.

Fallstricke, die die Entscheidung für Sie treffen

Ein Timeout beendet nur den direkten Kindprozess. Ich habe Process.Run("sh", ["-c", "sleep 31; true"], timeout: TimeSpan.FromMilliseconds(500)) ausgeführt. Run gab Canceled=True Signal=SIGKILL zurück, und pgrep -f "sleep 31" fand den sleep-Prozess danach immer noch, neu zugeordnet und laufend. Derselbe Test mit Process.Start und Kill(entireProcessTree: true) hinterließ nichts. dotnet build, npm run, docker compose und die meisten Shell-Skripte starten Kindprozesse. Wenn ein Timeout die Maschine also sauber hinterlassen muss, ist Process.Start mit dem Beenden des ganzen Baums weiterhin das richtige Werkzeug. ProcessStartInfo.KillOnParentExit (nur Windows, Linux und Android) ist kein Ersatz: Es greift, wenn Ihr Prozess endet, nicht wenn ein Timeout abläuft.

Es gibt keine Überladung mit String-Argumenten. Process.Run("git", "status") kompiliert nicht, weil string kein IEnumerable<string> ist. Übergeben Sie ["status"] oder erstellen Sie ein ProcessStartInfo("git", "status") und verwenden Sie die Überladung Run(ProcessStartInfo).

silent: true setzt auch stdin auf null. Ein Kindprozess, der eine Eingabe anfordert, etwa git beim Abfragen von Anmeldedaten, liest sofort Dateiende, statt zu hängen. In einem Dienst ist das meist gewünscht, in einem Entwicklerskript überraschend.

Run(ProcessStartInfo) lehnt Ihre bestehende Start-Info ab. Wenn Sie eine ProcessStartInfo migrieren, die noch RedirectStandardOutput = true hat, erhalten Sie InvalidOperationException: The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start or Process.StartAndForget. Die Meldung erwähnt weder Run noch RunAsync, aber es ist dieselbe Prüfung. Ersetzen Sie das Flag durch StandardOutputHandle oder wechseln Sie zu RunAndCaptureText.

Fehler beim Starten sind identisch. Eine fehlende ausführbare Datei wirft bei beiden APIs dieselbe Win32Exception (“No such file or directory”), sodass bestehende Fehlerbehandlung weiter funktioniert.

Keines von beiden wird auf iOS oder tvOS unterstützt. Beide tragen [UnsupportedOSPlatform("ios")] und [UnsupportedOSPlatform("tvos")], Mac Catalyst wird unterstützt. Der Plattform-Analyzer markiert Aufrufe in einem MAUI-Projekt in beiden Fällen.

Die Entscheidung

Für neuen .NET 11-Code, der einen Prozess bis zum Ende ausführt, sollten Sie standardmäßig Process.Run oder Process.RunAsync verwenden. Sie beseitigen die drei häufigsten Fehler in Prozesscode (der nicht disposte Process, das Timeout, das nichts beendet, der Exit-Code, der ein Signal verbirgt) ohne messbare Kosten. Wechseln Sie zu Process.RunAndCaptureText, sobald Sie die Ausgabe als String brauchen. Greifen Sie nur dann zu Process.Start, wenn der Prozess während der Laufzeit gesteuert, über die Shell geöffnet oder als ganzer Baum abgebaut werden muss. Der letzte Fall ist wichtiger, als er aussieht, denn viele Tools, die Sie in Process.Run verpacken würden, starten stillschweigend Kindprozesse, die dessen Timeout nicht erreicht. Wenn Sie noch älteren Code pflegen, der auf WaitForExit blockiert, sind die grundlegenden Muster zum Warten auf das Prozessende der übliche Ausgangspunkt für Process.Start-Code, und es lohnt sich, sie mit diesen Standardwerten im Hinterkopf neu zu betrachten.

Verwandte Artikel

Quellen

Comments

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

< Zurück