عنوان:

‫بهبودهای API فرآیند در NET 11.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۱۰ ۱۲:۱۵
آدرس: www.dntips.ir
کلاس System.Diagnostics.Process اصلی‌ترین راهکار .NET برای ایجاد و تعامل با فرآیندهای سیستم‌عامل است. در نسخه .NET 11، این کلاس یکی از بزرگ‌ترین به‌روزرسانی‌های خود را در سال‌های اخیر، دریافت کرده‌است. این تغییرات شامل APIهای سطح بالا برای شروع فرآیند و خواندن خروجی بدون ایجاد بن‌بست، کنترل کامل بر ارث‌بری هندل‌ها، ویژگی‌های مدیریت طول عمر فرآیند (مانند KillOnParentExit) و APIهای سبک مبتنی بر SafeProcessHandle است که برای سناریوهای NativeAOT بسیار مناسب‌تر هستند. هدف اصلی این به‌روزرسانی‌ها، ساده‌تر کردن کدهای رایج، افزایش قابلیت اطمینان و بهبود عملکرد قابل توجه‌است. در ادامه، به مهم‌ترین ویژگی‌های جدید آن می‌پردازیم.


خواندن خروجی فرآیند بدون بن‌بست

چرا خواندن خروجی گاهی باعث هنگ کردن برنامه می‌شود؟
هنگامی که RedirectStandardOutput و RedirectStandardError را فعال می‌کنید، .NET دو پایپ (pipe) ایجاد می‌کند. پایپ‌ها بافر محدودی دارند (معمولاً ۴ کیلوبایت در ویندوز و ۶۴ کیلوبایت در یونیکس). اگر فرآیند فرزند، داده‌ی بیشتری از اندازه بافر بنویسد و فرآیند والد، همزمان در حال خواندن نباشد، فرآیند فرزند، روی عملیات نوشتن بلاک می‌شود.
کد کلاسیک زیر بسیار مستعد بن‌بست است:
process.Start();
process.WaitForExit();
string output = process.StandardOutput.ReadToEnd();   // ممکن است بلاک شود
string error = process.StandardError.ReadToEnd();
حتی جابه‌جایی ترتیب خواندن هم مشکل را حل نمی‌کند، زیرا ReadToEnd تا زمان بسته شدن پایپ (خروج فرآیند) بلاک می‌ماند. راه‌حل، خواندن همزمان هر دو جریان است.

APIهای جدید برای خواندن خروجی
.NET 11 متدهای زیر را اضافه کرده است:
public (string StandardOutput, string StandardError) ReadAllText(TimeSpan? timeout = default);
public Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(CancellationToken ct = default);

public IEnumerable<ProcessOutputLine> ReadAllLines(TimeSpan? timeout = default);
public IAsyncEnumerable<ProcessOutputLine> ReadAllLinesAsync(CancellationToken ct = default);

public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(TimeSpan? timeout = default);
مثال استفاده:
using Process process = Process.Start(new ProcessStartInfo("dotnet", "--help")
{
    RedirectStandardOutput = true,
    RedirectStandardError = true
})!;

(string output, string error) = process.ReadAllText();

RunAndCaptureText — API یک‌خطی
برای سناریوهای رایج، متد RunAndCaptureText همه‌ی مراحل را در یک فراخوانی انجام می‌دهد:
ProcessTextOutput result = Process.RunAndCaptureText("dotnet", ["--help"]);

Console.WriteLine(result.StandardOutput);
Console.WriteLine($"Exit Code: {result.ExitStatus.ExitCode}");
این متد، همزمان خروجی و خطا را می‌خواند، منتظر خروج فرآیند می‌ماند و جزئیات کامل وضعیت خروج (ProcessExitStatus) را برمی‌گرداند.

