اگر چند سال پیش از برنامهنویسان #C میپرسیدید سختترین بخش زبان چیست، احتمالاً خیلیها از Thread یا Task نام میبردند. مدیریت همزمان چند عملیات همیشه یکی از چالشهای برنامهنویسی بوده است.
خوشبختانه از C# 5 به بعد، مایکروسافت با معرفی async و await این کار را بسیار سادهتر کرد.
اما یک نکته مهم وجود دارد.
بسیاری از برنامهنویسان فکر میکنند async باعث سریعتر شدن برنامه میشود؛ در حالی که این تصور اشتباه است.
در این مقاله دقیقاً بررسی میکنیم:در ای
- Async و Await چیست؟
- چه زمانی باید از آنها استفاده کنیم؟
- پشت صحنه چه اتفاقی میافتد؟
- تفاوت آن با Thread چیست؟اشتباهات رایج چیست؟
Async و Await چیست؟
فرض کنید یک API فراخوانی میکنید.
این درخواست ممکن است ۳ ثانیه طول بکشد.
اگر کد شما به صورت عادی اجرا شود:
var result = GetData();
Console.WriteLine(result);
برنامه تا دریافت پاسخ متوقف میشود.
به این حالت Synchronous Programming گفته میشود.
اما اگر از Async استفاده کنیم:
var result = await GetDataAsync();
Console.WriteLine(result);
در این حالت Thread منتظر نمیماند و میتواند کارهای دیگری انجام دهد تا پاسخ دریافت شود.
تفاوت اجرای Sync و Async
اجرای همزمان (Synchronous)
1-شروع
2-ارسال درخواست
3-منتظر پاسخ…
4-ادامه برنامه
در تمام مدت انتظار، Thread بلااستفاده است.
اجرای Async
1- شروع
2- ارسال درخواست
3- Thread آزاد می شود .
4- انجام کارهای دیگر
5- دریافت پاسخ
6- ادامه اجرای متد
این دقیقاً همان دلیلی است که Async در برنامههای تحت وب اهمیت زیادی دارد.
اولین مثال
فرض کنید متدی داریم که اطلاعات کاربر را از دیتابیس میگیرد.
public async Task<string> GetUserAsync()
{
await Task.Delay(3000);
return "Hossein";
}
حالا آن را فراخوانی میکنیم:
public async Task Run()
{
var user = await GetUserAsync();
Console.WriteLine(user);
}
در این مثال:
- متد اول Async است.
- هنگام رسیدن به await، اجرای متد متوقف میشود.
- Thread آزاد میشود.
- بعد از اتمام عملیات، ادامه متد اجرا میشود.
Async دقیقاً چه کاری انجام میدهد؟
بسیاری تصور میکنند:
Async یعنی اجرای برنامه روی Thread جدید.
این موضوع همیشه درست نیست.
async به تنهایی هیچ Thread جدیدی ایجاد نمیکند.
بلکه فقط به کامپایلر میگوید:
اگر عملیات زمانبر بود، Thread را آزاد کن و بعد از پایان عملیات، ادامه اجرای متد را از همین نقطه ادامه بده.
این تفاوت بسیار مهم است.
Await چه کاری انجام میدهد؟
کلمه کلیدی await فقط روی متدهایی قابل استفاده است که مقدار قابل انتظار (Awaitable) برگردانند.
مانند:
Task
Task<T>
ValueTask
ValueTask<T>
مثال:
await File.ReadAllTextAsync(path);
await httpClient.GetAsync(url);
await dbContext.Users.ToListAsync()
چرا در ASP.NET Core استفاده از Async اهمیت زیادی دارد؟
فرض کنید ۱۰۰۰ کاربر همزمان وارد سایت شما شوند.
اگر تمام درخواستها Sync باشند:
هر درخواست
↓
یک Thread اشغال میکند
↓
Thread Pool پر میشود
↓
درخواستهای بعدی منتظر میمانند
اما با Async:
ارسال Query
↓
Thread آزاد میشود
↓
Database پاسخ میدهد
↓
ادامه اجرا
در نتیجه:
1- مصرف Thread کمتر میشود.
2- تعداد کاربران همزمان افزایش پیدا میکند.
3- مقیاسپذیری بهتر میشود.
به همین دلیل تقریباً تمام متدهای EF Core نسخه Async دارند.
مثال واقعی در ASP.NET Core
[HttpGet]
public async Task<IActionResult> Get()
{
var users = await _context.Users.ToListAsync();
return Ok(users);
}
همین مثال ساده در پروژههای واقعی میتواند باعث شود سرور تعداد بسیار بیشتری درخواست را همزمان مدیریت کند.
تفاوت Task و Thread
یکی از رایجترین اشتباهات این است که این دو را یکی بدانیم.
| Task | Thread |
| نمایشدهنده یک عملیات | واحد اجرای سیستم عامل |
| سبکتر است | ساخت آن هزینه دارد |
| توسط Task Scheduler مدیریت میشود | مستقیماً توسط OS مدیریت میشود |
| ممکن است اصلاً Thread جدید نسازد | همیشه وجود فیزیکی دارد |
بنابراین Task الزاماً به معنای Thread جدید نیست.
آیا همیشه باید Async بنویسیم؟
خیر.
اگر متد شما فقط چند محاسبه ساده انجام میدهد:
public int Sum(int a, int b)
{
return a + b;
}
نیازی به Async نیست.
نوشتن این کد اشتباه است:
public async Task<int> Sum()
{
return 10;
}
زیرا هیچ عملیات Asynchronous وجود ندارد.
عملیاتهایی که معمولاً Async هستند
- خواندن فایل
- نوشتن فایل
- Database
- HTTP Request
- Web API
- شبکه
- Socket
- Cloud Storage
- Message Broker
عملیاتهایی که معمولاً Sync هستند
- محاسبات ریاضی
- پردازش آرایه
- تبدیل رشته
- الگوریتمها
- Serialization ساده
- اعتبارسنجی دادهها
اشتباهات رایج
۱. استفاده از .Result
var user = GetUserAsync().Result;
ممکن است باعث Deadlock شود.
روش صحیح:
var user = await GetUserAsync();
۲. استفاده از .Wait()
GetUserAsync().Wait();
این هم مشکل مشابهی ایجاد میکند.
۳. استفاده از async void
اشتباه:
public async void Save()
{
}
صحیح:
public async Task Save()
{
}
تنها استثنا، Event Handlerها هستند.
۴. فراموش کردن await
GetUserAsync();
اگر قصد دارید منتظر پایان عملیات بمانید، باید از await استفاده کنید:
await GetUserAsync();
ConfigureAwait چیست؟
در برنامههای دسکتاپ قدیمی مانند WPF یا WinForms، بعد از await معمولاً باید اجرای متد به همان Thread رابط کاربری برگردد.
اما در ASP.NET Core چنین نیازی وجود ندارد و Synchronization Context به شکل کلاسیک وجود ندارد. به همین دلیل در اکثر پروژههای ASP.NET Core، استفاده از ConfigureAwait(false) ضرورتی ندارد و معمولاً تفاوت محسوسی ایجاد نمیکند.
ValueTask چیست؟
گاهی یک متد در بیشتر مواقع نتیجه را بلافاصله در اختیار دارد و فقط در شرایط خاص نیاز به عملیات Async پیدا میکند.
در این سناریوها میتوان از ValueTask استفاده کرد تا در برخی موارد از ایجاد شیء جدید Task جلوگیری شود.
با این حال، در اکثر پروژهها استفاده از Task انتخاب مناسبتر و سادهتری است و تنها زمانی به سراغ ValueTask بروید که با اندازهگیری (Profiling) متوجه شده باشید این بهینهسازی واقعاً لازم است.
جمعبندی
async و await یکی از مهمترین قابلیتهای C# هستند و تقریباً در تمام پروژههای مدرن از آنها استفاده میشود.
نکتهای که باید همیشه به خاطر داشته باشید این است که Async برای کاهش زمان انتظار و استفاده بهتر از منابع سیستم طراحی شده است، نه برای سریعتر کردن محاسبات CPU.
اگر عملیات شما منتظر پاسخ دیتابیس، فایل یا شبکه است، Async انتخاب درستی است. اما اگر فقط در حال انجام محاسبات روی CPU هستید، استفاده از Async معمولاً سودی ندارد و حتی میتواند کد را پیچیدهتر کند
سوالات متداول (FAQ)
آیا Async باعث افزایش سرعت برنامه میشود؟
خیر. Async معمولاً زمان اجرای خود عملیات را کاهش نمیدهد، بلکه باعث میشود Threadها هنگام انتظار برای I/O آزاد شوند و برنامه بتواند درخواستهای بیشتری را همزمان مدیریت کند.
آیا هر متدی باید Async باشد؟
خیر. فقط متدهایی که عملیات I/O محور انجام میدهند یا متدهای Async دیگری را فراخوانی میکنند، باید Async باشند.
تفاوت Task و Task<T> چیست؟
Task نشاندهنده یک عملیات بدون مقدار بازگشتی است، اما Task<T> پس از اتمام عملیات، مقداری از نوع T برمیگرداند.



