چرا Dictionary معمولی در اجرای موازی شکست میخورد؟
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۰۴ ۰۹:۳۰
آدرس: www.dntips.ir
List و Dictionary به دلیل ساختار سبک و کارایی بالا، بیشترین کاربرد را در توسعه نرمافزار دارند. با این حال، این مجموعهها برای دسترسی همزمان و پردازش موازی طراحی نشدهاند. تغییر همزمان یک Dictionary توسط چند نخ (Thread) میتواند منجر به تخریب ساختار دادههای داخلی (Data Corruption)، بروز استثناهای غیرمنتظره و حتی قفل شدن پردازنده (100% CPU Usage) به دلیل حلقههای بینهایت در ساختارهای درونی گردد.System.Collections.Concurrent، الگوی بهینه استفاده از Lazy جهت جلوگیری از Race Condition در متدهای کارخانهای، و الگوهای نادرست رایج در دسترسیهای غیر اتمیک میپردازد.Dictionary عملکرد فوقالعادهای با پیچیدگی زمانی نزدیک به O(1) در خواندن و نوشتن ارائه میدهند.Dictionary داشته باشیم. این مجموعه بر پایه آرایهای از خانه هَشها (Buckets) و ورودیها (Entries) کار میکند. هنگام افزودن یک کلید جدید:HashCode) محاسبه شده و نگاشت آن به یک Bucket انجام میشود.// استفاده ناایمن از دیکشنری معمولی در اجرای موازی
Dictionary<int, string> cache = new();
Parallel.For(0, 10000, i =>
{
// حتی اگر کلیدها متفاوت باشند، تغییر ساختار داخلی ناایمن است!
cache[i] = ComputeExpensiveData(i);
});IndexOutOfRangeException یا NullReferenceException از درون کدهای داخلی Runtime داتنت.TryGetValue) وارد یک حلقه بینهایت میشود. در این حالت مصرف یک یا چند هسته CPU به ۱۰۰٪ رسیده و کل سرویس وب ASP.NET Core قفل میکند.System.Collections.Concurrent را جهت حل این چالشها ارائه داده است. کلاسهای موجود در این فضانام از تکنیکهای پیشرفته همروندی مانند Lock-Free Programming، ساختارهای داده اتمیک (Interlocked) و قفلهای ریزدانه (Fine-grained Locks / SpinLock) بهره میبرند.ConcurrentDictionary، عملیات خواندن و نوشتن همزمان بدون تخریب حافظه تضمین میگردد:using System.Collections.Concurrent;
// کد ایمن و بهینهشده برای اجرای موازی
ConcurrentDictionary<int, string> cache = new();
Parallel.For(0, 10000, i =>
{
cache[i] = ComputeExpensiveData(i);
});| نام کلاس | ساختار / الگوی کاری | سناریوی کاربردی توصیه شده |
ConcurrentDictionary | Key/Value Pair ایمن | کشهای اشتراکی در حافظه، سرویسهای Singleton |
ConcurrentQueue | FIFO (صف ورود و خروج) | الگوهای Producer-Consumer و پردازش صف پیامها |
ConcurrentStack | LIFO (پشته) | ارزیابی تعبیرها، الگوریتمهای پشتهای موازی |
ConcurrentBag | مجموعه بدون ترتیب (Unordered) | تخصیصهای محلی نخ (Thread-Local) که در آن یک نخ عمدتاً دادههای خود را اضافه/حذف میکند. |
BlockingCollection | Wrapper روی صف/پشته با امکان Block | سناریوهایی که تولیدکننده و مصرفکننده سرعت متفاوت دارند و نیاز به انتظار اتوماتیک است. |
GetOrAdd یکی از پرکاربردترین متدهای ConcurrentDictionary است. با این حال یک برداشت اشتباه رایج وجود دارد: بسیاری تصور میکنند تابع کارخانهای (Factory Delegate) ارسال شده به این متد، تنها یک بار اجرا میشود!// کد دارای چالش کارایی var connection = cache.GetOrAdd(id, key => CreateConnection(key));
GetOrAdd را فراخوانی کنند، جهت جلوگیری از قفل شدن کامل دیکشنری، داتنت ممکن است تابع CreateConnection(key) را برای هر دو نخ اجرا کند! در نهایت فقط یک مقدار در دیکشنری ذخیره میشود و مقدار دیگر دور انداخته خواهد شد. اما اگر ساخت شیء، سنگین باشد (مانند ایجاد Connection شبکه یا DB)، منابع سیستم هدر خواهد رفت.Lazy با حالت ایمنی ExecutionAndPublication استفاده نمود:// الگوی پیشرفته و کاملاً ایمن برای اشیاء سنگین
var lazyEntity = cache.GetOrAdd(
id,
key => new Lazy<Connection>(
() => CreateConnection(key),
LazyThreadSafetyMode.ExecutionAndPublication
)
);
// ساخت واقعی شیء تنها زمانی اتفاق میافتد که Value فراخوانی شود
Connection connection = lazyEntity.Value;Lazy ساخته شود، بسیار سبک هستند و ساخت واقعی شیء درون Value تنها یکبار و بهصورت اتمیک انجام میپذیرد.// الگوی ناایمن (Race Condition) - حتی با ConcurrentDictionary!
if (!dict.ContainsKey(key)) // گام ۱: بررسی
{
dict[key] = value; // گام ۲: عمل
}// استفاده از متد اتمیک تکمرحلهای bool added = dict.TryAdd(key, value); // یا استفاده از AddOrUpdate dict.AddOrUpdate(key, addValue, (k, oldValue) => updateValue);
Dictionary معمولی سریعتر و بهینهتر است.ReadOnlyDictionary یا ImmutableDictionary گزینههای فوقالعاده بهینهتری هستند.Dictionary برای کارایی بالا در شرایط تکنخی طراحی شدهاند و استفاده از آنها در محیطهای موازی و همروند (نظیر سرویسهای Singleton در ASP.NET Core) میتواند منجر به باگهای هولناک، نشت داده و قفل شدن کامل CPU شود.ConcurrentDictionary استفاده کنید.TryAdd، GetOrAdd و AddOrUpdate استفاده نمایید.GetOrAdd سنگین است، جهت جلوگیری از اجرای چندباره، آن را با Lazy ترکیب کنید.