مدیریت ارث‌بری هندل‌ها و تغییر مسیر
یکی از مشکلات قدیمی، ارث‌بری ناخواسته هندل‌ها توسط فرآیندهای فرزند و نوه بود که منجر به بن‌بست یا نشت منابع می‌شد.
ویژگی‌های جدید:
  • ProcessStartInfo.InheritedHandles: مشخص کردن دقیق هندل‌هایی که باید ارث‌بری شوند.
  • ProcessStartInfo.StandardInputHandle / StandardOutputHandle / StandardErrorHandle: تغییر مسیر مستقیم به هر SafeFileHandle دلخواه (فایل، پایپ، null و غیره).
مثال پایپینگ (ls | grep):
SafeFileHandle.CreateAnonymousPipe(out var readPipe, out var writePipe);

using (readPipe)
using (writePipe)
{
    var producer = new ProcessStartInfo("ls", ["/usr/bin"]) { StandardOutputHandle = writePipe };
    var consumer = new ProcessStartInfo("grep", ["zip"]) 
    { 
        StandardInputHandle = readPipe,
        StandardOutputHandle = File.OpenHandle("output.txt", FileMode.Create, FileAccess.Write)
    };

    using var p1 = Process.Start(producer)!;
    writePipe.Dispose(); // والد دیگر نیازی به آن ندارد

    using var p2 = Process.Start(consumer)!;
    readPipe.Dispose();

    await Task.WhenAll(p1.WaitForExitAsync(), p2.WaitForExitAsync());
}

مدیریت طول عمر فرآیند
  • Process.StartAndForget: فرآیند را شروع می‌کند، PID را برمی‌گرداند و بلافاصله همه منابع را آزاد می‌کند.
  • ProcessStartInfo.KillOnParentExit: وقتی فرآیند والد خارج شود، فرآیندهای فرزند به‌طور خودکار متوقف می‌شوند (ویندوز و لینوکس).
  • ProcessStartInfo.StartDetached: فرآیند کاملاً مستقل از والد اجرا می‌شود و حتی با خروج والد یا بستن ترمینال زنده می‌ماند.

SafeProcessHandle — API سبک و مناسب NativeAOT
برای سناریوهایی که نیاز به کنترل بیشتر دارید، SafeProcessHandle حالا APIهای عمومی غنی‌تری دارد:
using SafeProcessHandle handle = SafeProcessHandle.Start(startInfo);

ProcessExitStatus status = handle.WaitForExitOrKillOnTimeout(TimeSpan.FromMinutes(2));
این API ردپای بسیار کوچک‌تری در بیلدهای NativeAOT دارد (تا ۳۲٪ کاهش اندازه باینری نسبت به .NET 10).

بهبودهای عملکردی قابل توجه
  • ویندوز: دیگر BeginOutputReadLine و BeginErrorReadLine نخ‌های Thread Pool را بلاک نمی‌کنند؛ نتیجه: مقیاس‌پذیری بسیار بهتر، هنگام اجرای موازی فرآیندها.
  • Apple Silicon: استفاده از posix_spawn به جای fork+exec؛ نتیجه: تا ۱۰۰ برابر سریع‌تر شدن ایجاد فرآیند.
  • یونیکس: کاهش ۳۰-۵۰ درصدی تخصیص حافظه هنگام شروع فرآیند.
  • NativeAOT: کاهش چشمگیر اندازه باینری (تا ۲۰٪ با Process و تا ۳۲٪ با SafeProcessHandle).

