TGViewer
C# Geeks (.NET) C# Geeks (.NET) @csharpgeeks · 550 subscribers
Post #561 177
🧱 سطح 1️⃣ : اضافه کردن ConcurrentDictionary

اولین فکری که احتمالاً به ذهنت می‌رسد این است که نرخ‌ها را داخل یک ConcurrentDictionary ذخیره کنی 🧠.
این ساختار thread-safe است، پس در نگاه اول ابزار درستی به نظر می‌رسد ✅.
private static readonly ConcurrentDictionary<string, decimal> Cache = new();

// In the Handler:
if (Cache.TryGetValue(currencyCode, out var cachedRate))
{
return cachedRate;
}

var rate = await currencyClient.GetExchangeRateAsync(currencyCode);

Cache.TryAdd(currencyCode, rate.Value);

این کار قطعاً تحت بار بالا به بهبود performance کمک می‌کند 🚀.
چندین thread می‌توانند هم‌زمان از دیکشنری بخوانند یا در آن بنویسند بدون اینکه برنامه کرش کند 💪.
اما ConcurrentDictionary فقط از ساختار دیکشنری محافظت می‌کند، نه از منطق شما ⚠️.

اگر ۱۰۰ کاربر دقیقاً در یک لحظه نرخ "EUR" را درخواست کنند 👥👥👥، TryGetValue برای همه‌ی آن‌ها false برمی‌گرداند.
در نتیجه، همه‌شان هم‌زمان API را صدا می‌زنند 📡📡📡.

این یک race condition کلاسیک است 🏁.
شما حافظه را امن کرده‌اید، اما از API خارجی محافظت نکرده‌اید ❌.

یک مشکل دیگر هم وجود دارد:
نرخ‌ها هیچ‌وقت expire نمی‌شوند ⏳❌.

⏰ سطح 2️⃣: اضافه کردن انقضای Cache

نرخ ارزها برای همیشه ثابت نمی‌مانند 💱.
ما به راهی نیاز داریم که بعد از مدتی آن‌ها را منقضی کنیم.

از آن‌جایی که ConcurrentDictionary مفهوم Time To Live (TTL) ندارد،
باید داده‌هایمان را wrap کنیم 📦.
// Store both the rate and the time it was created
private record CacheEntry(decimal Rate, DateTime CreatedAt);

// Our cache now stores CacheEntry objects
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);

// Check: Is it there? And is it still "fresh"?
if (Cache.TryGetValue(currencyCode, out var entry) &&
(DateTime.UtcNow - entry.CreatedAt) < CacheDuration)
{
return entry.Rate;
}

حالا expiration داریم ✅.
اما در عوض، یک مشکل جدید ساخته‌ایم 😬:
🐘 Thundering Herd (یا Cache Stampede)

هر ۵ دقیقه یک‌بار، وقتی cache منقضی می‌شود ⏱️،
تمام درخواست‌های ورودی به‌صورت هم‌زمان داده‌ی «منقضی‌شده» می‌بینند
و همگی تلاش می‌کنند آن را refresh کنند 💥.

پس باید این مشکل را در مرحله‌ی بعدی حل کنیم.

🚦 سطح 3️⃣: حل مشکل «Cache Stampede»

برای حل این مشکل، باید مطمئن شویم که
فقط یک نفر اجازه دارد داده را به‌روزرسانی کند
و بقیه منتظر بمانند ⏸️.

در #C چطور این کار را انجام می‌دهیم؟ 🤔
ما از SemaphoreSlim و الگویی به نام Double-Checked Locking استفاده می‌کنیم 🔐.
اول یک بار cache را چک می‌کنیم (مسیر سریع 🏃‍♂️)،
بعد lock می‌گیریم،
و سپس دوباره چک می‌کنیم تا ببینیم آیا در این فاصله thread دیگری cache را پر کرده یا نه.
// Basically a mutex but async-friendly
private static readonly SemaphoreSlim Lock = new(1, 1);

public static async Task<decimal> GetRateAsync(string code, CurrencyApiClient client)
{
// Fast path: No locking needed
if (Cache.TryGetValue(code, out var entry) && IsFresh(entry))
{
return entry.Rate;
}

var acquired = await Lock.WaitAsync(TimeSpan.FromSeconds(10)); // Avoid deadlocks
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check: Did someone else finish the API call while we waited?
if (Cache.TryGetValue(code, out entry) && IsFresh(entry))
{
return entry.Rate;
}

var rate = await client.GetExchangeRateAsync(code);
var newEntry = new CacheEntry(rate.Value, DateTime.UtcNow);

// Atomically update the cache
// This is safe because we're inside the lock
Cache.AddOrUpdate(code, newEntry, (_, _) => newEntry);
return rate.Value;
}
finally
{
// Always release the lock
Lock.Release();
}
}

این کار یک بهبود واقعی است 👍.
اما هنوز یک حس بد وجود دارد… 😐

می‌توانی مشکل این کد را پیدا کنی؟ 👀

🔒 این lock مثل یک global lock رفتار می‌کند.
یعنی اگر یک thread در حال گرفتن نرخ "EUR" باشد،
تمام threadهای دیگر (حتی آن‌هایی که "JPY" می‌خواهند 🇯🇵)
تا پایان درخواست "EUR" بلاک می‌شوند ⛔️.

این مشکل به lock contention معروف است ⚠️.

در مرحله‌ی بعدی، این مشکل را هم حل می‌کنیم 🛠✨.
More from @csharpgeeks
  1. Sep 22, 2026یه مدتی قراره از دنیای NET. فاصله بگیرم، چون وقتشه برم سربازی. راستش نمیدونم این مدت رو چج…
  2. Sep 20, 2026🔥 حالا مشکل اصلی: Alert Storm فرض کن Database از دسترس خارج شده. ۱۰۰ Pod داری. هر Pod می‌…
  3. Sep 20, 2026🚨 طراحی سیستم Monitoring و Alerting در یک سیستم بزرگ فرض کن ساعت ۳ صبح است. سیستم شما با…
  4. Sep 19, 2026#Engineering_Leadership تصمیم نگرفتن هم یک تصمیم است یه چیز عجیب توی تیم‌های مهندسی: گاهی…
  5. Sep 19, 2026☑ چک‌لیست آماده‌سازی تیم، فرایندها و زیرساخت برای توسعه با AI توجه: هیچ چک‌لیستی جهان‌شمول…
  6. Sep 19, 2026📌پایان یک انتظار طولانی: اعتبارسنجی ناهمگام (Async Validation) در NET 11.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →