عنوان:

‫چرا نادیده گرفتن CancellationToken هزینه‌ساز است؟


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۰۱ ۰۸:۴۷
آدرس: www.dntips.ir
یکی از "قاتلان خاموش" عملکرد در برنامه‌های دات‌نت، نادیده گرفتن توکن لغو (CancellationToken) در عملیات‌های طولانی‌مدت است. این مقاله به بررسی عمیق مفهوم CancellationToken، چرایی اهمیت آن، و نحوه استفاده صحیح از آن برای جلوگیری از هدر رفت منابع و بهبود کارایی برنامه‌های .NET می‌پردازد.

مقدمه: کارایی پنهان و CancellationToken
تصور کنید با کسی که دیگر گوش نمی‌دهد، همچنان در حال صحبت کردن هستید. این دقیقاً همان اتفاقی است که در بسیاری از برنامه‌های .NET رخ می‌دهد، جایی که کدها بدون توجه به عدم نیاز کاربر به نتیجه، به کار خود ادامه می‌دهند. نادیده گرفتن CancellationToken، حتی اگر در ابتدا مشکل‌ساز به نظر نرسد، می‌تواند به تدریج منجر به نشت حافظه (Memory Leaks)، مصرف بی‌رویه پردازنده (Unnecessary CPU Usage) و انجام وظایف غیرضروری برای کاربرانی که مدتهاست از سیستم خارج شده‌اند، شود. این مشکلات معمولاً به صورت سقوط‌های ناگهانی (Crashes) ظاهر نمی‌شوند، بلکه به تدریج کارایی کلی برنامه را کاهش می‌دهند و شناسایی آن‌ها دشوار است. خوشبختانه، راه حل این مشکل، بسیار ساده و در دسترس است: استفاده از CancellationToken.

CancellationToken چیست و چگونه کار می‌کند؟
CancellationToken یک ساختار (Struct) در .NET است که به شما امکان می‌دهد تا عملیات‌های طولانی‌مدت را به صورت مشارکتی لغو (Cooperative Cancellation) کنید. این بدان معناست که CancellationToken خود عملیات را به زور متوقف نمی‌کند؛ بلکه سیگنالی را ارسال می‌کند که نشان می‌دهد می‌توان عملیات را متوقف کرد. این سیگنال به کدی که عملیات را انجام می‌دهد، اطلاع می‌دهد که دیگر نیازی به ادامه کار نیست. در واقع، CancellationToken مانند یک پرچم (Flag) است که کد می‌تواند به صورت دوره‌ای آن را بررسی کند و در صورت لغو درخواست، کار خود را متوقف کند.
صرف‌نظر از اینکه با یک تماس API وب (Web API Call)، یک سرویس پس‌زمینه (Background Service) یا یک حلقه تکرار (Loop) فشرده سر و کار دارید، اگر به CancellationToken توجه نکنید، کد شما به کار خود ادامه می‌دهد، حتی اگر هیچ کس منتظر نتیجه آن نباشد. کاربر صفحه را ترک کرده، تب مرورگر بسته شده است، اما کد شما همچنان در حال اجراست، انگار که کسی منتظر آن است.

تجربه عملی: مثالی از هدر رفت منابع و راه‌حل
برای درک بهتر اهمیت CancellationToken، به یک مثال واقعی می‌پردازیم. فرض کنید یک صفحه وب داریم که هنگام بارگذاری، فایلی را دانلود می‌کند. کد اولیه ممکن است به شکل زیر باشد:
[HttpGet("download")]
public async Task<IActionResult> DownloadFile(string fileName)
{
    var result = await DownloadFileAsync(fileName);
    return Ok(result);
}

public async Task<string> DownloadFileAsync(string fileName)
{
    await Task.Delay(5000); // Simulate long download
    return $"Downloaded {fileName}";
}
در این سناریو، هر بار که کاربر صفحه را بارگذاری مجدد می‌کند یا از آن خارج می‌شود، یک عملیات دانلود جدید آغاز می‌شود. اما دانلود قبلی چه می‌شود؟ این عملیات همچنان در حال اجراست، حافظه را اشغال می‌کند و منابع پردازشی را مصرف می‌کند، در حالی که هیچ کس منتظر آن نیست. اینجاست که CancellationToken به نجات ما می‌آید.
ASP.NET Core به طور خودکار یک توکن لغو از طریق HttpContext.RequestAborted در اختیار ما قرار می‌دهد. تنها کاری که باید انجام دهیم، استفاده از آن است. اکثر عملیات‌های ناهمگام (Asynchronous Operations) در .NET این توکن را قبول کرده و به آن احترام می‌گذارند. کد اصلاح شده به شکل زیر خواهد بود:
[HttpGet("download")]
public async Task<IActionResult> DownloadFile(string fileName, CancellationToken token)
{
    var result = await DownloadFileAsync(fileName, token);
    return Ok(result);
}

public async Task<string> DownloadFileAsync(string fileName, CancellationToken token)
{
    try
    {
        await Task.Delay(5000, token); // Respect the token
        return $"Downloaded {fileName}";
    }
    catch (OperationCanceledException)
    {
        return "Download cancelled";
    }
}
با اضافه کردن CancellationToken به امضای متدها و پاس دادن آن به Task.Delay، اکنون وقتی کاربر صفحه را بارگذاری مجدد می‌کند یا می‌بندد، سرور متوجه می‌شود که باید کار را متوقف کند و این کار را انجام می‌دهد. دیگر خبری از "وظایف شبح" (Ghost Tasks)، هدر رفت حافظه (RAM) یا سؤالاتی مانند "چرا این برنامه در حالت بیکاری 800 مگابایت مصرف می‌کند؟" نخواهد بود.

