بازبینی و ارتقای کدهای ناهمگام (Async/Await) در #C با GitHub Copilot
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۹ ۱۲:۳۲
آدرس: www.dntips.ir
async میتوانند به سادگی منجر به قفل شدن نخها (Thread-Pool Starvation)، بنبست (Deadlock)، مصرف افسارگسیخته حافظه یا از دست رفتن استثناها (Unobserved Exceptions) شوند. درخواستهای کلی مانند «این متد async را بازنویسی کن» معمولاً بدون ارزیابی بار سیستم، کدهای ترتیبی را کورکورانه به همزمانیهای سنگین تبدیل میکنند. استفاده اصولی از Copilot نیازمند هدایت مدل به سمت تحلیل گلوگاههای همزمانی، رفتار کنترل خطا و موازنههای فشار بر زیرساخت است.public async Task ProcessAsync()
{
var users = GetUsersAsync().Result;
foreach (var user in users)
{
ProcessUserAsync(user);
}
}.Result نخ جاری را بلاک میکند و در صورت وجود SynchronizationContext (مانند فریمورکهای UI یا ساختارهای قدیمی ASP.NET) میتواند به بنبست فوری منجر شود.ProcessUserAsync بدون await فراخوانی شده است؛ در نتیجه اجرای متد بدون انتظار برای پایان پردازشها به اتمام میرسد، استثناها بلعیده میشوند و وضعیت پردازش نامشخص میماند.CancellationToken برای متوقفسازی وظایف معلق وجود ندارد.این متد ناهمگام در C# را از نظر الزامات مهندسی همزمانی بازبینی کن:
محورهای ارزیابی:
۱. شناسایی فراخوانیهای بلاککننده (.Resultیا.Wait()) و خطرات Thread-Pool Starvation
۲. بررسی تسکهای رهاشده (Fire-and-Forget بدون await) و نحوه انتشار استثناها (Exception Propagation)۳. پشتیبانی کامل از الگوی توقف اضطراری (CancellationToken)۴. ماشین وضعیت غیرضروری (Unnecessary Async State Machines) و سربار تخصیص تسک
۵. مقایسه جامع اجرای ترتیبی (Sequential) در برابر اجرای همزمان (Concurrent/Parallel) با بررسی:
ProcessUserAsync نیازمند دسترسی به دیتابیس با همان DbContext است، اجرای ترتیبی الزامی است؛ چرا که نمونههای DbContext در Entity Framework Core به هیچ وجه Thread-safe نیستند:public async Task ProcessSequentialAsync(CancellationToken cancellationToken = default)
{
var users = await GetUsersAsync(cancellationToken);
foreach (var user in users)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessUserAsync(user, cancellationToken);
}
}Parallel.ForEachAsync یا SemaphoreSlim)Task.WhenAll همیشه بهتر است. ارسال صدها تسک همزمان به Task.WhenAll میتواند پورتهای اتصال شبکه یا استخر کانکشنهای دیتابیس را اشباع کند. الگوی بهینه مدرن در داتنت، محدودسازی درجه همزمانی (Throttling) است:public async Task ProcessConcurrentThrottledAsync(
int maxDegreeOfParallelism = 8,
CancellationToken cancellationToken = default)
{
var users = await GetUsersAsync(cancellationToken);
var options = new ParallelOptions
{
MaxDegreeOfParallelism = maxDegreeOfParallelism,
CancellationToken = cancellationToken
};
await Parallel.ForEachAsync(users, options, async (user, ct) =>
{
await ProcessUserAsync(user, ct);
});
}ValueTask برای مسیرهای پرتکرار یا همگام: از Copilot بپرسید آیا متدهایی که داده را غالباً از کش حافظه میخوانند میتوانند برای کاهش تخصیص حافظه (Zero-Allocation) از ValueTask استفاده کنند یا خیر.ExceptionDispatchInfo یا ساختارهای تجمیعی را ارزیابی کنید.async/await در متدهای Wrapper: اگر یک متد صرفاً تسک بازگرداندهشده از یک متد دیگر را به لایه بالاتر پاس میدهد (بدون نیاز به try/catch یا using)، میتوان کلمه کلیدی async را حذف کرده و مستقیماً خود شیء Task را بازگرداند تا از سربار ایجاد State Machine جلوگیری شود.