🚨 یک متد Async چطور میتواند کل برنامه را بدون هیچ Exceptionای قفل کند؟
یکی از عجیبترین باگهایی که ممکن است در پروژههای NET. با آن مواجه شوید این است:
برنامه اجرا میشود.
هیچ Exceptionای ندارید.CPU هم لزوماً بالا نیست.
اما یک بخش از برنامه دیگر جلو نمیرود.
همهچیز منتظر چیزی است...
و آن چیز هم منتظر همان Thread است که شما قفلش کردهاید.
به این وضعیت میگوییم:🔒 Async Deadlock
🤔 ءDeadlock دقیقاً چطور اتفاق میافتد؟
فرض کنید یک متد Async دارید:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "Hello";
}
و آن را اینطور صدا میزنید:
var result = GetDataAsync().Result;
در ظاهر شاید مشکلی نبینید.
اما در محیطهایی که
SynchronizationContext وجود دارد، اتفاق زیر ممکن است رخ دهد:Thread اصلی
│
▼
GetDataAsync()
│
▼
await
│
├── متد کنترل را آزاد میکند
│
▼
ءContinuation باید روی Context اصلی ادامه پیدا کند
│
❌
اما Thread اصلی با Result. بلاک شده است
│
▼
Continuation نمیتواند اجرا شود
│
▼
.Result منتظر پایان Task است
│
🔒 DEADLOCK
یعنی:
ءTask منتظر Thread است
و Thread منتظر Task
و هیچکدام نمیتوانند جلو بروند.
☠️ ترکیب خطرناک:await+.Result
مشکل اصلی
async نیست.مشکل زمانی ایجاد میشود که:
یک عملیات Async دارید
await میکنید Continuation باید به یک SynchronizationContext خاص برگردداما همان Context را با
.Result یا .Wait() بلاک کردهایدمثلاً:
var result = GetDataAsync().Result;
یا:
``
lua
GetDataAsync().Wait();``این دو مورد در چنین سناریوهایی میتوانند یکی از رایجترین دلایل Deadlock باشند.
🧠 نقش SynchronizationContext چیست؟
وقتی شما مینویسید:
await SomeAsyncOperation();
بعضی محیطهای NET. تلاش میکنند بعد از پایان عملیات Async، ادامه متد را دوباره روی همان Context قبلی اجرا کنند.
مثلاً:
UI Thread
│
▼
await
│
▼
عملیات Async
│
▼
بازگشت به UI Thread
این رفتار در محیطهایی مثل:
WPF
Windows Forms
ASP.NET کلاسیک
اهمیت زیادی دارد.
چون ممکن است بعد از
await نیاز داشته باشید دوباره به Context اصلی برگردید.اما اگر همان Context را بلاک کنید:
.Result
ءContinuation جایی برای اجرا ندارد.
و برنامه وارد Deadlock میشود.
🛠 راهکار اول: Async All the Way
بهترین و مهمترین راهکار این است که عملیات Async را دوباره به عملیات Sync تبدیل نکنید.
❌ این:
public string GetData()
{
return GetDataAsync().Result;
}
بهتر است به این تبدیل شود:
public async Task<string> GetDataAsync()
{
return await _service.GetDataAsync();
}
و در لایه بالاتر:
var result = await GetDataAsync();
قاعده ساده است:
اگر وارد دنیای Async شدید، تا جای ممکن در همان دنیا بمانید.
این همان چیزی است که معمولاً با عنوان:
🚀 Async All the Way
شناخته میشود.
🔧 ءConfigureAwait(false) چه کاری انجام میدهد؟
فرض کنید یک Library دارید:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "Hello";
}
بهصورت پیشفرض، در محیطهایی که Context وجود دارد،
await ممکن است تلاش کند ادامه اجرای متد را روی همان Context قبلی ادامه دهد.اما با:
await Task.Delay(1000).ConfigureAwait(false);
به سیستم میگویید:
بعد از پایان این عملیات، لازم نیست اجرای ادامه متد را روی Context قبلی ادامه بدهی.
مثلاً:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000)
.ConfigureAwait(false);
return "Hello";
}
در نتیجه احتمال گرفتار شدن در Deadlockهای کلاسیک ناشی از SynchronizationContext کاهش پیدا میکند.
⚠️ اما آیا باید همهجا ConfigureAwait(false) بنویسیم؟
نه دقیقاً.
اگر در یک Application UI هستید و بعد از
await میخواهید UI را تغییر دهید، ممکن است نیاز داشته باشید به همان Context اصلی برگردید.مثلاً:
await LoadDataAsync();
// ممکن است نیاز باشد روی UI Thread باشیم
MyLabel.Text = "Loaded";
اما در Libraryها و لایههایی که وابستگی مستقیمی به Context برنامه ندارند، معمولاً عدم وابستگی به Context باعث میشود کد قابلاستفادهتر و مستقلتر باشد.
به همین دلیل در بسیاری از Libraryها و کدهای زیرساختی، استفاده از:
.ConfigureAwait(false)
یک الگوی رایج است.