CancellationToken فراتر از کد ناهمگام
اکثر توسعه‌دهندگان تصور می‌کنند که لغو عملیات تنها در کد ناهمگام (Async Code) کاربرد دارد. اما این تصور نادرست است. CancellationToken حتی در وظایف همگام (Synchronous Tasks) نیز می‌تواند بسیار حیاتی باشد. برای مثال، یک سرویس پس‌زمینه را در نظر بگیرید که یک پردازش دسته‌ای (Batch Processing) را انجام می‌دهد:
public void ProcessBatch()
{
    var items = Enumerable.Range(0, 1000).ToList();
    foreach (var item in items)
    {
        // Simulate CPU-intensive work
        ExpensiveComputation(item);
    }
}
حتی در طول خاموش شدن آرام (Graceful Shutdown) برنامه، این حلقه به کار خود ادامه می‌دهد و نادیده می‌گیرد که برنامه در حال اتمام است. راه حل، باز هم استفاده از CancellationToken است:
public void ProcessBatch(CancellationToken token)
{
    var items = Enumerable.Range(0, 1000).ToList();
    foreach (var item in items)
    {
        if (token.IsCancellationRequested)
        {
            return; // Early exit on cancellation
        }

        // Simulate CPU-intensive work
        ExpensiveComputation(item);
    }
}
با بررسی token.IsCancellationRequested در داخل حلقه، می‌توانیم به صورت دوره‌ای بررسی کنیم که آیا درخواست لغو شده است یا خیر. اگر سیستم سیگنال لغو را صادر کند، حلقه به سرعت متوقف می‌شود و منابع آزاد می‌شوند. بنابراین، حتی حلقه‌های for به ظاهر بی‌ضرر شما نیز ممکن است به یک CancellationToken نیاز داشته باشند.

چرا توسعه‌دهندگان از CancellationToken چشم‌پوشی می‌کنند؟
دلایل مختلفی برای نادیده گرفته شدن CancellationToken وجود دارد. برخی معتقدند که جمع‌آوری زباله (Garbage Collector یا GC) در نهایت حافظه را آزاد خواهد کرد. برخی دیگر فکر می‌کنند که زیرساخت (Infrastructure) آن‌ها می‌تواند چند کار اضافی را تحمل کند. اما این رویکرد می‌تواند مشکلات جدی ایجاد کند:
  • پردازش‌های تکراری: یک کار پس‌زمینه ممکن است بارها و بارها تلاش کند تا یک عملیات را انجام دهد، فقط به این دلیل که کاربر آن را لغو کرده است، که منجر به پردازش‌های تکراری (Duplicated Processing) می‌شود.
  • تاخیر در خاموش شدن: سرویس‌های کاری (Worker Services) ممکن است پس از فرمان خاموش شدن، برای مدت طولانی (مثلاً 60 ثانیه) معلق بمانند، زیرا هرگز CancellationToken را بررسی نمی‌کنند.
  • افزایش هزینه‌های ابری: انجام کارهای نامرئی برای درخواست‌های رها شده (Abandoned Requests) می‌تواند به طور ناخواسته قبض صورتحساب ابری شما را افزایش دهد.

این مشکلات کوچک، به تدریج روی هم انباشته می‌شوند و می‌توانند به مسائل بزرگتر و پرهزینه‌تری تبدیل شوند.

چک‌لیست استفاده از CancellationToken
برای اطمینان از استفاده صحیح و مؤثر از CancellationToken، می‌توانید از یک چک‌لیست ساده پیروی کنید:
  • اضافه کردن به متدها: برای هر اکشن کنترلر (Controller Action) یا متد سرویس (Service Method) که ممکن است زمان‌بر باشد، یک CancellationToken اضافه کنید.
  • پاس دادن در زنجیره فراخوانی: CancellationToken را در سراسر زنجیره فراخوانی (Call Chain) به متدهای زیرین منتقل کنید.
  • بررسی در حلقه‌ها: برای حلقه‌هایی که کارهای سنگین انجام می‌دهند، IsCancellationRequested را هر چند تکرار یک بار بررسی کنید.
  • اجتناب از تأخیرهای بی‌توجه: از Task.Delay یا Thread.Sleep بدون پارامتر CancellationToken خودداری کنید، به خصوص اگر عملیات قابلیت لغو شدن داشته باشد.
  • شبیه‌سازی لغو: حتی در محیط‌های توسعه (Dev) و مرحله‌بندی (Staging)، لغو عملیات توسط کاربر را شبیه‌سازی کنید تا از عملکرد صحیح فرآیندهای پاکسازی (Cleanup) اطمینان حاصل کنید.

این یک عادت ساده است که پیش از اینکه به یک مشکل تبدیل شود، بازدهی خود را نشان می‌دهد و به بهبود پایداری (Stability) و کارایی (Efficiency) برنامه‌های شما کمک شایانی می‌کند.

نتیجه‌گیری
CancellationToken یک ابزار حیاتی در جعبه ابزار هر توسعه‌دهنده .NET است که به مدیریت مؤثر منابع و جلوگیری از هدر رفت بی‌مورد پردازش کمک می‌کند. نادیده گرفتن آن می‌تواند به صورت تدریجی منجر به مشکلات عملکردی پنهان شود که شناسایی و رفع آن‌ها دشوار است. با پیروی از الگوهای ساده و گنجاندن CancellationToken در طراحی برنامه‌های خود، می‌توانید برنامه‌هایی پایدارتر، کارآمدتر و پاسخگوتر ایجاد کنید. به یاد داشته باشید، همان‌طور که وقتی کسی گوش نمی‌دهد، صحبت را متوقف می‌کنید، کد شما نیز باید وقتی نیازی به آن نیست، اجرای خود را متوقف کند.


مشاهده مطلب اصلی