نتیجه‌گیری
به‌روزرسانی‌های Process در .NET 11 تجربه توسعه‌دهندگان را به شکل قابل توجهی بهبود بخشیده‌است. حالا نوشتن کدهای ایمن، خوانا و با عملکرد بالا برای اجرای فرآیندهای خارجی بسیار ساده‌تر شده است. ویژگی‌هایی مانند RunAndCaptureText، کنترل دقیق ارث‌بری هندل‌ها و KillOnParentExit دقیقاً همان چیزهایی هستند که توسعه‌دهندگان سال‌ها منتظر آن بودند.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۴/۱۶ ۰۷:۵۸
    مطلب تکمیلی: بهبودهای بیشتر API فرآیند در .NET 11 (بخش دوم)

    در مطلب جاری، به بررسی جامع بهبودهای بزرگ API System.Diagnostics.Process در .NET 11 پرداختیم؛ از جمله روش‌های جدید برای خواندن خروجی بدون بن‌بست (ReadAllText، RunAndCaptureText)، مدیریت ارث‌بری هندل‌ها، ویژگی‌های طول عمر فرآیند و بهبودهای عملکردی.
    در این مطلب تکمیلی، بر جنبه‌های عملی و ساده‌سازی سناریوهای رایج تمرکز می‌کنیم که در پست‌های معرفی .NET 11 Preview به آنها اشاره شده است: گرفتن وضعیت خروج فرآیند با متدهای Run و RunAsync و شروع فرآیند بدون انتظار با StartAndForget.

    گرفتن وضعیت خروج فرآیند باRunوRunAsync
    قبلاً برای اجرای یک فرمان ساده و بررسی موفقیت یا شکست آن، نیاز به نوشتن کد نسبتاً طولانی بود:
    var startInfo = new ProcessStartInfo
    {
        FileName = "pwd",
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        CreateNoWindow = true
    };
    
    using var process = Process.Start(startInfo);
    await process.WaitForExitAsync();
    
    if (process.ExitCode == 0)
        Log.Information("Success!");
    else
        Log.Error("Failed!");
    در .NET 11 این کار به شکل بسیار ساده‌تری انجام می‌شود:
    var result = await Process.RunAsync("pwd");
    
    if (result.ExitCode == 0)
        Log.Information("Success");
    else
        Log.Error("Failure");

    پشتیبانی از آرگومان‌ها
    var result = await Process.RunAsync("pwd", ["-L"]);
    متد Run (نسخه synchronous) و RunAsync مستقیماً یک شیء ProcessExitStatus برمی‌گردانند که شامل ExitCode، Signal (در سیستم‌های یونیکس) و پرچم Canceled است. این متدها مناسب مواردی هستند که فقط به نتیجه اجرای فرآیند نیاز دارید و نیازی به خواندن خروجی ندارید.

    شروع فرآیند و فراموش کردن آن (StartAndForget)
    در بسیاری از سناریوها (مانند اجرای فرآیندهای طولانی‌مدت یا background tasks) می‌خواهیم فرآیندی را شروع کنیم و دیگر منتظر آن نباشیم. روش قدیمی:
    Process.Start(new ProcessStartInfo
    {
        FileName = "ping",
        Arguments = "-v google.com",
        CreateNoWindow = true
    });
    این کد کار می‌کند، اما هدف برنامه‌نویس را به خوبی منتقل نمی‌کند و مدیریت منابع هم همیشه واضح نیست. در .NET 11 متد StartAndForget اضافه شده است:
    Process.StartAndForget("ping", ["-v", "example.com"]);
    مزایا:
    • بسیار کوتاه و خوانا
    • PID فرآیند را برمی‌گرداند (در صورت نیاز)
    • بلافاصله همه منابع مرتبط با شیء Process را آزاد می‌کند و کد آن کاملاً شفاف است: «شروع کن و فراموشش کن»

    جمع‌بندی و توصیه‌ها
    با معرفی متدهای Run/RunAsync و StartAndForget، .NET 11 تقریباً تمام سناریوهای رایج کار با فرآیندهای خارجی را پوشش داده است:
    • فقط اجرای فرمان و گرفتن وضعیت خروج: RunAsync
    • اجرای فرمان + گرفتن خروجی و خطا: RunAndCaptureTextAsync
    • اجرای فرمان بدون انتظار: StartAndForget
    • کنترل دقیق و سطح پایین: SafeProcessHandle و APIهای هندل
    این تغییرات نه تنها حجم کد را کاهش می‌دهند، بلکه احتمال خطا را پایین می‌آورند و کد را برای نگهداری آینده آماده‌تر می‌کنند. توصیه می‌شود در پروژه‌های جدید از این APIهای سطح بالا استفاده کنید مگر در موارد خاص که نیاز به کنترل بسیار دقیق (مانند P/Invoke سفارشی) داشته باشید. همه این قابلیت‌ها از .NET 11 Preview 4 به بعد در دسترس هستند.
  • وحید نصیری در ۱۴۰۵/۰۴/۱۷ ۱۰:۱۰
    مطلب تکمیلی: مشکل بن‌بست در .NET 10 و نسخه‌های قبل

    یکی از مشکلات رایج و کلاسیک هنگام کار با Process، خطر بن‌بست (Deadlock) بود. کد زیر نمونه‌ای از پیاده‌سازی ناامن در .NET 10 است:
    using Process process = Process.Start(new ProcessStartInfo("git", ["status"])
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true
    })!;
    
    // ⚠️ خطر بن‌بست!
    string output = process.StandardOutput.ReadToEnd();
    string error = process.StandardError.ReadToEnd();
    
    process.WaitForExit();
    دلیل بن‌بست:
    • پایپ‌ها بافر محدود دارند.
    • ReadToEnd() روی یک جریان بلاک می‌شود تا فرآیند خارج شود.
    • اگر فرآیند فرزند داده زیادی به stderr بنویسد، منتظر خوانده شدن آن می‌ماند.
    • والد هم تا خواندن کامل stdout منتظر می‌ماند: بن‌بست متقابل.

    راه‌حل‌های قدیمی (هنوز معتبر اما پیچیده)
    • استفاده از ReadToEndAsync() + Task.WhenAll
    • استفاده از رویدادهای OutputDataReceived و ErrorDataReceived همراه با BeginOutputReadLine()
    هر دو روش نیاز به کد boilerplate زیادی داشتند و مدیریت حالت‌های خطا را دشوار می‌کردند.

    APIهای جدید .NET 11 و سادگی استفاده
    .NET 11 این مشکلات را با APIهای سطح بالا و ایمن حل کرده است:
    1. ReadAllText / ReadAllTextAsync
    جایگزین مستقیم و امن ReadToEnd:
    using Process process = Process.Start(startInfo)!;
    
    (string output, string error) = await process.ReadAllTextAsync();
    // یا نسخه synchronous: process.ReadAllText(timeout)
    
    process.WaitForExit();

    2. ReadAllLines / ReadAllLinesAsync — خواندن خط به خط (ایده‌آل برای پردازش realtime)
    using Process process = Process.Start(startInfo)!;
    
    await foreach (ProcessOutputLine line in process.ReadAllLinesAsync())
    {
        if (line.StandardError)
            Console.ForegroundColor = ConsoleColor.Red;
    
        Console.WriteLine(line.Content);
        Console.ResetColor();
    }
    ProcessOutputLine یک readonly struct ساده شامل Content و StandardError است.

    3. ReadAllBytes / ReadAllBytesAsync
    برای مواردی که نیاز به داده خام (بدون decoding) دارید:
    (byte[] output, byte[] error) = await process.ReadAllBytesAsync();

    4. RunAndCaptureText / RunAndCaptureTextAsync — همه چیز در یک فراخوانی
    ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", ["status"]);
    
    if (result.ExitStatus.ExitCode == 0)
        Log.Information(result.StandardOutput);
    else
        Log.Error(result.StandardError);

    نکات پشت صحنه و بهینه‌سازی‌ها
    • نسخه synchronous: از تکنیک‌های مالتی‌پلکسینگ (poll در Unix و WaitForMultipleObjects در ویندوز) استفاده می‌کند تا هر دو پایپ را با یک نخ بخواند و از بن‌بست جلوگیری کند. همچنین از ArrayPool برای کاهش تخصیص حافظه بهره می‌برد.
    • نسخه asynchronous: بدون بلاک کردن نخ‌ها کار می‌کند.
    • پشتیبانی از Timeout و CancellationToken در همه متدها.
    • در صورت timeout/cancellation، فرآیند به‌طور خودکار kill می‌شود (در متدهای RunAndCapture).

    نتیجه‌گیری و توصیه عملی
    APIهای جدید .NET 11 نه تنها کد را بسیار کوتاه‌تر و خواناتر کرده‌اند، بلکه خطر اشتباهات رایج مانند بن‌بست را تقریباً حذف کرده‌اند.

    توصیه‌ها:
    • برای اکثر سناریوها از RunAndCaptureTextAsync شروع کنید.
    • اگر نیاز به پردازش realtime دارید، ReadAllLinesAsync بهترین انتخاب است.
    • همیشه از timeout یا cancellationToken استفاده کنید تا برنامه‌تان در برابر فرآیندهای هنگ‌کننده مقاوم باشد.
    • برای کنترل سطح پایین‌تر همچنان می‌توانید از SafeProcessHandle یا هندل‌های مستقیم استفاده کنید.
  • وحید نصیری در ۱۴۰۵/۰۴/۱۸ ۰۸:۳۸
    مطلب تکمیلی: شروع فرآیند Detached در .NET 11

    در مطالب قبلی به ویژگی StartAndForget برای شروع فرآیند و فراموش کردن آن پرداختیم. با این حال، یک محدودیت مهم وجود داشت: بسیاری از متدهای Run، RunAndCaptureText و مشابه آن‌ها، هنگام خاتمه یافتن برنامه اصلی (فرآیند والد)، فرآیندهای فرزند را نیز به‌طور خودکار متوقف می‌کنند. این رفتار معمولاً مطلوب است، اما در برخی سناریوها نیاز داریم فرآیند فرزند مستقل بماند و حتی پس از خروج برنامه اصلی به کار خود ادامه دهد. .NET 11 با افزودن ویژگی StartDetached این نیاز را به‌سادگی برآورده کرده است.

    نحوه استفاده از StartDetached
    این ویژگی را از طریق ProcessStartInfo تنظیم می‌کنید و سپس با StartAndForget (یا متدهای دیگر) استفاده می‌نمایید:
    using System.Diagnostics;
    
    var startInfo = new ProcessStartInfo
    {
        FileName = "ping",
        Arguments = "google.com",
        StartDetached = true,        // تنظیم کلیدی
        UseShellExecute = false,
        CreateNoWindow = true
    };
    
    Process.StartAndForget(startInfo);
    نکات مهم
    • وقتی StartDetached را true قرار می‌دهید، فرآیند فرزند از والد جدا (detached) می‌شود و با خروج والد، دریافت سیگنال یا بسته شدن ترمینال، به کار خود ادامه می‌دهد.
    • این ویژگی هم در Process.StartAndForget و هم در Process.Start قابل استفاده است.
    • اگر StartDetached فعال باشد و تغییر مسیری برای استاندارد هندل‌ها مشخص نشده باشد، به‌طور خودکار به null handle تغییر مسیر داده می‌شوند تا وابستگی غیرضروری ایجاد نشود.
    • پیاده‌سازی این ویژگی پلتفرم‌محور است (در ویندوز با فلگ DETACHED_PROCESS و در یونیکس با setsid).

    چه زمانی از آن استفاده کنیم؟
    • اجرای سرویس‌ها یا فرآیندهای پس‌زمینه طولانی‌مدت
    • مواردی که فرآیند فرزند باید کاملاً مستقل از عمر برنامه اصلی باشد
    • ابزارهای خط فرمان که فرآیندهایی را spawn می‌کنند و سپس خودشان خارج می‌شوند

    نکته: استفاده از StartDetached همراه با KillOnParentExit = false (مقدار پیش‌فرض) تضمین می‌کند فرآیند فرزند واقعاً مستقل بماند.

    نتیجه‌گیری
    ترکیب StartAndForget و StartDetached در .NET 11 کنترل دقیق و آسانی روی طول عمر و استقلال فرآیندهای فرزند فراهم می‌کند. این ویژگی‌ها کد را تمیز و خوانا نگه می‌دارند و انعطاف‌پذیری بالایی به توسعه‌دهندگان می‌دهند. اگر قبلاً برای ایجاد فرآیندهای مستقل از فلگ‌های دستی و پلتفرم‌محور استفاده می‌کردید، حالا می‌توانید با کد portable و ساده‌تری این کار را انجام دهید. این قابلیت از نسخه‌های Preview .NET 11 در دسترس است و مکمل عالی ویژگی‌های مدیریت طول عمر فرآیند به شمار می‌رود.