وبلاگ
برنامهنویسی Async/Await در C#: بهینهسازی عملکرد برنامهها
فهرست مطالب
“تسلط به برنامهنویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”
"تسلط به برنامهنویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"
"با شرکت در این دوره جامع و کاربردی، به راحتی مهارتهای برنامهنویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر میسازد تا به سرعت الگوریتمهای پیچیده را درک کرده و اپلیکیشنهای هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفهای و امکان دانلود و تماشای آنلاین."
ویژگیهای کلیدی:
بدون نیاز به تجربه قبلی برنامهنویسی
زیرنویس فارسی با ترجمه حرفهای
۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان
0 تا 100 عطرسازی + (30 فرمولاسیون اختصاصی حامی صنعت)
دوره آموزش Flutter و برنامه نویسی Dart [پروژه محور]
دوره جامع آموزش برنامهنویسی پایتون + هک اخلاقی [با همکاری شاهک]
دوره جامع آموزش فرمولاسیون لوازم آرایشی
دوره جامع علم داده، یادگیری ماشین، یادگیری عمیق و NLP
دوره فوق فشرده مکالمه زبان انگلیسی (ویژه بزرگسالان)
شمع سازی و عودسازی با محوریت رایحه درمانی
صابون سازی (دستساز و صنعتی)
صفر تا صد طراحی دارو
متخصص طب سنتی و گیاهان دارویی
متخصص کنترل کیفی شرکت دارویی
برنامهنویسی Async/Await در C#: بهینهسازی عملکرد برنامهها
در دنیای پرشتاب امروز، توسعهدهندگان نرمافزار همواره در تلاشند تا برنامههایی بسازند که نه تنها از نظر عملکردی کارآمد باشند، بلکه تجربه کاربری روان و پاسخگویی را نیز فراهم کنند. با افزایش پیچیدگی برنامهها و نیاز به تعامل با منابع بیرونی مانند پایگاههای داده، سرویسهای وب، و سیستمهای فایل، عملیاتهای ورودی/خروجی (I/O) میتوانند به یک گلوگاه عملکردی تبدیل شوند. برنامهنویسی همزمان (Synchronous) در این سناریوها باعث قفل شدن رشته اصلی (UI thread) یا رشتههای کاری (worker threads) میشود و منجر به تجربه کاربری نامطلوب یا کاهش مقیاسپذیری سرورها میگردد. اینجاست که مفهوم برنامهنویسی ناهمزمان (Asynchronous) وارد میدان میشود و با معرفی کلمات کلیدی async
و await
در C#، مایکروسافت انقلابی در نحوه مدیریت عملیاتهای I/O و محاسبات طولانیمدت ایجاد کرد.
هدف این مقاله، ارائه یک دیدگاه جامع و عمیق درباره برنامهنویسی ناهمزمان با استفاده از async
و await
در C# است. ما به بررسی تکامل این مدل برنامهنویسی، مکانیزمهای زیرین آن، پیادهسازی در سناریوهای مختلف، مدیریت خطا و لغو عملیات، و همچنین نکات و ترفندهای پیشرفته برای بهینهسازی عملکرد و جلوگیری از مشکلات رایج خواهیم پرداخت. این مقاله برای توسعهدهندگانی طراحی شده است که به دنبال درک عمیقتر و استفاده مؤثرتر از این قابلیت قدرتمند C# برای ساخت برنامههای کارآمد و مقیاسپذیر هستند.
مقدمهای بر برنامهنویسی ناهمزمان و نیاز به Async/Await
برای درک اهمیت async
و await
، ابتدا باید مفهوم برنامهنویسی ناهمزمان را درک کنیم. در یک برنامه همزمان، هر عملیات به ترتیب اجرا میشود. اگر یک عملیات زمانبر باشد، کل برنامه تا اتمام آن عملیات متوقف میشود. این موضوع در برنامههای دسکتاپ یا موبایل منجر به “فریز” شدن رابط کاربری و در برنامههای سمت سرور (مانند ASP.NET) باعث اشغال شدن رشتههای سرور و کاهش ظرفیت پاسخگویی به درخواستهای همزمان میشود.
برنامهنویسی ناهمزمان به برنامه اجازه میدهد تا در حین انجام یک عملیات طولانیمدت، به کارهای دیگر بپردازد و پس از اتمام آن عملیات، نتیجه را دریافت کند. این رویکرد به ویژه برای عملیاتهای I/O Bound (مانند فراخوانی وب سرویس، دسترسی به دیسک یا پایگاه داده) که بیشتر زمان خود را صرف انتظار برای تکمیل یک فرایند خارجی میکنند، بسیار مفید است. در این حالت، رشته اصلی یا رشته کاری آزاد میشود تا کارهای دیگر را انجام دهد و به محض آماده شدن نتیجه، مجدداً کار را از سر میگیرد.
چرا برنامهنویسی ناهمزمان ضروری شد؟
- پاسخگویی رابط کاربری (UI Responsiveness): در برنامههای دسکتاپ و موبایل، عملیاتهای همزمان میتوانند UI را برای مدت زمان طولانی قفل کنند، که منجر به تجربه کاربری ضعیف میشود. برنامهنویسی ناهمزمان تضمین میکند که UI حتی در حین انجام عملیاتهای سنگین، پاسخگو باقی بماند.
- مقیاسپذیری سرور (Server Scalability): در برنامههای سمت سرور مانند ASP.NET Core، هر درخواست ورودی به یک رشته در Thread Pool اختصاص مییابد. اگر این رشته مشغول انتظار برای عملیات I/O باشد، نمیتواند به درخواستهای دیگر پاسخ دهد. استفاده از
async
/await
باعث میشود که رشته در حین انتظار آزاد شده و به درخواستهای دیگر سرویس دهد، که به طور قابل توجهی مقیاسپذیری و توان عملیاتی سرور را افزایش میدهد. - بهرهوری منابع (Resource Efficiency): با آزاد کردن رشتهها در حین عملیاتهای I/O، منابع سیستم به طور مؤثرتری استفاده میشوند و تعداد رشتههای کمتری برای مدیریت حجم بالای درخواستها نیاز است.
- سادگی کد: قبل از
async
/await
، پیادهسازی برنامهنویسی ناهمزمان پیچیده و مستعد خطا بود (با استفاده از Callbacks، Event Handlers و Manual Thread Management).async
/await
این فرآیند را به طرز چشمگیری ساده کرده و کدی شبیه به کد همزمان ایجاد میکند که خوانایی و نگهداری آن آسانتر است.
درک این نکات اساسی، زمینه را برای فرو رفتن عمیقتر در مکانیزمهای async
و await
و نحوه استفاده مؤثر از آنها فراهم میکند.
تکامل مدلهای برنامهنویسی ناهمزمان در C# (APM, EAP, TPL)
قبل از معرفی async
و await
در C# 5، توسعهدهندگان مجبور بودند از مدلهای پیچیدهتری برای پیادهسازی برنامهنویسی ناهمزمان استفاده کنند. درک این مدلهای قبلی به ما کمک میکند تا ارزش و سادگی async
و await
را بهتر درک کنیم.
1. مدل برنامهنویسی ناهمزمان (Asynchronous Programming Model – APM)
APM که به الگوی Begin/End نیز معروف است، قدیمیترین مدل برنامهنویسی ناهمزمان در داتنت بود که با متدهای BeginX
و EndX
شناخته میشد. متد BeginX
بلافاصله برمیگردد و یک IAsyncResult
را برمیگرداند. متد EndX
منتظر میماند تا عملیات کامل شود و نتیجه را برمیگرداند. توسعهدهندگان میتوانستند از Callbacks (AsyncCallback
) یا Wait Handles برای اطلاع از اتمام عملیات استفاده کنند.
مثال APM:
public void ReadFileAPM(string filePath)
{
FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true); // true for async I/O
byte[] buffer = new byte[fs.Length];
// Begin reading asynchronously
IAsyncResult asyncResult = fs.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(ReadCompletedAPM), fs);
// Can do other work here while file is being read...
Console.WriteLine("Reading file asynchronously (APM)...");
}
private void ReadCompletedAPM(IAsyncResult asyncResult)
{
FileStream fs = (FileStream)asyncResult.AsyncState;
try
{
int bytesRead = fs.EndRead(asyncResult);
Console.WriteLine($"Read {bytesRead} bytes from file (APM).");
// Process buffer
}
catch (Exception ex)
{
Console.WriteLine($"Error reading file (APM): {ex.Message}");
}
finally
{
fs.Close();
}
}
معایب APM:
- پیچیدگی بالا: مدیریت Callbacks و انتقال وضعیت بین Begin/End متدها میتواند بسیار دشوار باشد، به خصوص برای عملیاتهای متوالی.
- “Callback Hell”: زنجیرهای از عملیاتهای ناهمزمان به سرعت منجر به کدی تو در تو و دشوار برای خواندن و دیباگ کردن میشود.
- عدم مدیریت خطا و لغو عملیات استاندارد.
2. الگوی ناهمزمان مبتنی بر رویداد (Event-based Asynchronous Pattern – EAP)
EAP که برای کامپوننتها و کنترلها طراحی شده بود، عملیاتهای ناهمزمان را با استفاده از رویدادها و متدهای Async
/Completed
پیادهسازی میکرد. یک متد XAsync
شروع کننده عملیات بود و یک رویداد XCompleted
(یا مشابه آن) پس از اتمام عملیات فراخوانی میشد.
مثال EAP: (معمولاً در کامپوننتهای UI دیده میشد)
public class MyHttpClient
{
public event EventHandler<DownloadDataCompletedEventArgs> DownloadDataCompleted;
public void DownloadDataAsync(string url)
{
// Simulate async operation
Task.Run(() =>
{
Thread.Sleep(2000); // Simulate network delay
string data = "Some downloaded data";
OnDownloadDataCompleted(new DownloadDataCompletedEventArgs(data, null, false, null));
});
}
protected virtual void OnDownloadDataCompleted(DownloadDataCompletedEventArgs e)
{
DownloadDataCompleted?.Invoke(this, e);
}
}
// Usage:
// MyHttpClient client = new MyHttpClient();
// client.DownloadDataCompleted += (sender, e) => { Console.WriteLine(e.Data); };
// client.DownloadDataAsync("http://example.com/data");
معایب EAP:
- فقط برای سناریوهای رویدادمحور مناسب بود.
- باز هم با “Callback Hell” مواجه میشد، به خصوص برای عملیاتهای متوالی.
- مدیریت خطا و لغو عملیات هنوز چالشبرانگیز بود.
3. کتابخانه موازیسازی تسک (Task Parallel Library – TPL)
TPL که در .NET Framework 4 معرفی شد، یک تغییر پارادایم بزرگ در برنامهنویسی موازی و ناهمزمان ایجاد کرد. TPL مفهوم Task
را به عنوان یک واحد انتزاعی از کار که میتواند به صورت موازی یا ناهمزمان اجرا شود، معرفی کرد. Task
ها قابلیتهای قدرتمندی برای ترکیب، انتظار و مدیریت خطا ارائه میدهند.
مثال TPL (قبل از async/await):
public Task<string> DownloadDataTPL(string url)
{
return Task.Run(() =>
{
// Simulate network operation
Thread.Sleep(2000);
return $"Data from {url}";
});
}
// Usage with ContinueWith for chaining:
// DownloadDataTPL("http://example.com/data")
// .ContinueWith(t =>
// {
// if (t.IsCompletedSuccessfully)
// {
// Console.WriteLine($"Downloaded: {t.Result}");
// }
// else if (t.IsFaulted)
// {
// Console.WriteLine($"Error: {t.Exception.InnerException.Message}");
// }
// });
مزایای TPL:
- انتزاع بالاتر:
Task
یک انتزاع قویتر از رشتهها است. - ترکیبپذیری: متدهایی مانند
ContinueWith
،WhenAll
،WhenAny
امکان ترکیب عملیاتهای ناهمزمان را فراهم میکنند. - مدیریت خطا: قابلیت مدیریت استثنائات با
Task.Exception
.
معایب TPL (به تنهایی):
- هنوز هم نیاز به استفاده از Callbacks (
ContinueWith
) دارد که میتواند کد را پیچیده کند. - خوانایی کد برای عملیاتهای متوالی کمی دشوار است.
با وجود پیشرفتهای TPL، هنوز هم یک راهکار سادهتر و خواناتر برای مدیریت عملیاتهای ناهمزمان نیاز بود. این نیاز منجر به معرفی async
و await
شد که بر پایه TPL بنا شده و تجربه برنامهنویسی ناهمزمان را به طرز چشمگیری ساده کرد.
مبانی Async و Await: درک کلمات کلیدی و مکانیزم زیرین
async
و await
که در C# 5 معرفی شدند، یک انقلاب در برنامهنویسی ناهمزمان ایجاد کردند. این کلمات کلیدی به توسعهدهندگان اجازه میدهند تا کدی بنویسند که به صورت ناهمزمان اجرا میشود، اما از نظر خوانایی و ساختار، شبیه به کد همزمان است. این به اصطلاح “Async/Await Pattern” یک لایه انتزاعی بر روی TPL (Task Parallel Library) ایجاد میکند.
کلمه کلیدی async
کلمه کلیدی async
یک Modifier است که به یک متد (یا lambda expression یا anonymous method) اعمال میشود. این کلمه کلیدی به کامپایلر C# اطلاع میدهد که متد شامل یک یا چند عبارت await
است و بنابراین میتواند به صورت ناهمزمان اجرا شود و قبل از اتمام کامل خود، به فراخواننده بازگردد.
- متدی که با
async
علامتگذاری شده، معمولاًTask
،Task<TResult>
یاvoid
برمیگرداند.Task
: برای متدهای ناهمزمانی که مقداری برنمیگردانند (معادلvoid
در متدهای همزمان).Task<TResult>
: برای متدهای ناهمزمانی که مقداری از نوعTResult
را برمیگردانند.void
: تنها در Event Handlers استفاده میشود. استفاده ازvoid
در متدهایasync
دیگر توصیه نمیشود، زیرا مدیریت استثنائات را پیچیده میکند (استثنائات مستقیماً به SynchronizationContext میروند و نمیتوانند توسط فراخواننده catch شوند).ValueTask
یاValueTask<TResult>
: برای سناریوهای عملکرد بالا که در آن تخصیص شیءTask
میتواند سربار داشته باشد.
- یک متد
async
بدون عبارتawait
به صورت همزمان اجرا میشود و یکTask
کامل شده را برمیگرداند.
کلمه کلیدی await
کلمه کلیدی await
را میتوان تنها در داخل یک متد async
استفاده کرد. اپراتور await
بر روی یک “awaitable” (معمولاً یک Task
یا Task<TResult>
) اعمال میشود. وقتی کامپایلر به یک عبارت await
میرسد، مراحل زیر اتفاق میافتد:
- بررسی میشود که آیا
awaitable
(مثلاًTask
) قبلاً کامل شده است یا خیر.- اگر
Task
کامل شده باشد، اجرای متد بدون وقفه ادامه مییابد. - اگر
Task
هنوز کامل نشده باشد، کنترل به فراخواننده متدasync
بازگردانده میشود. این کار باعث آزاد شدن رشته فعلی (مثلاً رشته UI یا رشته Thread Pool) میشود تا کارهای دیگر را انجام دهد.
- اگر
- زمانی که
awaitable
(Task
) کامل شد، ادامه متدasync
(کدی که بعد ازawait
میآید) در یک رشته مناسب (بر اساسSynchronizationContext
یا Thread Pool) زمانبندی میشود. - اگر
awaitable
(Task
) یک نتیجه (TResult
) داشته باشد، آن نتیجه ازawait
برگردانده میشود. - اگر
awaitable
(Task
) با یک استثنا شکست خورده باشد، آن استثنا دوباره پرتاب میشود.
مکانیزم زیرین: State Machine
جادوی async
و await
در C# توسط کامپایلر انجام میشود. هنگامی که شما یک متد async
مینویسید، کامپایلر آن را به یک کلاس “State Machine” تبدیل میکند. این State Machine مسئول ردیابی وضعیت اجرای متد، ذخیره متغیرهای محلی، و از سرگیری اجرای متد پس از تکمیل await
است.
- State Machine شامل فیلدهایی برای نگهداری متغیرهای محلی متد و پارامترها است.
- عبارت
await
به نقاطی در کد تبدیل میشود که در آنها متد میتواند “متوقف” شده و کنترل را به فراخواننده برگرداند. - وقتی
Task
کامل میشود، State Machine از سر گرفته میشود و اجرای متد از همان نقطه که متوقف شده بود ادامه مییابد.
این مکانیزم پیچیده به توسعهدهنده اجازه میدهد تا کدی خطی و خوانا بنویسد، در حالی که تمام پیچیدگیهای مدیریت رشتهها، Callbacks و Continuationها توسط کامپایلر و .NET Runtime مدیریت میشود.
مثال ساده async
/await
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class AsyncAwaitExample
{
public static async Task Main(string[] args)
{
Console.WriteLine("Starting download...");
string data = await DownloadWebsiteContentAsync("https://www.example.com");
Console.WriteLine($"Download complete. Content length: {data.Length} characters.");
Console.WriteLine("Application finished.");
}
public static async Task<string> DownloadWebsiteContentAsync(string url)
{
using (HttpClient client = new HttpClient())
{
Console.WriteLine($"Downloading from {url}...");
string content = await client.GetStringAsync(url); // This is where the await happens
Console.WriteLine($"Finished downloading from {url}.");
return content;
}
}
}
در این مثال:
- متد
Main
باasync Task
علامتگذاری شده تا بتواند ازawait
استفاده کند. - متد
DownloadWebsiteContentAsync
نیزasync Task<string>
است و یک رشته را به صورت ناهمزمان برمیگرداند. - وقتی
await client.GetStringAsync(url)
فراخوانی میشود وGetStringAsync
هنوز نتیجه را برنگردانده، کنترل به متدMain
بازمیگردد. Main
به اجرای خود ادامه نمیدهد، اما رشته اصلی برنامه آزاد میشود تا کارهای دیگر (اگر وجود داشتند) را انجام دهد.- پس از اتمام
GetStringAsync
، رشته مناسبی (احتمالاً یک رشته از Thread Pool) برای ادامه اجرایDownloadWebsiteContentAsync
و سپسMain
انتخاب میشود.
SynchronizationContext و ConfigureAwait(false)
یک مفهوم کلیدی دیگر در برنامهنویسی async
/await
، SynchronizationContext
است. در محیطهای UI (مثل WinForms, WPF, ASP.NET قدیمی)، یک SynchronizationContext
وجود دارد که تضمین میکند ادامه یک متد async
(کدی که بعد از await
میآید) در همان رشتهای اجرا شود که قبل از await
بود (مثلاً رشته UI). این برای دسترسی ایمن به کنترلهای UI ضروری است.
با این حال، در برنامههای سمت سرور یا کتابخانهها، اغلب نیازی به بازگشت به همان SynchronizationContext
نیست و این کار میتواند سربار ایجاد کند یا حتی منجر به Deadlock شود (مخصوصاً در محیطهایی که SynchronizationContext
وجود دارد اما رشته برای انجام عملیات await
شده به کار دیگری مشغول است).
برای جلوگیری از این مشکل و بهینهسازی عملکرد، میتوان از .ConfigureAwait(false)
استفاده کرد:
public async Task<string> DownloadDataOptimizedAsync(string url)
{
using (HttpClient client = new HttpClient())
{
// No need to return to the original SynchronizationContext
string content = await client.GetStringAsync(url).ConfigureAwait(false);
return content;
}
}
وقتی ConfigureAwait(false)
استفاده میشود، .NET Runtime تلاش نمیکند تا ادامه متد را در SynchronizationContext
اصلی زمانبندی کند. در عوض، ادامه متد میتواند در هر رشته Thread Pool موجود اجرا شود. این کار به افزایش مقیاسپذیری سرور و جلوگیری از Deadlock کمک میکند.
قاعده کلی:
- اگر در یک متد کتابخانهای (که ممکن است هم در UI و هم در بکاند استفاده شود) یا در کدهای سمت سرور (مثل ASP.NET Core) هستید، تقریباً همیشه از
.ConfigureAwait(false)
استفاده کنید. - اگر در کد رابط کاربری (WinForms, WPF, UWP, Xamarin) هستید و نیاز دارید که بعد از
await
به رشته UI برگردید تا کنترلهای UI را بهروز کنید، از.ConfigureAwait(false)
استفاده نکنید (یا به عبارت دیگر، آن را حذف کنید).
درک عمیق این مفاهیم اساسی، پایه و اساس استفاده صحیح و کارآمد از async
و await
را تشکیل میدهد.
پیادهسازی Async/Await در سناریوهای مختلف: از UI تا سرویسهای بکاند
قدرت و انعطافپذیری async
/await
در C# به گونهای است که میتوان آن را در طیف وسیعی از سناریوهای برنامهنویسی پیادهسازی کرد. در ادامه به بررسی کاربردهای کلیدی آن در محیطهای مختلف میپردازیم.
1. برنامههای دسکتاپ و موبایل (WinForms, WPF, UWP, Xamarin)
در برنامههای UI، استفاده از async
/await
حیاتی است تا رابط کاربری در حین انجام عملیاتهای طولانیمدت (مانند بارگذاری داده از اینترنت، پردازش فایلهای بزرگ یا کوئریهای پایگاه داده) پاسخگو باقی بماند. رشته UI مسئول پردازش رویدادها و بهروزرسانی کنترلهای UI است. اگر این رشته قفل شود، برنامه “فریز” میشود.
مثال WPF (Button Click Event Handler):
using System.Windows;
using System.Net.Http;
using System.Threading.Tasks;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private async void LoadDataButton_Click(object sender, RoutedEventArgs e)
{
// Disable button to prevent multiple clicks
LoadDataButton.IsEnabled = false;
TextBlockStatus.Text = "Loading data...";
try
{
string data = await GetDataFromWebAsync("https://www.example.com/api/data");
TextBlockStatus.Text = $"Data loaded: {data.Length} characters.";
// Update other UI elements with 'data'
}
catch (HttpRequestException ex)
{
TextBlockStatus.Text = $"Error: {ex.Message}";
}
finally
{
LoadDataButton.IsEnabled = true;
}
}
private async Task<string> GetDataFromWebAsync(string url)
{
using (HttpClient client = new HttpClient())
{
// Note: No ConfigureAwait(false) here because we need to return to UI thread
// to update UI after completion.
return await client.GetStringAsync(url);
}
}
}
در این مثال، LoadDataButton_Click
یک async void
متد است (که برای Event Handlers قابل قبول است). وقتی await GetDataFromWebAsync(...)
فراخوانی میشود، کنترل بلافاصله به WPF UI thread بازمیگردد و UI پاسخگو میماند. پس از دریافت پاسخ از وب، ادامه متد LoadDataButton_Click
(بعد از await
) در همان رشته UI ادامه مییابد و اجازه میدهد تا TextBlockStatus
بدون خطا بهروز شود.
2. برنامههای سمت سرور (ASP.NET Core Web API, MVC)
در برنامههای سمت سرور، هدف اصلی async
/await
افزایش مقیاسپذیری و توان عملیاتی است. هر درخواست وب به طور پیشفرض توسط یک رشته از Thread Pool پردازش میشود. اگر این رشته مشغول انتظار برای یک عملیات I/O باشد (مانند فراخوانی پایگاه داده یا سرویس خارجی)، نمیتواند به درخواستهای دیگر پاسخ دهد و منجر به اشغال شدن رشتههای Thread Pool میشود. استفاده از async
/await
این رشته را در حین انتظار آزاد میکند تا به درخواستهای دیگر سرویس دهد.
مثال ASP.NET Core Controller:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
// Awaiting a database call or external service call
var products = await _productService.GetAllProductsAsync();
return Ok(products);
}
[HttpPost]
public async Task<ActionResult<Product>> AddProduct(Product product)
{
var newProduct = await _productService.AddProductAsync(product);
return CreatedAtAction(nameof(GetProducts), new { id = newProduct.Id }, newProduct);
}
}
// Example ProductService interface and implementation
public interface IProductService
{
Task<IEnumerable<Product>> GetAllProductsAsync();
Task<Product> AddProductAsync(Product product);
}
public class ProductService : IProductService
{
// Assume a DbContext or HttpClient is injected
private readonly ApplicationDbContext _dbContext; // Or HttpClient, etc.
public ProductService(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<Product>> GetAllProductsAsync()
{
// Example: Asynchronous database query with Entity Framework Core
// Use ConfigureAwait(false) in service/repository layer for optimal performance
return await _dbContext.Products.ToListAsync().ConfigureAwait(false);
}
public async Task<Product> AddProductAsync(Product product)
{
_dbContext.Products.Add(product);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
return product;
}
}
در کد بالا، کنترلرها و سرویسها از async
/await
برای مدیریت عملیاتهای پایگاه داده یا فراخوانی سرویسهای خارجی استفاده میکنند. این کار باعث میشود که رشتههای Thread Pool بلافاصله پس از شروع عملیات I/O آزاد شوند و برای پردازش درخواستهای دیگر در دسترس باشند. استفاده از ConfigureAwait(false)
در لایههای پایینتر (مانند ProductService
) به افزایش کارایی کمک میکند زیرا نیازی به بازیابی SynchronizationContext
مربوط به درخواست وب نیست.
3. برنامههای کنسول و Worker Services
در برنامههای کنسول یا Worker Services که معمولاً عملیاتهای طولانیمدت (پردازش فایلها، ارسال پیامها، محاسبات سنگین) را انجام میدهند، async
/await
میتواند به مدیریت کارآمدتر این عملیاتها کمک کند. اگرچه پاسخگویی UI مطرح نیست، اما استفاده از آن باعث بهرهوری بهتر از Thread Pool و جلوگیری از اشغال بیمورد رشتهها میشود.
مثال Console Application:
using System;
using System.Threading.Tasks;
using System.IO;
public class ConsoleApp
{
public static async Task Main(string[] args)
{
Console.WriteLine("Starting file processing...");
await ProcessLargeFileAsync("large_data.txt");
Console.WriteLine("File processing complete.");
}
public static async Task ProcessLargeFileAsync(string filePath)
{
if (!File.Exists(filePath))
{
Console.WriteLine($"Creating dummy file: {filePath}");
await File.WriteAllTextAsync(filePath, new string('A', 1000000)); // Create a large file
}
Console.WriteLine($"Reading file: {filePath}");
string content = await File.ReadAllTextAsync(filePath); // Asynchronous file I/O
Console.WriteLine($"File read. Content length: {content.Length}");
Console.WriteLine("Performing dummy long computation...");
await Task.Delay(3000); // Simulate a long running computation
Console.WriteLine("Computation finished.");
}
}
در این مثال، متدهای File.ReadAllTextAsync
و Task.Delay
عملیاتهای ناهمزمان را انجام میدهند. await
تضمین میکند که برنامه منتظر تکمیل این عملیاتها میماند بدون اینکه رشته اجرایی را مسدود کند.
4. کتابخانهها (Class Libraries)
هنگام توسعه کتابخانهها، بسیار مهم است که متدهای ناهمزمان به گونهای طراحی شوند که برای هر دو محیط UI و غیر UI (مانند سرور) بهینه باشند. این بدان معناست که شما باید از .ConfigureAwait(false)
به طور پیشفرض در متدهای کتابخانهای خود استفاده کنید تا از Deadlock جلوگیری کرده و عملکرد را بهینه سازید.
مثال Library Method:
using System.Net.Http;
using System.Threading.Tasks;
public class MyLibraryClient
{
private readonly HttpClient _httpClient;
public MyLibraryClient()
{
_httpClient = new HttpClient();
}
public async Task<string> GetSomeDataFromApiAsync(string apiEndpoint)
{
// Always use ConfigureAwait(false) in library code
// unless you explicitly need to return to the original SynchronizationContext.
// This avoids deadlocks when consumers call this method from UI threads
// and improves performance on server-side applications.
string data = await _httpClient.GetStringAsync(apiEndpoint).ConfigureAwait(false);
return data;
}
public async Task ProcessDataLocallyAsync(byte[] data)
{
// Simulate a CPU-bound operation that should run on a Thread Pool thread
await Task.Run(() =>
{
// Perform heavy computation here
System.Threading.Thread.Sleep(1000); // For demonstration
Console.WriteLine("Data processed locally.");
}).ConfigureAwait(false);
}
}
در متد GetSomeDataFromApiAsync
، استفاده از ConfigureAwait(false)
ضروری است. در متد ProcessDataLocallyAsync
، Task.Run
برای انتقال یک عملیات CPU-bound به یک رشته Thread Pool استفاده میشود، و سپس ConfigureAwait(false)
برای اطمینان از اینکه ادامه متد در هر رشتهای از Thread Pool ادامه مییابد، به کار میرود.
با پیادهسازی صحیح async
/await
در این سناریوهای مختلف، میتوان برنامههایی ساخت که هم از نظر عملکرد و هم از نظر تجربه کاربری بهینهسازی شدهاند.
مدیریت خطا، لغو عملیات و بهینهسازی پیشرفته با Async/Await
همانند برنامهنویسی همزمان، مدیریت خطا، قابلیت لغو و بهینهسازی عملکرد در برنامهنویسی ناهمزمان نیز از اهمیت بالایی برخوردار است. async
/await
ابزارهای قدرتمندی برای این منظور فراهم میکند.
مدیریت خطا (Exception Handling)
یکی از مزایای بزرگ async
/await
این است که مدیریت استثنائات در آن شبیه به کد همزمان با استفاده از بلوکهای try-catch-finally
انجام میشود. استثناهایی که در یک متد async
(قبل یا بعد از یک await
) رخ میدهند، توسط Task
شامل میشوند و زمانی که Task
await
میشود، دوباره پرتاب (re-thrown) میشوند.
مثال مدیریت خطا:
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class ErrorHandlingExample
{
public static async Task Main(string[] args)
{
try
{
Console.WriteLine("Attempting to download from an invalid URL...");
string data = await DownloadInvalidUrlAsync("http://invalid.url.com");
Console.WriteLine($"Downloaded: {data.Length} characters.");
}
catch (HttpRequestException httpEx)
{
Console.WriteLine($"Caught HTTP Request Error: {httpEx.Message}");
// Log the exception, show user a message, etc.
}
catch (Exception ex)
{
Console.WriteLine($"Caught General Error: {ex.Message}");
}
Console.WriteLine("\nAttempting to download from a valid URL...");
try
{
// Example of a try-catch within the async method itself
string data = await DownloadValidUrlWithInternalCatchAsync("https://www.example.com");
Console.WriteLine($"Downloaded: {data.Length} characters.");
}
catch (Exception ex)
{
Console.WriteLine($"Caught Error from internal try-catch: {ex.Message}");
}
}
public static async Task<string> DownloadInvalidUrlAsync(string url)
{
using (HttpClient client = new HttpClient())
{
// This will throw HttpRequestException
string content = await client.GetStringAsync(url).ConfigureAwait(false);
return content;
}
}
public static async Task<string> DownloadValidUrlWithInternalCatchAsync(string url)
{
using (HttpClient client = new HttpClient())
{
try
{
string content = await client.GetStringAsync(url).ConfigureAwait(false);
if (content.Length < 100)
{
throw new InvalidOperationException("Content is too short!");
}
return content;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Internal HTTP Error: {ex.Message}. Returning default.");
return "Default Content On Error"; // Or rethrow, depending on logic
}
}
}
}
هنگامی که چندین await
در یک try
بلوک وجود دارد، اگر هر کدام از آنها خطا دهد، بقیه await
ها اجرا نمیشوند و کنترل به بلوک catch
منتقل میشود. اگر چندین Task
را به صورت موازی اجرا کنید (با Task.WhenAll
)، همه استثنائات در Task.Exception.InnerExceptions
جمعآوری میشوند و شما میتوانید آنها را به صورت یکجا مدیریت کنید.
لغو عملیات (Cancellation)
قابلیت لغو عملیاتهای ناهمزمان برای برنامههای پاسخگو و کارآمد بسیار مهم است. کاربر ممکن است بخواهد یک عملیات طولانیمدت را متوقف کند، یا یک سرویس ممکن است به دلیل Time-out یا Shutdown نیاز به لغو عملیاتهای در حال انجام داشته باشد. در داتنت، لغو عملیات با استفاده از CancellationTokenSource
و CancellationToken
پیادهسازی میشود.
مکانیزم لغو:
- یک
CancellationTokenSource
ایجاد کنید. - یک
CancellationToken
ازCancellationTokenSource
بگیرید. - این
CancellationToken
را به متدهای ناهمزمان خود ارسال کنید. - در داخل متدهای ناهمزمان، به صورت دورهای
token.ThrowIfCancellationRequested()
را فراخوانی کنید یاtoken.IsCancellationRequested
را بررسی کنید. - برای درخواست لغو، متد
Cancel()
را رویCancellationTokenSource
فراخوانی کنید.
مثال لغو عملیات:
using System;
using System.Threading;
using System.Threading.Tasks;
public class CancellationExample
{
public static async Task Main(string[] args)
{
using (CancellationTokenSource cts = new CancellationTokenSource())
{
Task longRunningTask = PerformLongOperationAsync(5000, cts.Token);
Console.WriteLine("Press 'c' to cancel the operation.");
// Simulate user input for cancellation
Task.Run(() =>
{
if (Console.ReadKey().KeyChar == 'c')
{
cts.Cancel();
Console.WriteLine("\nCancellation requested!");
}
});
try
{
await longRunningTask;
Console.WriteLine("Operation completed successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled!");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
}
public static async Task PerformLongOperationAsync(int durationMs, CancellationToken cancellationToken)
{
Console.WriteLine($"Long operation started for {durationMs}ms.");
try
{
for (int i = 0; i < durationMs / 1000; i++)
{
cancellationToken.ThrowIfCancellationRequested(); // Check for cancellation
Console.WriteLine($"Working... {i + 1}s passed.");
await Task.Delay(1000, cancellationToken); // Task.Delay also respects CancellationToken
}
Console.WriteLine("Long operation finished without cancellation.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Long operation was internally cancelled.");
throw; // Re-throw to be caught by the caller
}
}
}
در این مثال، اگر کاربر ‘c’ را فشار دهد، cts.Cancel()
فراخوانی میشود و OperationCanceledException
پرتاب میشود که توسط بلوک catch
در Main
گرفته میشود.
بهینهسازی پیشرفته
1. ValueTask برای کاهش تخصیص حافظه
در سناریوهای با کارایی بالا که تخصیص حافظه (memory allocation) برای Task
میتواند سربار زیادی ایجاد کند (به ویژه در حلقههای داغ یا زمانی که متد async
اغلب به صورت همزمان کامل میشود)، میتوان از ValueTask
به جای Task
استفاده کرد. ValueTask
یک نوع ساختار است که میتواند یا یک نتیجه را به صورت مستقیم نگهداری کند یا به یک Task
ارجاع دهد. این کار میتواند تخصیص شیء را در برخی موارد حذف کند.
using System.Threading.Tasks;
public class AdvancedOptimization
{
private static int _cache = 123; // Simulate a cached value
// Using ValueTask to avoid Task allocation if result is immediately available
public static async ValueTask<int> GetCachedValueOrComputeAsync()
{
if (_cache != 0) // Assume 0 means not cached
{
return _cache; // No Task allocation
}
// Simulate a network call if not cached
await Task.Delay(100);
_cache = 456;
return _cache;
}
}
ValueTask
برای متدهایی که در اکثر مواقع به صورت همزمان کامل میشوند، مفید است. در غیر این صورت، Task
همچنان انتخاب مناسبی است.
2. IAsyncEnumerable برای Stream کردن داده
در .NET Core 3.0 و C# 8.0، مفهوم IAsyncEnumerable<T>
و await foreach
معرفی شد که امکان Stream کردن دادهها به صورت ناهمزمان را فراهم میکند. این کار برای سناریوهایی که نیاز به پردازش مجموعه بزرگی از دادهها به صورت تکه تکه (بدون بارگذاری همه آنها در حافظه به یکباره) دارند، بسیار مفید است.
مثال IAsyncEnumerable:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public class AsyncEnumerableExample
{
public static async IAsyncEnumerable<int> GenerateNumbersAsync(int count, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100); // Simulate async operation per item
yield return i;
}
}
public static async Task Main(string[] args)
{
Console.WriteLine("Generating numbers asynchronously:");
try
{
await foreach (var number in GenerateNumbersAsync(10))
{
Console.WriteLine($"Received: {number}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Number generation cancelled.");
}
}
}
await foreach
امکان تکرار بر روی یک IAsyncEnumerable
را فراهم میکند، در حالی که yield return
در یک متد async
IAsyncEnumerable
را تولید میکند. این الگو برای APIهای استریمینگ و خواندن/نوشتن فایلهای بزرگ به صورت ناهمزمان بسیار کارآمد است.
3. Task.WhenAll و Task.WhenAny
برای اجرای چندین عملیات ناهمزمان به صورت موازی و انتظار برای تکمیل همه یا اولین آنها، میتوانید از Task.WhenAll
و Task.WhenAny
استفاده کنید.
Task.WhenAll(task1, task2, ...)
: یکTask
را برمیگرداند که زمانی کامل میشود که تمامTask
های ورودی کامل شده باشند. اگر هر کدام از آنها خطا دهد،WhenAll
نیز خطا میدهد.Task.WhenAny(task1, task2, ...)
: یکTask
را برمیگرداند که زمانی کامل میشود که اولینTask
از بینTask
های ورودی کامل شده باشد.
این ابزارها برای سناریوهایی مانند فراخوانی موازی چندین سرویس وب یا اجرای چندین کوئری پایگاه داده به صورت همزمان بسیار مفید هستند.
با درک و به کارگیری این تکنیکها، توسعهدهندگان میتوانند برنامههایی با کارایی بالا، پاسخگو و مقاوم در برابر خطا ایجاد کنند.
نکات و ترفندهای پیشرفته، بهترین شیوهها و جلوگیری از بنبستها
استفاده صحیح از async
/await
فراتر از تنها نوشتن کدهای ناهمزمان است. رعایت بهترین شیوهها و آگاهی از مشکلات رایج میتواند به جلوگیری از بنبستها (Deadlocks) و بهینهسازی عملکرد کمک کند.
1. Callbacks، Event Handlers و Async Void
همانطور که قبلاً اشاره شد، استفاده از async void
تنها در Event Handlers توصیه میشود. دلیل آن این است که استثنائات پرتاب شده از یک متد async void
مستقیماً به SynchronizationContext
ارسال میشوند و توسط فراخواننده قابل گرفتن (catch) نیستند. این موضوع دیباگ کردن و مدیریت خطا را دشوار میکند.
// Bad practice: async void for non-event handler methods
// public async void MyBusinessLogicMethod()
// {
// throw new InvalidOperationException("This exception will be unhandled!");
// }
// Good practice: async Task for business logic
public async Task MyBusinessLogicMethodAsync()
{
// ...
throw new InvalidOperationException("This exception can be caught by the caller!");
}
2. جلوگیری از بنبستها (Deadlocks)
یکی از رایجترین و گیجکنندهترین مشکلات در برنامهنویسی ناهمزمان، Deadlock است. این مشکل زمانی رخ میدهد که یک رشته منتظر یک عملیات ناهمزمان باشد که خود آن عملیات نیاز به بازگشت به همان رشته دارد تا ادامه یابد.
سناریوی Deadlock رایج (در UI یا ASP.NET قدیمی):
// This code would deadlock in a UI application (e.g., WinForms button click)
// or older ASP.NET applications if not used carefully.
public string GetContentSynchronously()
{
// This blocks the UI thread and waits for the async method to complete.
// The async method tries to resume on the UI thread after await,
// but the UI thread is already blocked here. Deadlock!
return GetContentAsync().Result; // Or .Wait()
}
public async Task<string> GetContentAsync()
{
using (HttpClient client = new HttpClient())
{
return await client.GetStringAsync("http://example.com");
}
}
راه حل:
- استفاده از
.ConfigureAwait(false)
: این مهمترین راهکار برای جلوگیری از Deadlock در کتابخانهها و کدهای سمت سرور است. باConfigureAwait(false)
، ادامه متدasync
نیازی به بازگشت بهSynchronizationContext
اصلی ندارد. - فراخوانی از طریق
async
/await
سراسری: بهترین راهکار این است که زنجیره فراخوانی کاملاً ناهمزمان باشد. یعنی از متدهایasync
به متدهایasync
دیگر فراخوانی کنید. - اجتناب از
.Result
یا.Wait()
: این متدها باعث بلاک شدن رشته جاری میشوند و به همین دلیل عامل اصلی Deadlock هستند. فقط زمانی از آنها استفاده کنید که مطمئن هستید هیچSynchronizationContext
درگیر نیست یا در ابتداییترین نقطه ورودی برنامه (مثلMain
) که هیچ UI threadی درگیر نیست.
// Correct approach: End-to-end async
public async Task<string> GetContentCorrectAsync()
{
return await GetContentInternalAsync();
}
public async Task<string> GetContentInternalAsync()
{
using (HttpClient client = new HttpClient())
{
// Use ConfigureAwait(false) in library/service methods
return await client.GetStringAsync("http://example.com").ConfigureAwait(false);
}
}
3. ترکیب عملیاتهای CPU-bound و I/O-bound
async
/await
عمدتاً برای عملیاتهای I/O-bound (انتظار برای تکمیل) بهینه است. برای عملیاتهای CPU-bound (محاسبات سنگین)، هنوز هم باید آنها را به یک رشته دیگر منتقل کنید تا رشته اصلی یا UI آزاد بماند.
استفاده از Task.Run
:
public async Task<int> PerformHeavyCalculationAsync()
{
// Moves the CPU-bound work to a Thread Pool thread
int result = await Task.Run(() =>
{
Console.WriteLine($"Starting heavy calculation on thread {Thread.CurrentThread.ManagedThreadId}");
// Simulate heavy calculation
long sum = 0;
for (int i = 0; i < 1000000000; i++)
{
sum += i;
}
Console.WriteLine($"Finished heavy calculation on thread {Thread.CurrentThread.ManagedThreadId}");
return (int)(sum % int.MaxValue);
}); // No ConfigureAwait(false) needed directly on Task.Run result if you want to come back to context
return result;
}
Task.Run
یک عملیات CPU-bound را به یک رشته Thread Pool منتقل میکند و یک Task
را برمیگرداند که میتوانید آن را await
کنید.
4. از async
/await
برای هر متد استفاده نکنید
فقط متدهایی را async
کنید که واقعاً عملیات ناهمزمان (I/O-bound یا CPU-bound با Task.Run
) انجام میدهند. اضافه کردن async
/await
به متدهای همزمان فقط سربار ایجاد میکند.
5. نامگذاری (Naming Conventions)
طبق قراردادهای داتنت، متدهای ناهمزمان باید با پسوند Async
(مثلاً GetDataAsync
) نامگذاری شوند تا نشان دهند که یک Task
یا ValueTask
را برمیگردانند و میتوانند await
شوند.
6. استفاده از Async-aware APIs
همیشه سعی کنید از نسخههای ناهمزمان APIها استفاده کنید (مثلاً Stream.ReadAsync
به جای Stream.Read
، HttpClient.GetStringAsync
به جای HttpClient.GetString
، DbContext.ToListAsync
به جای DbContext.ToList
). این APIها به طور داخلی بهینهسازی شدهاند تا عملیاتهای I/O را به صورت ناهمزمان انجام دهند و رشتهها را آزاد کنند.
7. مدیریت Lifetime منابع با using
async
/await
با using
کار میکند، حتی اگر Dispose
کردن منابع زمانبر باشد، میتوان از IAsyncDisposable
و await using
استفاده کرد (از C# 8). این اطمینان میدهد که منابع به درستی آزاد میشوند.
// Before C# 8:
public async Task OldStyleUsingExample()
{
HttpClient client = null;
try
{
client = new HttpClient();
await client.GetStringAsync("http://example.com");
}
finally
{
client?.Dispose();
}
}
// C# 8 and later:
public async Task NewStyleUsingExample()
{
using (HttpClient client = new HttpClient()) // HttpClient is IDisposable
{
await client.GetStringAsync("http://example.com");
} // client.Dispose() is called automatically
}
// With IAsyncDisposable (e.g. some Stream types)
public async Task AsyncDisposeExample()
{
await using (var fileStream = new FileStream("test.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
await fileStream.WriteAsync(new byte[] { 1, 2, 3 });
} // fileStream.DisposeAsync() is called automatically
}
8. بررسی وضعیت Task
میتوانید وضعیت یک Task
را با خواص IsCompleted
، IsCompletedSuccessfully
، IsFaulted
، IsCanceled
بررسی کنید. اینها به شما امکان میدهند تا منطق شرطی را بر اساس نتیجه Task
پیادهسازی کنید.
با رعایت این نکات و بهترین شیوهها، میتوانید از قدرت کامل async
/await
بهره ببرید و برنامههایی robust و با عملکرد بالا بسازید.
مقایسه Async/Await با رویکردهای همزمان و موازیسازی سنتی
برای درک کامل ارزش async
/await
، مهم است که آن را با رویکردهای سنتی برنامهنویسی همزمان (Synchronous) و موازیسازی (Parallelism) مقایسه کنیم. این مقایسه به ما کمک میکند تا بفهمیم async
/await
در چه سناریوهایی بهترین انتخاب است و تفاوتهای آن با Threading سنتی چیست.
1. برنامهنویسی همزمان (Synchronous Programming)
در برنامهنویسی همزمان، هر عملیات به ترتیب انجام میشود و تا زمانی که عملیات فعلی کامل نشود، عملیات بعدی آغاز نمیشود. این رویکرد سادهترین روش برنامهنویسی است، اما معایب قابل توجهی در مواجهه با عملیاتهای زمانبر دارد.
مزایا:
- سادهترین مدل برنامهنویسی.
- کد خطی و آسان برای دنبال کردن و دیباگ کردن (بدون نیاز به نگرانی از Race Conditions یا Deadlocks ناشی از Concurrency).
معایب:
- مسدود شدن (Blocking): در حین عملیاتهای I/O Bound یا CPU Bound طولانیمدت، رشته اجرایی (مانند رشته UI یا رشته سرور) مسدود میشود.
- عدم پاسخگویی UI: رابط کاربری در طول عملیاتهای طولانی “فریز” میشود.
- کاهش مقیاسپذیری سرور: رشتهها اشغال میشوند و نمیتوانند به درخواستهای جدید سرویس دهند، که منجر به کاهش توان عملیاتی سرور میشود.
- اتلاف منابع: رشتههای مسدود شده منابع سیستم (حافظه و CPU) را اشغال میکنند در حالی که منتظر عملیات I/O هستند.
2. موازیسازی (Parallelism) با Threading یا TPL
موازیسازی به معنای اجرای همزمان چندین بخش از یک برنامه (یا چندین کار مستقل) بر روی چندین هسته CPU است. هدف اصلی آن بهبود عملکرد با کاهش زمان کلی اجرای یک کار CPU-bound است.
روشها:
- Threadها به صورت دستی: ایجاد و مدیریت مستقیم رشتهها (
new Thread()
). بسیار پیچیده و مستعد خطا (Race Conditions، Deadlocks). - Thread Pool: استفاده از Thread Pool برای مدیریت رشتهها، کاهش سربار ایجاد/حذف رشته.
- Task Parallel Library (TPL): انتزاعی بالاتر از رشتهها، با قابلیتهایی مانند
Parallel.For
،Parallel.ForEach
،Task.Run
برای عملیاتهای CPU-bound.
مزایا:
- بهبود عملکرد برای عملیاتهای CPU-bound با استفاده از چندین هسته پردازنده.
- میتواند به حفظ پاسخگویی UI کمک کند (با انتقال کار به رشتههای پسزمینه).
معایب:
- پیچیدگی: مدیریت رشتهها، همگامسازی (Synchronization)، Race Conditions و Deadlocks میتواند بسیار پیچیده باشد.
- سربار منابع: هر رشته مقدار مشخصی از حافظه و سربار CPU را به خود اختصاص میدهد. تعداد زیادی رشته میتواند منجر به سربار Switch Context و کاهش عملکرد شود.
- عدم کارایی برای I/O-bound: برای عملیاتهای I/O-bound، Threading سنتی به سادگی رشته را مسدود میکند و منابع را بیهوده اشغال میکند.
- مدیریت خطا دشوار: مدیریت استثنائات در Threadهای جداگانه پیچیدهتر است.
3. برنامهنویسی ناهمزمان با Async/Await
async
/await
یک رویکرد ناهمزمان است که عمدتاً برای عملیاتهای I/O-bound طراحی شده است. هدف آن آزاد کردن رشتههای اجرایی در حین انتظار برای تکمیل عملیاتهای I/O است.
مزایا:
- بهرهوری بالا برای I/O-bound: رشتهها در حین انتظار برای عملیاتهای I/O آزاد میشوند، که منجر به استفاده کارآمدتر از منابع و افزایش مقیاسپذیری (به ویژه در سرورها) میشود.
- پاسخگویی UI: رابط کاربری حتی در حین عملیاتهای طولانیمدت پاسخگو باقی میماند.
- سادگی کد: کد شبیه به کد همزمان نوشته میشود و از پیچیدگی Callbacks و مدیریت دستی رشتهها جلوگیری میکند.
- مدیریت خطای آسان: استثنائات با بلوکهای
try-catch
استاندارد مدیریت میشوند. - قابلیت لغو: یکپارچگی خوب با
CancellationToken
برای لغو عملیاتها.
معایب:
- مفهوم جدید: نیاز به درک مفاهیم
Task
،SynchronizationContext
وConfigureAwait(false)
. - پیچیدگی در ترکیب با کد همزمان: ترکیب کد
async
و همزمان میتواند منجر به Deadlock شود (مگر با دقت و رعایتConfigureAwait(false)
). - برای CPU-bound به تنهایی کافی نیست:
async
/await
به تنهایی یک عملیات CPU-bound را موازی نمیکند؛ برای این کار نیاز بهTask.Run
است.
جدول مقایسه خلاصه
ویژگی | همزمان (Synchronous) | موازیسازی (Parallelism) | ناهمزمان (Async/Await) |
---|---|---|---|
هدف اصلی | سادگی، اجرای ترتیبی | افزایش سرعت اجرای CPU-bound با بهرهگیری از هستهها | افزایش پاسخگویی و مقیاسپذیری برای I/O-bound |
نوع عملیات | هر دو I/O و CPU | عمدتاً CPU-bound | عمدتاً I/O-bound |
مدیریت رشته | یک رشته را مسدود میکند | چندین رشته را فعال میکند | رشته را آزاد میکند (غیرمسدود کننده) |
پیچیدگی کد | پایین | بالا (نیاز به همگامسازی) | متوسط (اما سادهتر از Callbacks) |
پاسخگویی UI | پایین (UI فریز میشود) | بالا (با انتقال کار به پسزمینه) | بالا (UI پاسخگو میماند) |
مقیاسپذیری سرور | پایین (رشتهها اشغال میشوند) | متوسط (برای CPU-bound خوب، برای I/O-bound بد) | بالا (رشتهها آزاد میشوند) |
بهترین سناریو | عملیاتهای سریع و بدون انتظار | محاسبات پیچیده، پردازش دادههای بزرگ | فراخوانی وب سرویس، دسترسی به دیسک/پایگاه داده، هر عملیات I/O |
نتیجهگیری این است که async
/await
و موازیسازی (با Task.Run
) مکمل یکدیگر هستند. شما از async
/await
برای مدیریت عملیاتهای I/O و حفظ پاسخگویی و مقیاسپذیری استفاده میکنید و اگر نیاز به انجام محاسبات سنگین دارید، آنها را با Task.Run
به رشتههای Thread Pool منتقل میکنید تا همزمان با async
/await
کار کنند.
آینده برنامهنویسی ناهمزمان در C# و .NET
جامعه برنامهنویسی داتنت و تیم توسعه C# به طور مداوم در حال پیشرفت و بهبود قابلیتهای برنامهنویسی ناهمزمان هستند. async
/await
یک پایه قدرتمند ایجاد کرده است، اما نوآوریها و بهینهسازیهای بیشتری در راه است تا این مدل را حتی کارآمدتر و قابل دسترستر کند.
1. بهینهسازیهای داخلی و Performance Enhancements
هر نسخه جدید از .NET (مانند .NET 6, .NET 7, .NET 8) بهبودهای عملکردی قابل توجهی در نحوه اجرای async
/await
در سطح Runtime و JIT compiler ارائه میدهد. این بهینهسازیها شامل کاهش تخصیص حافظه برای State Machine، بهبود Thread Pool و زمانبندی Taskها است. هدف کاهش سربار (overhead) استفاده از async
/await
و نزدیکتر کردن عملکرد آن به کدهای همزمان در سناریوهای خاص است.
مثال: بهینهسازیهای مربوط به ValueTask
و TaskCache
که تخصیص حافظه را برای Taskهای از پیش کامل شده به حداقل میرساند.
2. Asynchronous Streams با IAsyncEnumerable/IAsyncEnumerator
همانطور که در بخش مدیریت خطا و بهینهسازی پیشرفته ذکر شد، IAsyncEnumerable
و await foreach
(معرفی شده در C# 8 و .NET Core 3.0) یک گام بزرگ رو به جلو در مدیریت دادههای استریمینگ ناهمزمان بودند. این قابلیت به برنامهها اجازه میدهد تا به صورت ناهمزمان روی مجموعههای بزرگی از دادهها تکرار کنند بدون اینکه همه آنها را به یکباره در حافظه بارگذاری کنند. انتظار میرود که این الگو در APIهای .NET و کتابخانههای شخص ثالث بیشتر مورد استفاده قرار گیرد.
// Revisited for future implications
public async IAsyncEnumerable<LogEntry> GetLogEntriesFromDatabaseAsync(DateTime startDate, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Simulate database cursor or streaming API
await foreach (var row in _dbContext.LogEntries.Where(x => x.Timestamp >= startDate).AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return row;
}
}
این رویکرد به ویژه برای Microservices، APIهای استریمینگ و برنامههایی که با حجم زیادی از دادهها سروکار دارند (مانند ETL pipelines) بسیار قدرتمند است.
3. Custom Async Methods و Poolable Async State Machines
برای توسعهدهندگان کتابخانهها و فریمورکهای سطح پایین، امکان ایجاد “Custom Awaitables” و “Custom Async Methods” از قبل وجود داشته است، اما پیچیده بود. در نسخههای جدیدتر C# و .NET، قابلیتهای پیشرفتهتری برای کنترل دقیقتر بر روی State Machine و Pool کردن آنها برای کاهش تخصیص حافظه در سناریوهای بسیار خاص و عملکرد محور معرفی شده است. این امکانات به ندرت توسط توسعهدهندگان معمولی استفاده میشوند، اما برای بهینهسازیهای زیربنایی بسیار مهم هستند.
مثال: استفاده از IValueTaskSource
برای پیادهسازی Custom Awaitables بهینهسازی شده.
4. بهبود ابزارهای Diagnostic و Debugging
دیباگ کردن کدهای ناهمزمان میتواند چالشبرانگیز باشد، به خصوص در مورد ردیابی استثنائات یا فهمیدن Stack Trace. مایکروسافت به طور مداوم در حال بهبود ابزارهای دیباگینگ در Visual Studio و .NET Diagnostic Tools است تا تجربه دیباگ کردن async
/await
را روانتر کند. این شامل بهبود نمایش Call Stack، تشخیص Deadlockها و ابزارهای پروفایلینگ است.
5. تطبیق با الگوهای مدرن در Concurrency
جامعه برنامهنویسی در حال بررسی الگوهای جدید Concurrency است که میتواند به مدل async
/await
اضافه شود. این موارد شامل Workflowsهای پیچیدهتر، Actors Model یا حتی الگوهای الهام گرفته از زبانهای دیگر (مانند Go channels). در حالی که async
/await
هسته اصلی باقی میماند، ممکن است انتزاعات سطح بالاتری بر روی آن ساخته شوند تا حل مشکلات پیچیدهتر Concurrency را سادهتر کنند.
نتیجهگیری
برنامهنویسی async
/await
در C# به ابزاری ضروری برای ساخت برنامههای مدرن و کارآمد تبدیل شده است. با درک عمیق از مکانیزمهای زیرین، بهترین شیوهها، و آگاهی از قابلیتهای پیشرفته مانند ValueTask
و IAsyncEnumerable
، توسعهدهندگان میتوانند از پتانسیل کامل .NET برای ایجاد سیستمهایی با پاسخگویی بالا و مقیاسپذیری عالی بهرهمند شوند.
مایکروسافت به سرمایهگذاری در این حوزه ادامه میدهد، و با هر نسخه جدید از C# و .NET، برنامهنویسی ناهمزمان سادهتر، کارآمدتر و قدرتمندتر میشود. تسلط بر async
/await
دیگر یک مزیت نیست، بلکه یک مهارت اساسی برای هر توسعهدهنده جدی C# است.
“تسلط به برنامهنویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”
"تسلط به برنامهنویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"
"با شرکت در این دوره جامع و کاربردی، به راحتی مهارتهای برنامهنویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر میسازد تا به سرعت الگوریتمهای پیچیده را درک کرده و اپلیکیشنهای هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفهای و امکان دانلود و تماشای آنلاین."
ویژگیهای کلیدی:
بدون نیاز به تجربه قبلی برنامهنویسی
زیرنویس فارسی با ترجمه حرفهای
۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان