Async/Await در #C: اجرای همزمان بدون قفل شدن Thread اصلی
مسئله قفل شدن (Blocking)
قبل از ظهور async/await، مدیریت ناهمزمانی در C# اغلب شامل استفاده مستقیم از Threadها، ThreadPool یا الگوهایی مانند APM (Asynchronous Programming Model) و EAP (Event-based Asynchronous Pattern) بود که همگی پیچیدگیهای خاص خود را داشتند.
هنگامی که یک Thread عملیات ورودی/خروجی (I/O) یا محاسباتی طولانی را آغاز میکند، آن Thread قفل میشود و تا زمان اتمام عملیات نمیتواند وظیفه دیگری را انجام دهد. در برنامههای دسکتاپ (مانند WPF یا Windows Forms) یا برنامههای موبایل، Thread اصلی مسئول بهروزرسانی رابط کاربری (UI) است. اگر این Thread قفل شود، برنامه از پاسخگویی باز میایستد، یک پدیده که معمولاً به عنوان "قفل شدن" یا "فریز کردن" شناخته میشود.
ظهور Task و Task Parallel Library (TPL)
پایه و اساس async/await بر روی شیء Task و Task Parallel Library (TPL) بنا شده است. یک Task در واقع نماینده یک عملیات است که قرار است در آینده به پایان برسد و نتیجهای تولید کند (یا استثنایی را مطرح کند). Taskها روش استاندارد و ترجیحی برای کار با ناهمزمانی در .NET مدرن هستند.
مکانیزم Async/Await
کلمات کلیدی async و await تنها "قند سینتکسی" (Syntactic Sugar) هستند که کامپایلر C# را قادر میسازند تا کد ناهمزمان پیچیده را به کدی ساده، قابل خواندن و شبیه به کد همزمان (Synchronous) تبدیل کند.
کلمه کلیدی async
-
این کلمه کلیدی بر روی امضای متد قرار میگیرد و به کامپایلر میگوید که این متد شامل یک یا چند عبارت await است و باید برای اجرای ناهمزمان آماده شود.
-
متد async باید یکی از انواع بازگشتی زیر را داشته باشد: Task (برای متدهایی که مقداری باز نمیگردانند)، Task (برای متدهایی که مقداری از نوع TResult باز میگردانند)، یا void (به ندرت و فقط برای Event Handlerها).
-
نکته حیاتی: استفاده از async به تنهایی، کد را ناهمزمان نمیکند؛ بلکه تنها آن را برای استفاده از await فعال میسازد.
کلمه کلیدی await
-
این کلمه کلیدی بر روی یک Task اعمال میشود و در واقع نقطه اصلی تعلیق متد است.
-
وقتی کامپایلر به عبارت await someTask میرسد، دو اتفاق میافتد:
-
بررسی میکند که آیا someTask قبلاً تکمیل شده است یا خیر.
-
اگر تکمیل نشده باشد، متد async متوقف میشود، کنترل به فراخواننده (Caller) بازگردانده میشود، و Thread فعلی آزاد میشود تا بتواند وظایف دیگری (مانند بهروزرسانی UI) را انجام دهد.
-
هنگامی که someTask به اتمام میرسد، ادامه متد async در یک Context مناسب از سر گرفته میشود.
-

