پاسخ: async و await کلمات کلیدی در C# هستند که برای سادهسازی برنامهنویسی ناهمزمان معرفی شدهاند. آنها به شما اجازه میدهند کدی بنویسید که به نظر میرسد همزمان (Synchronous) است، اما در واقع به صورت ناهمزمان اجرا میشود و از بلاک شدن thread اصلی جلوگیری میکند.
چرا از Async/Await استفاده میکنیم؟
مثال عملی:
// بدون Async/Await (بلاک کننده) public string DownloadContentSync(string url) { using (var client = new HttpClient()) { // این خط UI thread را بلاک میکند string content = client.GetStringAsync(url).Result; return content; } } // با Async/Await (غیر بلاک کننده) public async Task<string> DownloadContentAsync(string url) { using (var client = new HttpClient()) { // thread آزاد میشود و به کار دیگری میپردازد string content = await client.GetStringAsync(url); return content; } } // نحوه فراخوانی در UI (مثلاً در یک دکمه) private async void Button_Click(object sender, EventArgs e) { // UI همچنان پاسخگو خواهد بود string data = await DownloadContentAsync("http://example.com"); MessageBox.Show(data); } // نحوه فراخوانی در Console (یا Backend) public static async Task Main(string[] args) { Console.WriteLine("Starting download..."); string data = await DownloadContentAsync("http://example.com"); Console.WriteLine("Download complete."); Console.WriteLine(data.Substring(0, 50)); }
مفاهیم کلیدی:
Task.Run()
و await
چیست؟ چه زمانی از هر کدام استفاده میکنیم؟پاسخ: هر دو Task.Run()
و await
در برنامهنویسی ناهمزمان نقش دارند، اما برای سناریوهای متفاوتی استفاده میشوند و تفاوتهای اساسی در نحوه مدیریت threadها و اجرای کد دارند.
await
:
await
روی یک Task
فراخوانی میشود، کنترل به فراخواننده بازگردانده میشود و thread فعلی آزاد میشود. زمانی که عملیات ناهمزمان کامل شد، ادامه کد پس از await
اجرا میشود. این کار بدون ایجاد thread جدید انجام میشود.HttpClient.GetStringAsync()
, Stream.ReadAsync()
, DbContext.SaveChangesAsync()
).مثال await
:
public async Task<string> DownloadDataAsync(string url)
{
Console.WriteLine($"Thread ID before await: {Thread.CurrentThread.ManagedThreadId}");
using (var client = new HttpClient())
{
// thread آزاد میشود
string data = await client.GetStringAsync(url);
Console.WriteLine($"Thread ID after await: {Thread.CurrentThread.ManagedThreadId}");
return data;
}
}
Task.Run()
:
Task.Run()
یک عملیات همزمان را در یک thread جدید از Thread Pool اجرا میکند. این کار از بلاک شدن thread اصلی (مثلاً UI thread) جلوگیری میکند.مثال Task.Run()
:
public async Task<long> CalculateFactorialAsync(int number) { Console.WriteLine($"Thread ID before Task.Run: {Thread.CurrentThread.ManagedThreadId}"); // عملیات CPU-bound را به Thread Pool میفرستد long result = await Task.Run(() => { Console.WriteLine($"Thread ID inside Task.Run: {Thread.CurrentThread.ManagedThreadId}"); long factorial = 1; for (int i = 1; i <= number; i++) { factorial *= i; } return factorial; }); Console.WriteLine($"Thread ID after Task.Run: {Thread.CurrentThread.ManagedThreadId}"); return result; }
جدول مقایسه:
ویژگی | await |
Task.Run() |
---|---|---|
نوع عملیات | I/O-bound | CPU-bound |
مدیریت Thread | thread را آزاد میکند | thread جدید از Thread Pool اشغال میکند |
هدف اصلی | پاسخگویی UI و مقیاسپذیری سرور | جلوگیری از بلاک شدن thread اصلی توسط محاسبات سنگین |
ایجاد Thread جدید | خیر | بله (از Thread Pool) |
پیچیدگی | سادهسازی کد ناهمزمان | اجرای کد همزمان در پسزمینه |
چه زمانی از کدام استفاده کنیم؟
await
: زمانی که عملیات شما شامل انتظار برای یک منبع خارجی (شبکه، دیسک، دیتابیس) است و متد ناهمزمان (با پسوند Async) برای آن وجود دارد.Task.Run()
: زمانی که عملیات شما یک محاسبه سنگین است که در حافظه یا CPU انجام میشود و هیچ متد ناهمزمان داخلی برای آن وجود ندارد. این کار از بلاک شدن thread اصلی جلوگیری میکند.نکته مهم: هرگز Task.Run()
را برای عملیاتهای I/O-bound استفاده نکنید، زیرا این کار باعث اشغال بیمورد یک thread از Thread Pool میشود در حالی که thread میتواند آزاد باشد. همیشه برای I/O-bound از متدهای Async موجود استفاده کنید.
Task
و Task<T>
در .NET چیست؟پاسخ: Task
و Task<T>
کلاسهایی در .NET هستند که عملیاتهای ناهمزمان را نشان میدهند. آنها بخشی از Task Parallel Library (TPL) هستند و سنگ بنای مدل برنامهنویسی async/await را تشکیل میدهند.
Task
:
void
در برنامهنویسی همزمان).Task
میتواند در یکی از وضعیتهای زیر باشد:
Created
: Task ایجاد شده اما هنوز شروع نشده است.WaitingForActivation
: منتظر فعال شدن.Running
: در حال اجرا.WaitingForChildrenToComplete
: منتظر تکمیل Taskهای فرزند.RanToCompletion
: با موفقیت کامل شده است.Canceled
: لغو شده است.Faulted
: با خطا مواجه شده است.public async Task PerformActionAsync() { Console.WriteLine("Performing action..."); await Task.Delay(1000); // شبیهسازی عملیات ناهمزمان Console.WriteLine("Action completed."); } // فراخوانی public async Task CallAction() { await PerformActionAsync(); Console.WriteLine("Action method finished."); }
Task<TResult>
:
TResult
را برمیگرداند (مشابه متد TResult
در برنامهنویسی همزمان).Result
قابل دسترسی است. دسترسی به Result
قبل از تکمیل Task
باعث بلاک شدن thread میشود، بنابراین باید با await
استفاده شود.public async Task<int> GetRandomNumberAsync() { Console.WriteLine("Generating random number..."); await Task.Delay(500); return new Random().Next(1, 100); } // فراخوانی public async Task CallGetNumber() { int number = await GetRandomNumberAsync(); Console.WriteLine($"Generated number: {number}"); }
ویژگیهای مشترک:
IsCompleted
: آیا Task کامل شده است (با موفقیت، خطا یا لغو).IsCanceled
: آیا Task لغو شده است.IsFaulted
: آیا Task با خطا مواجه شده است.Exception
: در صورت Faulted بودن Task، شامل اطلاعات خطا است.ContinueWith()
: برای تعریف یک Callback که پس از تکمیل Task اجرا میشود.WhenAll()
/ WhenAny()
: برای انتظار برای تکمیل چندین Task.چرا Task
به جای void
برای متدهای async
؟
async void
فقط برای Event Handlerها (مانند Button_Click
) توصیه میشوند.async Task
امکان await
کردن آنها را فراهم میکنند، که مدیریت خطا و جریان کنترل را بسیار سادهتر میکند.async void
مستقیماً به Synchronization Context ارسال میشوند و میتوانند برنامه را Crash کنند، در حالی که خطاهای async Task
در Task
کپسوله میشوند و میتوانند با try-catch
مدیریت شوند.نتیجهگیری: Task
و Task<T>
ابزارهای اساسی برای مدیریت عملیاتهای ناهمزمان در .NET هستند. درک صحیح آنها برای نوشتن کد async/await کارآمد و قابل نگهداری ضروری است.
پاسخ: Synchronization Context یک مفهوم کلیدی در .NET است که تعیین میکند ادامه یک متد async (کد پس از await) باید در کدام thread اجرا شود. این مفهوم به ویژه در برنامههای UI و ASP.NET (قبل از .NET Core) اهمیت دارد.
نقش Synchronization Context:
await
در یک thread دیگر اجرا شود، منجر به خطای InvalidOperationException
میشود.await
شده، ادامه کد در همان thread اصلی (UI thread) که قبل از await
بود، اجرا شود.نحوه کار:
async
فراخوانی میشود، SynchronizationContext.Current
در آن لحظه ذخیره میشود.await
به یک عملیات ناهمزمان میرسد، اگر عملیات هنوز کامل نشده باشد، کنترل به فراخواننده بازگردانده میشود و thread آزاد میشود.انواع Synchronization Context:
WindowsFormsSynchronizationContext
: برای برنامههای WinForms.DispatcherSynchronizationContext
: برای برنامههای WPF.AspNetSynchronizationContext
: برای ASP.NET (Full Framework).null
(پیشفرض): در برنامههای Console و ASP.NET Core، SynchronizationContext.Current
معمولاً null
است. در این حالت، ادامه کد پس از await
در هر thread موجود از Thread Pool اجرا میشود.مثال در WPF:
// WPF Window.xaml.cs
public partial class MainWindow : Window
{
private async void Button_Click(object sender, RoutedEventArgs e)
{
// این کد در UI thread اجرا میشود
MyButton.Content = "Downloading...";
// SynchronizationContext.Current در اینجا DispatcherSynchronizationContext است
string data = await DownloadDataAsync("http://example.com");
// این کد نیز در UI thread اجرا میشود (به دلیل Synchronization Context)
MyButton.Content = "Download Complete";
MyTextBlock.Text = data;
}
private async Task<string> DownloadDataAsync(string url)
{
using (var client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}
}
ConfigureAwait(false)
:
ConfigureAwait(false)
به await
میگوید که نیازی به بازگشت به Synchronization Context اصلی نیست و ادامه کد میتواند در هر thread موجود از Thread Pool اجرا شود.ConfigureAwait(false)
استفاده کنید تا از ایجاد Deadlock جلوگیری شود و عملکرد بهبود یابد. کتابخانهها نباید فرض کنند که فراخواننده دارای Synchronization Context خاصی است.await
دارید، نباید از ConfigureAwait(false)
استفاده کنید.ConfigureAwait(false)
نیست زیرا ASP.NET Core به طور پیشفرض Synchronization Context ندارد و await
به طور خودکار به Thread Pool بازمیگردد.مثال ConfigureAwait(false)
:
public async Task<string> DownloadDataAndProcessAsync(string url)
{
// این خط در UI thread (اگر وجود داشته باشد) اجرا میشود
Console.WriteLine($"Before await: {Thread.CurrentThread.ManagedThreadId}");
using (var client = new HttpClient())
{
// ادامه کد پس از این await میتواند در هر thread از Thread Pool اجرا شود
string data = await client.GetStringAsync(url).ConfigureAwait(false);
// این خط ممکن است در یک thread متفاوت از Thread Pool اجرا شود
Console.WriteLine($"After await: {Thread.CurrentThread.ManagedThreadId}");
// پردازش داده (CPU-bound)
string processedData = data.ToUpper();
return processedData;
}
}
// فراخوانی در UI (مثلاً در یک دکمه)
private async void Button_Click(object sender, RoutedEventArgs e)
{
MyButton.Content = "Processing...";
string result = await DownloadDataAndProcessAsync("http://example.com");
// این خط در UI thread اجرا میشود (چون ConfigureAwait(false) فقط برای ادامه متد DownloadDataAndProcessAsync اعمال شده بود)
MyButton.Content = "Done";
MyTextBlock.Text = result;
}
پاسخ: Deadlock در async/await زمانی رخ میدهد که یک متد async در حال انتظار برای تکمیل یک Task
باشد، در حالی که Task
برای بازگشت به Synchronization Context اصلی (که توسط متد async بلاک شده است) منتظر است. این یک سناریوی رایج در برنامههای UI است.
سناریوی Deadlock:
async
دارید که در UI thread فراخوانی میشود.DownloadDataAsync()
) await
میکند.await
، Synchronization Context فعلی (UI thread) ذخیره میشود..Result
یا .Wait()
) منتظر تکمیل همان Task
میشوید.Task
تکمیل میشود و سعی میکند ادامه کد را به Synchronization Context اصلی (UI thread) بازگرداند..Result
یا .Wait()
بلاک شده است و منتظر تکمیل Task
است.مثال Deadlock (در یک برنامه UI):
// در UI thread private void Button_Click(object sender, EventArgs e) { // این خط باعث Deadlock میشود! string data = DownloadDataAsync().Result; MessageBox.Show(data); } private async Task<string> DownloadDataAsync() { await Task.Delay(1000); // شبیهسازی عملیات ناهمزمان return "Data downloaded"; }
چگونه از Deadlock جلوگیری کنیم؟
await
استفاده کنید: اگر متد شما async
است، همیشه از await
برای انتظار برای Task
استفاده کنید. هرگز از .Result
یا .Wait()
در کدهای async استفاده نکنید، مگر اینکه مطمئن باشید در یک thread بدون Synchronization Context هستید (مانند متد Main
در برنامههای Console).ConfigureAwait(false)
: این مهمترین راه برای جلوگیری از Deadlock در کتابخانهها و کدهای غیر UI است. با استفاده از ConfigureAwait(false)
، شما به await
میگویید که نیازی به بازگشت به Synchronization Context اصلی نیست و میتواند در هر thread موجود از Thread Pool ادامه یابد. این کار باعث میشود که UI thread بلاک نشود و Deadlock رخ ندهد.مثال رفع Deadlock با ConfigureAwait(false)
:
// در UI thread private async void Button_Click(object sender, EventArgs e) { // این خط باعث Deadlock نمیشود string data = await DownloadDataAsync(); MessageBox.Show(data); } private async Task<string> DownloadDataAsync() { await Task.Delay(1000).ConfigureAwait(false); // مهم! return "Data downloaded"; }
قانون طلایی:
ConfigureAwait(false)
استفاده کنید.await
دارید، از ConfigureAwait(false)
استفاده نکنید.ConfigureAwait(false)
نیست زیرا به طور پیشفرض Synchronization Context وجود ندارد.async void
چیست و چرا باید از آن اجتناب کنیم؟پاسخ: async void
یک امضای متد ناهمزمان است که هیچ Task
یا Task<T>
را برنمیگرداند. این نوع متدها فقط برای Event Handlerها (مانند متدهای Button_Click
در UI) طراحی شدهاند و در سایر موارد باید از آنها اجتناب شود.
مشکلات async void
:
async void
پرتاب میشوند، مستقیماً به Synchronization Context ارسال میشوند. اگر Synchronization Context وجود نداشته باشد (مانند برنامههای Console یا ASP.NET Core)، استثنا به صورت یک UnhandledException
پرتاب میشود که میتواند برنامه را Crash کند.async Task
در خود Task
کپسوله میشوند و میتوانند توسط await
کننده با try-catch
مدیریت شوند.await
کردن:
async void
را await
کنید. این بدان معناست که فراخواننده نمیتواند بداند چه زمانی عملیات ناهمزمان کامل شده است، که مدیریت جریان کنترل را بسیار دشوار میکند.async void
را await
کنید، ممکن است کد پس از فراخوانی async void
قبل از تکمیل عملیات ناهمزمان اجرا شود، که میتواند منجر به رفتارهای غیرمنتظره شود.Synchronization Context
:
async void
همیشه سعی میکنند به Synchronization Context اصلی بازگردند. این میتواند در محیطهایی که Synchronization Context وجود ندارد (مانند ASP.NET Core) منجر به مشکلات پیشبینی نشده شود.مثال مشکلساز async void
:
public class MyService
{
public async void DoSomethingAndNotify()
{
await Task.Delay(1000);
throw new Exception("An error occurred!"); // این استثنا به صورت UnhandledException پرتاب میشود
}
}
// در متد Main یا یک متد همزمان دیگر
public static void Main(string[] args)
{
var service = new MyService();
service.DoSomethingAndNotify();
Console.WriteLine("This line might execute before the exception is thrown.");
Console.ReadKey();
}
چه زمانی از async void
استفاده کنیم؟
async void
، Event Handlerها هستند. در این سناریو، فریمورک UI (مانند WPF یا WinForms) انتظار دارد که Event Handler یک متد void
باشد و به طور خودکار استثناهای پرتاب شده را مدیریت میکند.مثال صحیح async void
(فقط در Event Handler):
private async void Button_Click(object sender, RoutedEventArgs e) { try { await DoLongRunningOperationAsync(); MessageBox.Show("Operation completed!"); } catch (Exception ex) { MessageBox.Show($"Error: {ex.Message}"); } }
نتیجهگیری: همیشه سعی کنید از async Task
یا async Task<T>
استفاده کنید و از async void
فقط در Event Handlerها اجتنابناپذیر است، استفاده کنید.
Task.Delay
و Thread.Sleep
چه تفاوتی دارند؟پاسخ: هر دو Task.Delay
و Thread.Sleep
برای ایجاد تأخیر در اجرای کد استفاده میشوند، اما تفاوت اساسی در نحوه مدیریت threadها دارند و برای سناریوهای کاملاً متفاوتی به کار میروند.
Thread.Sleep(millisecondsTimeout)
:
مثال Thread.Sleep
:
public void DoSomethingSync() { Console.WriteLine($"Start Sync: {DateTime.Now.ToLongTimeString()} - Thread ID: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(2000); // بلاک کردن thread فعلی برای 2 ثانیه Console.WriteLine($"End Sync: {DateTime.Now.ToLongTimeString()} - Thread ID: {Thread.CurrentThread.ManagedThreadId}"); }
Task.Delay(millisecondsDelay)
:
Task
را برمیگرداند که پس از مدت زمان مشخص شده کامل میشود. وقتی با await
استفاده میشود، thread فعلی آزاد میشود و میتواند کارهای دیگری را انجام دهد. پس از اتمام تأخیر، ادامه کد در یک thread از Thread Pool (یا Synchronization Context اصلی اگر وجود داشته باشد) اجرا میشود.مثال Task.Delay
:
public async Task DoSomethingAsync() { Console.WriteLine($"Start Async: {DateTime.Now.ToLongTimeString()} - Thread ID: {Thread.CurrentThread.ManagedThreadId}"); await Task.Delay(2000); // thread آزاد میشود Console.WriteLine($"End Async: {DateTime.Now.ToLongTimeString()} - Thread ID: {Thread.CurrentThread.ManagedThreadId}"); }
جدول مقایسه:
ویژگی | Thread.Sleep |
Task.Delay |
---|---|---|
نوع عملیات | همزمان (Synchronous) | ناهمزمان (Asynchronous) |
بلاک کردن Thread | بله، thread فعلی را بلاک میکند | خیر، thread فعلی را آزاد میکند |
بازگشت | void |
Task |
کاربرد اصلی | تست، دیباگینگ (در موارد نادر) | تأخیرهای غیر بلاککننده در برنامههای ناهمزمان |
پاسخگویی UI/سرور | کاهش پاسخگویی | حفظ پاسخگویی |
نتیجهگیری: در برنامهنویسی مدرن C# و .NET، تقریباً همیشه باید از Task.Delay
به جای Thread.Sleep
برای ایجاد تأخیر استفاده کنید، مگر اینکه به طور خاص نیاز به بلاک کردن یک thread داشته باشید (که معمولاً توصیه نمیشود).
Task.WhenAll
و Task.WhenAny
چه کاربردی دارند؟پاسخ: Task.WhenAll
و Task.WhenAny
متدهای استاتیک در کلاس Task
هستند که برای مدیریت چندین عملیات ناهمزمان به صورت همزمان (Concurrency) استفاده میشوند. آنها به شما امکان میدهند تا برای تکمیل گروهی از Task
ها منتظر بمانید.
Task.WhenAll(params Task[] tasks)
:
Task
های ارائه شده کامل شوند.Task
را برمیگرداند که زمانی کامل میشود که همه Task
های ورودی کامل شده باشند (چه با موفقیت، چه با خطا، چه با لغو).Task
های ورودی با خطا مواجه شود، Task.WhenAll
نیز با خطا مواجه میشود و تمام استثناهای Task
های خطا رفته را در یک AggregateException
جمعآوری میکند.مثال Task.WhenAll
:
public async Task DownloadMultipleFilesAsync() { var urls = new[] { "http://example.com/file1.txt", "http://example.com/file2.txt", "http://example.com/file3.txt" }; var downloadTasks = urls.Select(async url => { using (var client = new HttpClient()) { Console.WriteLine($"Downloading {url}..."); var content = await client.GetStringAsync(url); Console.WriteLine($"Finished {url}"); return content.Length; // یا هر پردازش دیگری } }).ToList(); try { // منتظر میماند تا همه دانلودها کامل شوند var results = await Task.WhenAll(downloadTasks); Console.WriteLine($"All downloads completed. Total characters: {results.Sum()}"); } catch (Exception ex) { Console.WriteLine($"An error occurred during downloads: {ex.Message}"); } }
Task.WhenAny(params Task[] tasks)
:
Task
از مجموعه ارائه شده کامل شود.Task<Task>
را برمیگرداند که زمانی کامل میشود که یکی از Task
های ورودی کامل شده باشد. Result
این Task<Task>
، همان Task
ای است که زودتر از بقیه کامل شده است.Task.WhenAny
خودش خطا نمیدهد، حتی اگر Task
ی که زودتر کامل شده، با خطا مواجه شده باشد. شما باید Task
برگشتی را بررسی کنید تا وضعیت آن را متوجه شوید.مثال Task.WhenAny
:
public async Task GetFirstAvailableDataAsync() { var task1 = Task.Delay(3000).ContinueWith(_ => "Data from Source 1"); var task2 = Task.Delay(1000).ContinueWith(_ => "Data from Source 2"); var task3 = Task.Delay(2000).ContinueWith(_ => "Data from Source 3"); // منتظر میماند تا اولین Task کامل شود var completedTask = await Task.WhenAny(task1, task2, task3); // بررسی نتیجه Task تکمیل شده if (completedTask == task1) { Console.WriteLine($"First data received from Source 1: {await task1}"); } else if (completedTask == task2) { Console.WriteLine($"First data received from Source 2: {await task2}"); } else if (completedTask == task3) { Console.WriteLine($"First data received from Source 3: {await task3}"); } }
جدول مقایسه:
ویژگی | Task.WhenAll |
Task.WhenAny |
---|---|---|
هدف | انتظار برای تکمیل تمام Taskها | انتظار برای تکمیل اولین Task |
بازگشت | Task (یا Task<TResult[]> ) |
Task<Task> (یا Task<Task<TResult>> ) |
مدیریت خطا | AggregateException در صورت خطای هر Task |
خطا را به خودی خود مدیریت نمیکند؛ باید Task برگشتی را بررسی کنید |
سناریو | عملیاتهای مستقل که همه باید کامل شوند | رقابت بین عملیاتها، پیادهسازی timeout |
IAsyncEnumerable<T>
و yield return async
چه کاربردی دارند؟پاسخ: IAsyncEnumerable<T>
و yield return async
(که به عنوان Async Streams شناخته میشوند) ویژگیهایی هستند که در C# 8.0 معرفی شدند و به شما امکان میدهند تا دادهها را به صورت ناهمزمان و جریانی (Stream) تولید و مصرف کنید. این قابلیت برای سناریوهایی که نیاز به پردازش دادهها به صورت تکه تکه و بدون بلاک کردن thread دارید، بسیار مفید است.
مشکل قبل از Async Streams:
IAsyncEnumerable<T>
:
IAsyncEnumerator<T> GetAsyncEnumerator()
است که به شما امکان میدهد به صورت ناهمزمان روی عناصر تکرار کنید.yield return async
(Async Iterators):
async
و yield return
را با هم ترکیب میکند تا یک متد iterator ناهمزمان ایجاد کند.yield return
اجرا میشود، یک عنصر تولید میشود و کنترل به فراخواننده بازگردانده میشود. متد iterator در جایی که متوقف شده بود، ادامه مییابد. این کار به صورت ناهمزمان و بدون بلاک کردن thread انجام میشود.مثال IAsyncEnumerable<T>
و yield return async
:
public async IAsyncEnumerable<int> GenerateNumbersAsync(int count) { for (int i = 0; i < count; i++) { await Task.Delay(100); // شبیهسازی عملیات ناهمزمان برای تولید هر عدد yield return i; } } // نحوه مصرف public async Task ConsumeNumbersAsync() { await foreach (var number in GenerateNumbersAsync(10)) { Console.WriteLine($"Received: {number}"); } }
مزایای Async Streams:
کاربردها:
ValueTask
و ValueTask<T>
چه تفاوتی با Task
و Task<T>
دارند؟پاسخ: ValueTask
و ValueTask<T>
ساختارهایی (structs) هستند که در .NET Core معرفی شدند تا بهینهسازیهایی را برای سناریوهای خاصی که Task
و Task<T>
ممکن است سربار (overhead) غیرضروری ایجاد کنند، فراهم کنند. تفاوت اصلی در این است که Task
یک کلاس (reference type) است، در حالی که ValueTask
یک ساختار (value type) است.
Task
و Task<T>
:
Task
یا Task<T>
برگردانده میشود، یک شیء جدید روی heap تخصیص داده میشود. این سربار در سناریوهایی که عملیات ناهمزمان اغلب به صورت همزمان تکمیل میشود (مثلاً از یک کش)، میتواند قابل توجه باشد.WhenAll
، ContinueWith
) دارند.ValueTask
و ValueTask<T>
:
ValueTask
مستقیماً نتیجه را در خود نگهداری میکند و نیازی به تخصیص شیء روی heap نیست.ValueTask
یک Task
(یا IValueTaskSource
) را در خود کپسوله میکند و سربار تخصیص حافظه مشابه Task
خواهد بود.async
که انتظار میرود در اکثر مواقع به صورت همزمان تکمیل شوند (مثلاً خواندن از یک کش در حافظه).جدول مقایسه:
ویژگی | Task / Task<T> |
ValueTask / ValueTask<T> |
---|---|---|
نوع | کلاس (Reference Type) | ساختار (Value Type) |
تخصیص حافظه (Heap Allocation) | همیشه (حتی اگر عملیات همزمان تکمیل شود) | فقط زمانی که عملیات ناهمزمان تکمیل شود |
عملکرد | خوب برای اکثر سناریوها | بهینهتر برای سناریوهای تکمیل همزمان مکرر |
قابلیتها | کاملتر (پشتیبانی از WhenAll ، ContinueWith و غیره) |
محدودتر (برای قابلیتهای پیشرفتهتر نیاز به تبدیل به Task دارد) |
کاربرد اصلی | عملیاتهای ناهمزمان عمومی | عملیاتهای ناهمزمان با احتمال بالای تکمیل همزمان |
چه زمانی از ValueTask
استفاده کنیم؟
Task
در یک مسیر داغ (Hot Path) باعث ایجاد سربار عملکردی قابل توجهی میشوند.async
شما اغلب به صورت همزمان تکمیل میشود (مثلاً از یک کش).نکته مهم: استفاده نادرست از ValueTask
میتواند منجر به مشکلات پیچیدهای شود، به خصوص اگر چندین بار await
شود یا به صورت همزمان await
شود. در اکثر موارد، Task
و Task<T>
انتخاب صحیح هستند.
async
و await
چگونه در پشت صحنه کار میکنند؟ (StateMachine)پاسخ: زمانی که شما یک متد را با کلمات کلیدی async
و await
علامتگذاری میکنید، کامپایلر C# کد شما را به یک StateMachine (ماشین حالت) پیچیده تبدیل میکند. این StateMachine مسئول مدیریت جریان کنترل، ذخیره وضعیت متد، و از سرگیری اجرا پس از تکمیل عملیات ناهمزمان است.
مراحل کلی:
async
شما را به یک کلاس StateMachine تبدیل میکند. این کلاس شامل فیلدهایی برای نگهداری متغیرهای محلی و پارامترهای متد است که باید بین نقاط await
حفظ شوند.async
اصلی به یک متد همزمان تبدیل میشود که یک نمونه از این StateMachine را ایجاد و شروع میکند.await
:
await
در متد async
به یک نقطه توقف (suspension point) تبدیل میشود.async
بازگردانده میشود و thread آزاد میشود.async
را از نقطه توقف (پس از await
) از سر میگیرد.شبه کد (Pseudo-code) از تبدیل:
// کد اصلی async public async Task<int> GetValueAsync() { Console.WriteLine("Before await"); int result = await SomeAsyncOperation(); Console.WriteLine("After await"); return result; } // تبدیل شده توسط کامپایلر به StateMachine public sealed class <GetValueAsync>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder<int> <>t__builder; public int <result>5__1; private TaskAwaiter<int> <>u__1; void IAsyncStateMachine.MoveNext() { int num = <>1__state; try { TaskAwaiter<int> awaiter; if (num != 0) { Console.WriteLine("Before await"); awaiter = SomeAsyncOperation().GetAwaiter(); if (!awaiter.IsCompleted) { <>1__state = 0; <>u__1 = awaiter; <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); return; } } else { awaiter = <>u__1; <>u__1 = default(TaskAwaiter<int>); <>1__state = -1; } <result>5__1 = awaiter.GetResult(); Console.WriteLine("After await"); } catch (Exception exception) { <>1__state = -2; <>t__builder.SetException(exception); return; } <>1__state = -2; <>t__builder.SetResult(<result>5__1); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { <>t__builder.SetStateMachine(stateMachine); } }
مزایای StateMachine:
نتیجهگیری: StateMachine یک الگوی قدرتمند است که در پشت صحنه async/await
کار میکند و پیچیدگیهای برنامهنویسی ناهمزمان را از دید توسعهدهنده پنهان میکند. درک این مکانیسم به شما کمک میکند تا کدهای async/await بهتری بنویسید و مشکلات احتمالی (مانند Deadlock) را تشخیص دهید.
async
و await
در ASP.NET Core چگونه کار میکنند؟پاسخ: استفاده از async
و await
در ASP.NET Core برای بهبود مقیاسپذیری و پاسخگویی برنامههای وب بسیار حیاتی است. برخلاف نسخههای قدیمیتر ASP.NET (Full Framework)، ASP.NET Core به طور پیشفرض از Synchronization Context استفاده نمیکند، که این امر مدیریت async/await را سادهتر و کارآمدتر میکند.
نحوه کار در ASP.NET Core:
await
فراخوانی کند:async
را از سر میگیرد.await
نیست. این بدان معناست که شما معمولاً نیازی به استفاده از .ConfigureAwait(false)
در کدهای ASP.NET Core ندارید، مگر اینکه در حال فراخوانی کد کتابخانهای باشید که ممکن است Deadlock ایجاد کند.مزایای استفاده از async/await
در ASP.NET Core
:
مثال در ASP.NET Core Controller:
public class ProductsController : ControllerBase { private readonly IProductService _productService; public ProductsController(IProductService productService) { _productService = productService; } [HttpGet] public async Task<ActionResult<IEnumerable<Product>>> GetProducts() { // thread آزاد میشود در حین انتظار برای دیتابیس var products = await _productService.GetAllProductsAsync(); return Ok(products); } [HttpGet("{id}")] public async Task<ActionResult<Product>> GetProduct(int id) { var product = await _productService.GetProductByIdAsync(id); if (product == null) { return NotFound(); } return Ok(product); } }
نکات مهم:
async
را تا پایینترین سطح پشته فراخوانی (Call Stack) گسترش دهید (Async All The Way). به این معنی که اگر یک متد async
را فراخوانی میکنید، متد فراخواننده نیز باید async
باشد و از await
استفاده کند..Result
یا .Wait()
در ASP.NET Core اجتناب کنید، زیرا میتوانند منجر به Deadlock یا کاهش عملکرد شوند.Task.Run()
استفاده کنید تا thread اصلی را بلاک نکنید.async
و await
در Entity Framework Core چگونه کار میکنند؟پاسخ: async
و await
در Entity Framework Core (EF Core) برای اجرای عملیاتهای دیتابیس به صورت ناهمزمان استفاده میشوند. این قابلیت به طور قابل توجهی مقیاسپذیری و پاسخگویی برنامههایی را که با دیتابیس تعامل دارند، بهبود میبخشد، به خصوص در برنامههای وب و سرویسها.
چرا از async/await
در EF Core
استفاده کنیم؟
async
در EF Core، thread سرور در حین انتظار برای پاسخ دیتابیس آزاد میشود و میتواند درخواستهای دیگر را پردازش کند. این امر به طور چشمگیری Throughput برنامه را افزایش میدهد.async
در EF Core این مشکل را حل میکنند.متدهای async
رایج در EF Core
:
ToListAsync()
FirstOrDefaultAsync()
SingleOrDefaultAsync()
CountAsync()
AnyAsync()
LoadAsync()
SaveChangesAsync()
مثال استفاده از async/await
در EF Core
:
public class ProductRepository : IProductRepository { private readonly ApplicationDbContext _context; public ProductRepository(ApplicationDbContext context) { _context = context; } public async Task<IEnumerable<Product>> GetAllProductsAsync() { // thread آزاد میشود در حین انتظار برای دیتابیس return await _context.Products.ToListAsync(); } public async Task<Product> GetProductByIdAsync(int id) { return await _context.Products.FirstOrDefaultAsync(p => p.Id == id); } public async Task AddProductAsync(Product product) { _context.Products.Add(product); await _context.SaveChangesAsync(); // thread آزاد میشود در حین انتظار برای دیتابیس } public async Task UpdateProductAsync(Product product) { _context.Entry(product).State = EntityState.Modified; await _context.SaveChangesAsync(); } public async Task DeleteProductAsync(int id) { var product = await _context.Products.FindAsync(id); if (product != null) { _context.Products.Remove(product); await _context.SaveChangesAsync(); } } }
نکات مهم:
async
در EF Core استفاده کنید، مگر اینکه دلیل خاصی برای استفاده از متدهای همزمان داشته باشید (که نادر است)..Result
یا .Wait()
روی Task
های برگشتی از EF Core اجتناب کنید، زیرا میتوانند منجر به Deadlock شوند.EF Core
به طور خودکار ConfigureAwait(false)
را در پشت صحنه برای متدهای async
خود استفاده میکند، بنابراین نیازی نیست شما به صورت دستی آن را اضافه کنید.Cancellation Token
چیست و چگونه در async/await استفاده میشود؟پاسخ: Cancellation Token
یک مکانیسم استاندارد در .NET برای هماهنگی لغو عملیاتهای ناهمزمان است. این به شما امکان میدهد تا به یک عملیات در حال اجرا سیگنال دهید که باید متوقف شود، بدون اینکه به طور ناگهانی آن را قطع کنید.
چرا به Cancellation Token
نیاز داریم؟
نحوه استفاده:
CancellationTokenSource
: این کلاس برای ایجاد و مدیریت CancellationToken
استفاده میشود. شما یک نمونه از آن را ایجاد میکنید و سپس Token
آن را به متدهای ناهمزمان خود پاس میدهید.CancellationToken
: این یک ساختار (struct) است که به متدهای ناهمزمان پاس داده میشود. متدها میتوانند وضعیت این Token
را بررسی کنند تا ببینند آیا درخواست لغو ارسال شده است یا خیر.ThrowIfCancellationRequested()
: این متد روی CancellationToken
فراخوانی میشود و اگر درخواست لغو ارسال شده باشد، یک OperationCanceledException
پرتاب میکند.Register()
: برای ثبت یک Callback که هنگام لغو Token
اجرا میشود.Cancel()
: این متد روی CancellationTokenSource
فراخوانی میشود تا درخواست لغو را به تمام Token
های مرتبط ارسال کند.مثال استفاده از CancellationToken
:
public async Task DoLongRunningOperationAsync(CancellationToken cancellationToken) { for (int i = 0; i < 10; i++) { // هر بار قبل از انجام کار، وضعیت لغو را بررسی میکنیم cancellationToken.ThrowIfCancellationRequested(); Console.WriteLine($"Processing step {i + 1}..."); await Task.Delay(500, cancellationToken); // Task.Delay هم CancellationToken میگیرد } Console.WriteLine("Operation completed."); } public async Task RunOperationWithCancellation() { using (var cts = new CancellationTokenSource()) { var operationTask = DoLongRunningOperationAsync(cts.Token); // بعد از 2 ثانیه، درخواست لغو میدهیم await Task.Delay(2000); cts.Cancel(); try { await operationTask; } catch (OperationCanceledException) { Console.WriteLine("Operation was cancelled!"); } catch (Exception ex) { Console.WriteLine($"An error occurred: {ex.Message}"); } } }
نکات مهم:
CancellationToken
یک مکانیسم مشارکتی (Cooperative) است. یعنی عملیات باید به صورت فعالانه وضعیت Token
را بررسی کند و به درخواست لغو پاسخ دهد.HttpClient.GetAsync
, Stream.ReadAsync
, Task.Delay
) یک CancellationToken
را به عنوان پارامتر میپذیرند.CancellationTokenSource
را در یک بلوک using
قرار دهید تا منابع آن به درستی آزاد شوند.async
و await
در ASP.NET Web Forms چگونه کار میکنند؟پاسخ: استفاده از async
و await
در ASP.NET Web Forms (که یک فریمورک قدیمیتر است) کمی پیچیدهتر از ASP.NET Core است، زیرا Web Forms به شدت به Synchronization Context متکی است. با این حال، استفاده از async/await
میتواند به بهبود مقیاسپذیری برنامههای Web Forms کمک کند، به خصوص برای عملیاتهای I/O-bound.
نحوه کار در ASP.NET Web Forms:
AspNetSynchronizationContext
است که تضمین میکند ادامه کد پس از await
به thread اصلی درخواست (Request Thread) بازگردانده شود. این برای حفظ HttpContext و سایر اطلاعات مربوط به درخواست ضروری است.async
را در چرخه حیات صفحه (Page Lifecycle) ثبت کنید. این کار معمولاً با استفاده از Page.RegisterAsyncTask
یا با علامتگذاری متد Page_Load
یا Button_Click
به عنوان async void
انجام میشود.await
فراخوانی میشود، thread فعلی درخواست آزاد میشود و به Thread Pool بازگردانده میشود.AspNetSynchronizationContext
تضمین میکند که ادامه کد به همان thread اصلی درخواست (یا یک thread دیگر از Thread Pool که به آن Context متصل شده است) بازگردانده شود.مثال در ASP.NET Web Forms:
// Default.aspx.cs public partial class _Default : Page { protected void Page_Load(object sender, EventArgs e) { // ثبت یک عملیات ناهمزمان در چرخه حیات صفحه RegisterAsyncTask(new PageAsyncTask(LoadDataAsync)); } private async Task LoadDataAsync() { // شبیهسازی عملیات طولانی دیتابیس یا API string data = await GetDataFromServiceAsync(); lblData.Text = data; } private async Task<string> GetDataFromServiceAsync() { using (var client = new HttpClient()) { return await client.GetStringAsync("http://example.com/api/data"); } } protected async void btnSubmit_Click(object sender, EventArgs e) { // این متد async void است و برای Event Handlerها مناسب است string result = await ProcessFormAsync(); lblResult.Text = result; } private async Task<string> ProcessFormAsync() { await Task.Delay(1500); // شبیهسازی پردازش فرم return "Form processed successfully!"; } }
نکات مهم و مشکلات احتمالی:
.Result
یا .Wait()
روی Task
ها استفاده کنید، به راحتی میتوانید با Deadlock مواجه شوید.ConfigureAwait(false)
استفاده کنید تا از Deadlock جلوگیری شود. اما در کدهای UI یا کدهای مربوط به صفحه، نباید از آن استفاده کنید، زیرا نیاز به بازگشت به Synchronization Context برای بهروزرسانی UI یا دسترسی به HttpContext دارید.async
کنید.async
و await
در WPF و WinForms چگونه کار میکنند؟پاسخ: در برنامههای دسکتاپ WPF و WinForms، async
و await
برای حفظ پاسخگویی رابط کاربری (UI Responsiveness) در حین انجام عملیاتهای طولانی مدت استفاده میشوند. این فریمورکها به شدت به یک UI thread واحد متکی هستند که مسئول بهروزرسانی UI و پردازش ورودیهای کاربر است.
نحوه کار در WPF/WinForms:
DispatcherSynchronizationContext
و WinForms از WindowsFormsSynchronizationContext
استفاده میکنند. این Contextها تضمین میکنند که ادامه کد پس از await
به UI thread بازگردانده شود.async
در UI thread فراخوانی میشود و به یک عملیات ناهمزمان (مانند فراخوانی API، دسترسی به دیتابیس، یا Task.Delay
) await
میکند:await
به UI thread “پست” (Post) شود. این کار به شما امکان میدهد تا بدون نگرانی از خطاهای cross-thread، UI را بهروزرسانی کنید.مثال در WPF:
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
private async void Button_Click(object sender, RoutedEventArgs e)
{
// این کد در UI thread اجرا میشود
MyButton.Content = "Downloading...";
// UI thread آزاد میشود
string data = await DownloadDataAsync("http://example.com");
// این کد به UI thread بازمیگردد و UI را بهروزرسانی میکند
MyButton.Content = "Download Complete";
MyTextBlock.Text = data;
}
private async Task<string> DownloadDataAsync(string url)
{
using (var client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}
}
نکات مهم:
Button_Click
) معمولاً async void
هستند. این تنها مورد مجاز برای استفاده از async void
است..Result
یا .Wait()
روی Task
ها در UI thread استفاده کنید. همیشه از await
استفاده کنید.ConfigureAwait(false)
استفاده کنید تا از Deadlock جلوگیری شود و عملکرد بهبود یابد. اما در کدهای UI که نیاز به بهروزرسانی UI پس از await
دارید، نباید از آن استفاده کنید.Task.Run()
استفاده کنید.async
و await
در Console Application چگونه کار میکنند؟پاسخ: استفاده از async
و await
در Console Applicationها برای اجرای عملیاتهای ناهمزمان (مانند فراخوانی API، دسترسی به فایل) بدون بلاک کردن thread اصلی برنامه استفاده میشود. تفاوت اصلی در Console Applicationها این است که به طور پیشفرض Synchronization Context خاصی وجود ندارد.
نحوه کار در Console Application:
SynchronizationContext.Current
معمولاً null
است. این بدان معناست که پس از await
، ادامه کد در هر thread موجود از Thread Pool اجرا میشود.Main
را به صورت async Task Main()
یا async Task<int> Main()
تعریف کنید. این کار به شما امکان میدهد تا await
را مستقیماً در متد Main
استفاده کنید.await
فراخوانی میشود، thread اصلی برنامه آزاد میشود و میتواند کارهای دیگری را انجام دهد (اگرچه در یک برنامه Console ساده، معمولاً کار دیگری برای انجام وجود ندارد).مثال در Console Application:
public class Program { public static async Task Main(string[] args) { Console.WriteLine($"Main thread ID: {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("Starting long running operation..."); string result = await GetLongRunningResultAsync(); Console.WriteLine($"Operation completed. Result: {result}"); Console.WriteLine($"Current thread ID: {Thread.CurrentThread.ManagedThreadId}"); Console.ReadKey(); // برای نگه داشتن کنسول باز } public static async Task<string> GetLongRunningResultAsync() { Console.WriteLine($"Inside GetLongRunningResultAsync - Thread ID before delay: {Thread.CurrentThread.ManagedThreadId}"); await Task.Delay(3000); // شبیهسازی عملیات طولانی Console.WriteLine($"Inside GetLongRunningResultAsync - Thread ID after delay: {Thread.CurrentThread.ManagedThreadId}"); return "Data from async operation"; } }
نکات مهم:
ConfigureAwait(false)
معمولاً ضروری نیست، زیرا Synchronization Context وجود ندارد و await
به طور پیشفرض به Thread Pool بازمیگردد. با این حال، استفاده از آن ضرری ندارد و میتواند عادت خوبی برای کدهای کتابخانهای باشد.Task.Run()
استفاده کنید تا thread اصلی برنامه را بلاک نکنید.Main
ناهمزمان، نیازی به استفاده از .Result
یا .Wait()
نیست. اگر متد Main
شما همزمان است و نیاز به فراخوانی یک متد async
دارید، میتوانید از .Result
یا .Wait()
استفاده کنید، اما این کار میتواند منجر به Deadlock شود اگر متد async
داخلی از ConfigureAwait(false)
استفاده نکند و Synchronization Context وجود داشته باشد (که در Console معمولاً نیست).async
و await
در Unit Testing چگونه کار میکنند؟پاسخ: تست کردن کدهای ناهمزمان با async
و await
نیازمند رویکرد صحیح است تا اطمینان حاصل شود که تستها به درستی اجرا میشوند و نتایج قابل اعتمادی ارائه میدهند. فریمورکهای تست مدرن (مانند xUnit, NUnit, MSTest) از متدهای تست async
پشتیبانی میکنند.
نحوه تست کدهای async
:
async Task
:
void
، متد تست خود را به عنوان async Task
علامتگذاری کنید. این به فریمورک تست میگوید که این یک تست ناهمزمان است و باید منتظر تکمیل Task
برگشتی باشد.await
کردن عملیات ناهمزمان:
await
فراخوانی کنید. این تضمین میکند که تست تا زمانی که عملیات ناهمزمان کامل شود، منتظر میماند.async Task
پرتاب میشوند، به درستی توسط فریمورک تست مدیریت میشوند. میتوانید از Assert.ThrowsAsync<T>
(در xUnit) یا مشابه آن برای تست پرتاب استثناهای ناهمزمان استفاده کنید.مثال تست ناهمزمان با xUnit:
public class MyServiceTests { [Fact] public async Task GetDataAsync_ReturnsExpectedData() { // Arrange var service = new MyService(); // Act string result = await service.GetDataAsync(); // Assert Assert.Equal("Some Data", result); } [Fact] public async Task GetDataAsync_ThrowsExceptionOnFailure() { // Arrange var service = new MyService(shouldThrow: true); // Act & Assert await Assert.ThrowsAsync<InvalidOperationException>(() => service.GetDataAsync()); } } public class MyService { private readonly bool _shouldThrow; public MyService(bool shouldThrow = false) { _shouldThrow = shouldThrow; } public async Task<string> GetDataAsync() { await Task.Delay(100); // شبیهسازی عملیات ناهمزمان if (_shouldThrow) { throw new InvalidOperationException("Failed to get data."); } return "Some Data"; } }
نکات مهم:
async void
: هرگز متدهای تست را async void
نکنید. این کار باعث میشود فریمورک تست نتواند منتظر تکمیل عملیات ناهمزمان بماند و تست ممکن است قبل از اتمام عملیات به پایان برسد و نتایج نادرست بدهد..Result
یا .Wait()
: از بلاک کردن thread تست با استفاده از .Result
یا .Wait()
خودداری کنید، زیرا این کار میتواند منجر به Deadlock شود. همیشه از await
استفاده کنید.SynchronizationContext.SetSynchronizationContext
چه کاربردی دارد؟پاسخ: متد استاتیک SynchronizationContext.SetSynchronizationContext(SynchronizationContext syncContext)
برای تنظیم SynchronizationContext
فعلی برای thread جاری استفاده میشود. این متد به ندرت در کدهای برنامههای کاربردی معمولی استفاده میشود و عمدتاً توسط فریمورکها (مانند WPF, WinForms, ASP.NET) در پشت صحنه برای تنظیم Context مناسب استفاده میشود.
کاربردها (عمدتاً داخلی فریمورک):
DispatcherSynchronizationContext
یا WindowsFormsSynchronizationContext
را برای UI thread تنظیم میکند. این کار تضمین میکند که هر await
در UI thread، ادامه کد را به همان UI thread بازگرداند.AspNetSynchronizationContext
برای هر درخواست تنظیم میشود تا اطمینان حاصل شود که HttpContext و سایر اطلاعات درخواست در دسترس باقی میمانند.SynchronizationContext
سفارشی برای شبیهسازی رفتار یک محیط خاص (مانند UI) تنظیم شود. این کار به تست کدهای ناهمزمان کمک میکند که به Synchronization Context وابسته هستند.مثال (فقط برای درک، استفاده در کد واقعی توصیه نمیشود):
public class CustomSynchronizationContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { // در اینجا میتوانید منطق سفارشی برای اجرای Callback را پیادهسازی کنید Console.WriteLine($"Custom Context: Posting callback on thread {Thread.CurrentThread.ManagedThreadId}"); ThreadPool.QueueUserWorkItem(_ => d(state)); } public override void Send(SendOrPostCallback d, object state) { // در اینجا میتوانید منطق سفارشی برای اجرای Callback را پیادهسازی کنید Console.WriteLine($"Custom Context: Sending callback on thread {Thread.CurrentThread.ManagedThreadId}"); d(state); } } public class Program { public static async Task Main(string[] args) { // تنظیم یک SynchronizationContext سفارشی برای thread اصلی SynchronizationContext.SetSynchronizationContext(new CustomSynchronizationContext()); Console.WriteLine($"Main thread ID before async call: {Thread.CurrentThread.ManagedThreadId}"); await DoSomethingAsync(); Console.WriteLine($"Main thread ID after async call: {Thread.CurrentThread.ManagedThreadId}"); Console.ReadKey(); } public static async Task DoSomethingAsync() { Console.WriteLine($"Inside DoSomethingAsync - Thread ID before await: {Thread.CurrentThread.ManagedThreadId}"); await Task.Delay(1000); Console.WriteLine($"Inside DoSomethingAsync - Thread ID after await: {Thread.CurrentThread.ManagedThreadId}"); } }
نکات مهم:
SynchronizationContext.SetSynchronizationContext
یک متد بسیار قدرتمند است و استفاده نادرست از آن میتواند منجر به رفتارهای غیرمنتظره و مشکلات پیچیده در برنامههای ناهمزمان شود.ExecutionContext
و LogicalCallContext
چه نقشی در async/await دارند؟پاسخ: ExecutionContext
و LogicalCallContext
(که بخشی از ExecutionContext
است) مفاهیم پیشرفتهتری در .NET هستند که به مدیریت و انتقال اطلاعات مربوط به Context در طول عملیاتهای ناهمزمان کمک میکنند. آنها تضمین میکنند که اطلاعات مهمی مانند هویت کاربر، تنظیمات محلی (Culture)، و تراکنشها در طول فراخوانیهای ناهمزمان حفظ شوند.
ExecutionContext
:
SynchronizationContext
(اگر وجود داشته باشد)SecurityContext
(اطلاعات امنیتی)LogicalCallContext
(اطلاعات مربوط به فراخوانی منطقی)await
آغاز میشود، ExecutionContext
فعلی
ذخیره میشود. پس از تکمیل عملیات ناهمزمان، ExecutionContext
ذخیره شده بازیابی میشود و ادامه کد در آن Context اجرا میشود. این تضمین میکند که اطلاعات مهم Context در طول عملیات ناهمزمان از بین نرود.LogicalCallContext
:
LogicalCallContext
قرار میگیرند، به صورت خودکار در طول عملیاتهای ناهمزمان (مانند await
) و بین threadها منتقل میشوند.مثال استفاده از LogicalCallContext
(از طریق AsyncLocal<T>
):
// در .NET Core و نسخههای جدیدتر، AsyncLocal<T> جایگزین LogicalCallContext شده است public class Program { private static readonly AsyncLocal<string> _correlationId = new AsyncLocal<string>(); public static async Task Main(string[] args) { _correlationId.Value = Guid.NewGuid().ToString(); Console.WriteLine($"Main thread: Correlation ID = {_correlationId.Value}"); await ProcessRequestAsync(); Console.WriteLine($"Main thread after async call: Correlation ID = {_correlationId.Value}"); Console.ReadKey(); } public static async Task ProcessRequestAsync() { Console.WriteLine($"ProcessRequestAsync: Correlation ID = {_correlationId.Value}"); await Task.Delay(1000); Console.WriteLine($"ProcessRequestAsync after delay: Correlation ID = {_correlationId.Value}"); } }
نکات مهم:
ExecutionContext
و LogicalCallContext
معمولاً به صورت خودکار توسط .NET مدیریت میشوند و شما نیازی به تعامل مستقیم با آنها ندارید.AsyncLocal<T>
به عنوان جایگزین مدرن و کارآمدتر برای LogicalCallContext
معرفی شده است.async
و await
در LINQ چگونه کار میکنند؟پاسخ: استفاده از async
و await
در LINQ به طور مستقیم توسط LINQ to Objects پشتیبانی نمیشود. با این حال، میتوانید از async/await
در داخل عبارات LINQ استفاده کنید، به خصوص با استفاده از Task.WhenAll
برای اجرای همزمان عملیاتهای ناهمزمان.
سناریوهای رایج:
Select
برای ایجاد یک مجموعه از Task
ها استفاده کنید و سپس با Task.WhenAll
منتظر تکمیل همه آنها بمانید.Where
استفاده کنید.IAsyncEnumerable<T>
:
IAsyncEnumerable<T>
در C# 8.0، میتوانید از LINQ به صورت ناهمزمان با استفاده از await foreach
و متدهای LINQ ناهمزمان (مانند WhereAwait
, SelectAwait
) استفاده کنید. این قابلیت در کتابخانههایی مانند System.Linq.Async
پیادهسازی شده است.مثال اجرای همزمان با Task.WhenAll
:
public async Task ProcessUrlsAsync(IEnumerable<string> urls) { var downloadTasks = urls.Select(async url => { using (var client = new HttpClient()) { return await client.GetStringAsync(url); } }); var results = await Task.WhenAll(downloadTasks); foreach (var result in results) { Console.WriteLine(result.Substring(0, 50)); } }
مثال فیلتر کردن ناهمزمان:
public async Task<IEnumerable<string>> FilterValidUrlsAsync(IEnumerable<string> urls) { var validUrls = new List<string>(); foreach (var url in urls) { if (await IsUrlValidAsync(url)) { validUrls.Add(url); } } return validUrls; } private async Task<bool> IsUrlValidAsync(string url) { try { using (var client = new HttpClient()) { var response = await client.GetAsync(url); return response.IsSuccessStatusCode; } } catch { return false; } }
نکات مهم:
LINQ to Objects
به طور ذاتی ناهمزمان نیست. استفاده از async
در داخل Select
یا Where
میتواند منجر به رفتارهای غیرمنتظره شود اگر به درستی مدیریت نشود.System.Linq.Async
توصیه میشود که متدهای LINQ را برای IAsyncEnumerable<T>
پیادهسازی میکنند.async
و await
در Constructorها چگونه کار میکنند؟پاسخ: شما نمیتوانید یک Constructor را به عنوان async
علامتگذاری کنید. این یک محدودیت در C# است، زیرا Constructorها باید به صورت همزمان اجرا شوند و یک شیء را برگردانند.
چرا نمیتوان Constructor
را async
کرد؟
async
باید Task
یا Task<T>
برگردانند.await
در Constructor میتواند منجر به این شود که شیء قبل از تکمیل مقداردهی اولیه، در دسترس قرار گیرد، که میتواند منجر به رفتارهای غیرمنتظره شود.راه حلها:
async
ایجاد میکنید که مسئول ایجاد و مقداردهی اولیه ناهمزمان شیء است.InitializeAsync
: شما یک متد async
جداگانه برای مقداردهی اولیه ایجاد میکنید که باید پس از ایجاد شیء فراخوانی شود. این رویکرد کمتر امن است، زیرا ممکن است فراموش کنید متد InitializeAsync
را فراخوانی کنید.مثال Factory Method Pattern:
public class MyService { private MyService() { } public static async Task<MyService> CreateAsync() { var service = new MyService(); await service.InitializeAsync(); return service; } private async Task InitializeAsync() { // عملیات مقداردهی اولیه ناهمزمان await Task.Delay(1000); } } // نحوه استفاده public async Task UseService() { var service = await MyService.CreateAsync(); // ... }
مثال متد InitializeAsync
:
public class MyService { public async Task InitializeAsync() { // عملیات مقداردهی اولیه ناهمزمان await Task.Delay(1000); } } // نحوه استفاده public async Task UseService() { var service = new MyService(); await service.InitializeAsync(); // ... }
async
و await
در Property Getters/Setters چگونه کار میکنند؟پاسخ: شما نمیتوانید Property Getters/Setters را به عنوان async
علامتگذاری کنید. این یک محدودیت دیگر در C# است.
چرا نمیتوان Property
را async
کرد؟
Task
.راه حلها:
Get/Set
ناهمزمان: به جای Property، متدهای async
جداگانه برای دریافت و تنظیم مقدار ایجاد کنید.AsyncLazy<T>
استفاده کنید.مثال متدهای Get/Set
ناهمزمان:
public class MyClass { private string _myValue; public async Task<string> GetMyValueAsync() { if (_myValue == null) { _myValue = await LoadValueFromDatabaseAsync(); } return _myValue; } public async Task SetMyValueAsync(string value) { await SaveValueToDatabaseAsync(value); _myValue = value; } private async Task<string> LoadValueFromDatabaseAsync() { await Task.Delay(500); return "Some Value"; } private async Task SaveValueToDatabaseAsync(string value) { await Task.Delay(500); } }
async
و await
در Locking چگونه کار میکنند؟پاسخ: شما نمیتوانید از await
در داخل یک بلوک lock
استفاده کنید. این یک محدودیت در C# است، زیرا lock
یک مکانیسم همزمان است که thread را بلاک میکند.
چرا نمیتوان await
را در lock
استفاده کرد؟
lock
تضمین میکند که فقط یک thread در یک زمان میتواند به یک بخش از کد دسترسی داشته باشد. اگر await
در داخل lock
مجاز بود، thread میتوانست در حین انتظار برای عملیات ناهمزمان، lock را نگه دارد و سایر threadها را بلاک کند.راه حلها:
SemaphoreSlim
: این بهترین و رایجترین راه حل برای Locking ناهمزمان است. SemaphoreSlim
یک مکانیسم Locking ناهمزمان است که به شما امکان میدهد با استفاده از WaitAsync()
و Release()
، دسترسی به یک منبع را به صورت ناهمزمان مدیریت کنید.مثال استفاده از SemaphoreSlim
:
public class MySharedResource { private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); public async Task AccessResourceAsync() { await _semaphore.WaitAsync(); try { // عملیات ناهمزمان روی منبع مشترک await Task.Delay(1000); } finally { _semaphore.Release(); } } }
async
و await
در Exception Handling چگونه کار میکنند؟پاسخ: مدیریت خطا در کدهای async/await
بسیار شبیه به کدهای همزمان است، اما با چند تفاوت کلیدی.
نحوه کار:
Task
: وقتی یک استثنا در یک متد async Task
یا async Task<T>
پرتاب میشود، استثنا در Task
برگشتی کپسوله میشود و وضعیت Task
به Faulted
تغییر میکند.await
: وقتی شما await
را روی یک Task
خطا رفته فراخوانی میکنید، استثنای کپسوله شده دوباره پرتاب میشود و میتوانید آن را با یک بلوک try-catch
مدیریت کنید.Task
را با Task.WhenAll
اجرا کنید و بیش از یک Task
با خطا مواجه شود، استثناهای آنها در یک AggregateException
جمعآوری میشوند.مثال مدیریت خطا:
public async Task RunAsync() { try { await DoSomethingAsync(); } catch (Exception ex) { Console.WriteLine($"Caught exception: {ex.Message}"); } } public async Task DoSomethingAsync() { await Task.Delay(100); throw new InvalidOperationException("Something went wrong!"); }
نکات مهم:
async void
: استثناهای پرتاب شده از async void
نمیتوانند به راحتی مدیریت شوند و میتوانند برنامه را Crash کنند.AggregateException
: هنگام استفاده از Task.WhenAll
، آماده مدیریت AggregateException
باشید.async
و await
در Performance چه تأثیری دارند؟پاسخ: async/await
به طور مستقیم سرعت اجرای کد را افزایش نمیدهد، بلکه با بهبود مقیاسپذیری و پاسخگویی، عملکرد کلی برنامه را بهبود میبخشد.
تأثیرات عملکردی:
async/await
مقداری سربار به دلیل ایجاد StateMachine و مدیریت Context دارد. این سربار معمولاً در مقایسه با مزایای مقیاسپذیری و پاسخگویی، ناچیز است.نکات بهینهسازی:
ConfigureAwait(false)
: در کدهای کتابخانهای، از ConfigureAwait(false)
برای کاهش سربار بازگشت به Synchronization Context استفاده کنید.ValueTask
: در مسیرهای داغ (Hot Paths) که عملیات ناهمزمان اغلب به صورت همزمان تکمیل میشود، از ValueTask
برای کاهش تخصیص حافظه استفاده کنید.async/await
در کدهای CPU-bound
: برای عملیاتهای CPU-bound، از Task.Run()
استفاده کنید.async
و await
در .NET Standard چگونه کار میکنند؟پاسخ: async/await
در .NET Standard به طور کامل پشتیبانی میشود. این به شما امکان میدهد تا کتابخانههای ناهمزمان بنویسید که میتوانند در پلتفرمهای مختلف .NET (مانند .NET Core, .NET Framework, Xamarin) استفاده شوند.
نکات مهم:
ConfigureAwait(false)
: در کتابخانههای .NET Standard، همیشه از ConfigureAwait(false)
استفاده کنید تا از Deadlock در محیطهای مختلف جلوگیری شود.async/await
به درستی استفاده میکنند.async
و await
در Blazor چگونه کار میکنند؟پاسخ: در Blazor، async/await
برای اجرای عملیاتهای ناهمزمان (مانند فراخوانی API) بدون بلاک کردن UI thread استفاده میشود.
نحوه کار:
async/await
به آزاد کردن thread سرور کمک میکند و مقیاسپذیری را بهبود میبخشد.async/await
به حفظ پاسخگویی UI در مرورگر کمک میکند.مثال در Blazor:
@page "/fetchdata"
@inject HttpClient Http
<h1>Weather forecast</h1>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
// ...
}
@code {
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
}
async
و await
در MAUI و Xamarin چگونه کار میکنند؟پاسخ: در MAUI و Xamarin، async/await
برای حفظ پاسخگویی UI در برنامههای موبایل استفاده میشود. این فریمورکها نیز دارای UI thread و Synchronization Context هستند.
نحوه کار:
await
را به UI thread بازمیگرداند.مثال در MAUI:
private async void OnCounterClicked(object sender, EventArgs e) { // این کد در UI thread اجرا میشود CounterBtn.Text = "Loading..."; // UI thread آزاد میشود string data = await DownloadDataAsync(); // این کد به UI thread بازمیگردد CounterBtn.Text = data; }
async
و await
در .NET 8 چه تغییراتی داشتهاند؟پاسخ: در .NET 8، بهبودهای عملکردی و بهینهسازیهایی در async/await
و Task
ها انجام شده است. برخی از این تغییرات عبارتند از:
Task.WhenAll
: Task.WhenAll
در .NET 8 بهینهسازی شده است تا در سناریوهایی که تعداد زیادی Task
وجود دارد، عملکرد بهتری داشته باشد.ValueTask
: ValueTask
در .NET 8 بهینهسازی شده است تا در سناریوهای بیشتری از تخصیص حافظه جلوگیری کند.