Thread آزاد میشود: قلب ناهمزمانی
مهمترین مزیت async/await، توانایی آن در آزاد کردن Thread اصلی است.
عملیات I/O-Bound
در عملیاتهای I/O-Bound (مانند درخواستهای HTTP، خواندن از دیسک یا پایگاه داده)، در واقع Thread نیازی به انجام محاسبات ندارد؛ بلکه منتظر یک پاسخ خارجی است. در این حالت، await اجازه میدهد که Thread فعلی (به احتمال زیاد Thread اصلی UI) برای انجام وظایف دیگر آزاد شود. وقتی عملیات I/O به پایان میرسد، یک Thread از ThreadPool برای ادامه اجرای متد async استفاده میشود. این موضوع باعث افزایش چشمگیر مقیاسپذیری (Scalability) در برنامههای سرور (مانند ASP.NET Core) میشود، زیرا Threadها به جای بیکار ماندن، میتوانند درخواستهای ورودی بیشتری را پردازش کنند.
Synchronization Context
در محیطهایی که دارای رابط کاربری هستند (مانند Windows Forms یا WPF)، SynchronizationContext تضمین میکند که پس از پایان await، ادامه کد در همان Thread اصلی (UI Thread) اجرا شود. این امر حیاتی است زیرا فقط Thread اصلی میتواند رابط کاربری را بهروزرسانی کند. async/await با حفظ این Context، بهروزرسانیهای UI را ایمن و آسان میسازد.
عملیات CPU-Bound
برای عملیاتهای CPU-Bound (مانند محاسبات سنگین، پردازش تصویر یا رمزگذاری)، async/await به تنهایی Thread را آزاد نمیکند، بلکه تنها ترتیب اجرای متد را تغییر میدهد. برای انجام محاسبات سنگین به صورت ناهمزمان، باید صراحتاً از Task.Run() استفاده شود. Task.Run() کد را به یک Thread جداگانه در ThreadPool منتقل میکند تا Thread اصلی UI آزاد بماند.
// مثال برای عملیات CPU-Bound
public async Task CalculateHeavyResultAsync()
{
// Thread اصلی آزاد می شود و عملیات در ThreadPool اجرا می شود
return await Task.Run(() =>
{
// ... انجام محاسبات سنگین
return result;
});
}
مزایای Async/Await
| مزیت | توضیح |
| بهبود پاسخگویی | Thread اصلی آزاد میماند و میتواند UI یا سایر درخواستهای حیاتی را بهروزرسانی و پردازش کند. |
| افزایش مقیاسپذیری | در برنامههای سرور، Threadهای کمتری برای مدیریت درخواستهای ورودی/خروجی قفل میشوند، در نتیجه توان عملیاتی (Throughput) سرور افزایش مییابد. |
| سادگی کد | کد ناهمزمان شبیه به کد همزمان نوشته میشود و مدیریت آن بسیار آسانتر از Threadهای خام یا Callbacks است. |
| مدیریت آسان خطا | استثناهای پرتاب شده در یک Task به سادگی توسط بلوکهای try/catch در متد فراخواننده ناهمزمان گرفته میشوند. |
بهترین شیوهها و دامهای رایج
-
پایاندهی با Async: همیشه متدهایی را که بهطور ناهمزمان اجرا میشوند، با پسوند Async نامگذاری کنید (مانند GetDataAsync).
-
استفاده از نسخههای Async: همیشه از نسخههای ناهمزمان APIها (مانند Stream.ReadAsync به جای Stream.Read) استفاده کنید. استفاده از متدهای همزمان در یک متد async باعث قفل شدن Thread میشود.
-
اجتناب از async void: به جز Event Handlerها، از async void اجتناب کنید. متدهای async void مدیریت خطا و امکان await شدن توسط فراخواننده را مختل میکنند.
-
استفاده از ConfigureAwait(false): در کتابخانههای عمومی یا کدهای سرور (مانند ASP.NET Core) که نیازی به SynchronizationContext ندارند، استفاده از await someTask.ConfigureAwait(false) توصیه میشود. این کار سربار بازیابی Context را از بین میبرد و کمی عملکرد را بهبود میبخشد، اما باید توجه داشت که این کار باعث میشود ادامه متد در یک Thread دلخواه از ThreadPool اجرا شود.
جمعبندی
async و await در C# صرفاً ابزارهایی برای نوشتن کد نیستند؛ بلکه یک تغییر پارادایم در نحوه تفکر درباره ناهمزمانی و عملکرد برنامه هستند. آنها به برنامهنویسان اجازه میدهند تا برنامههایی بسازند که هم بسیار پاسخگو (Responsive) باشند (مانند برنامههای دسکتاپ و موبایل) و هم بسیار مقیاسپذیر (Scalable) باشند (مانند برنامههای سرور)، و همه اینها را با کدی که به طرز شگفتآوری خوانا و قابل نگهداری است، انجام دهند. با رها کردن Thread اصلی از بار عملیاتهای طولانی، C# تضمین میکند که برنامههای مدرن میتوانند از منابع سیستم به کارآمدترین شکل ممکن استفاده کنند.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.