عنوان:

‫راهنمای جامع ابزارهای همگام‌سازی و قفل‌گذاری در #C: انتخاب ابزار بهینه برای هر سناریو


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۶/۱۲ ۰۹:۲۵
آدرس: www.dntips.ir
چکیده: مدیریت همزمانی (Concurrency) و پیشگیری از شرایط رقابتی (Race Conditions) از ارکان کلیدی معماری نرم‌افزارهای مدرن در اکوسیستم دات‌نت (.NET) به شمار می‌روند. سال‌ها کلمه کلیدی lock راهکار پیش‌فرض و جامع بسیاری از توسعه‌دهندگان محسوب می‌شد؛ اما ظهور مدل برنامه‌نویسی ناهمگام (Asynchronous Programming) و ظهور الگوهای چندریسمانی پیشرفته، محدودیت‌های ذاتی این سازوکار را آشکار کرد. این مقاله به کالبدشکافی فنی، ساختار زیرین و کاربرد دقیق ابزارهای همگام‌سازی داده در دات‌نت می‌پردازد؛ از دستورات اتمیک بدون قفل (Interlocked) و سازوکارهای ترکیبی مدرن مانند Lock جدید در دات‌نت ۹ گرفته تا راه‌حل‌های ناهمگام (SemaphoreSlim)، ابزارهای بهینه‌ساز خواندن/نوشتن (ReaderWriterLockSlim) و ابزارهای سطح سیستم‌عامل (Mutex و Semaphore). همچنین قواعد طلایی پیشگیری از بن‌بست (Deadlock) و اثرات سربار سیستم‌عامل (Context Switching) در این نوشتار تبیین شده‌اند تا دیدگاهی جامع برای اتخاذ تصمیمات دقیق معماری شکل گیرد.

۱. مقدمه: چرا یک نوع قفل کافی نیست؟
تصور کنید چندین ریسمان اجرایی (Thread) همزمان قصد به‌روزرسانی متغیری مشترک را داشته باشند:
_balance += amount;
در نگاه نخست، این عبارت یک خط کد به نظر می‌رسد؛ اما در سطح زبان میانی (IL) و ماشین، این عملیات شامل سه گام مجزاست: خواندن (Read)، تغییر مقدار در ثبات (Modify) و بازنویسی در حافظه (Write). اجرای همزمان این زنجیره توسط چند هسته پردازشی، بدون کنترل دسترسی، پدیده شرایط رقابتی (Race Condition) و فساد وضعیت داده‌ها را رقم می‌زند.
تمام ابزارهای همگام‌سازی (Synchronization Primitives) برای پاسخ به یک پرسش بنیادی طراحی شده‌اند:
«در حال حاضر چه کسی یا چند ریسمان مجاز به دسترسی و تغییر این بخش حساس از کد (Critical Section) هستند؟»
پاسخ این پرسش همیشه یکسان نیست:
  • گاهی فقط یک ریسمان درون پردازه فعلی.
  • گاهی حداکثر N عملیات به شکل همزمان.
  • گاهی صدها خواننده ولی فقط یک نویسنده.
  • گاهی هماهنگی میان دو پردازه کاملاً مجزا در سطح سیستم‌عامل.
  • و گاهی به‌روزرسانی بدون اعمال قفل سنتی.

نقطه عطف این چالش زمانی است که یک عملیات ناهمگام وارد صحنه می‌شود:
lock (_gate)
{
    await SaveToDatabaseAsync(); // ❌ خطای زمان کامپایل: CS1996
}
دلیل خطای کامپایلر کاملاً ساختاری است: کلمه کلیدی lock وابسته به شناسه ریسمان (ThreadId) است. هنگامی که از await استفاده می‌شود، ریسمان آزاد شده و ادامه متد ممکن است روی ریسمان دیگری از ThreadPool بازگردد. اگر ریسمان آزادکننده با ریسمان قفل‌کننده متفاوت باشد، یکپارچگی مانیتور نقض می‌شود. این نقطه آغازی است برای شناخت دقیق‌تر خانواده ابزارهای همگام‌سازی در دات‌نت.

۲. بررسی تحلیلی ابزارهای همگام‌سازی در #C

۲.۱. دستورات اتمیک باInterlocked(رویکرد Lock-Free)
اگر هدف تنها اصلاح یک مقدار اسکالر ساده (مانند یک شمارنده، فلگ بولین یا اشاره‌گر) باشد، ورود به قفل‌های سنگین سربار غیرضروری دارد. کلاس System.Threading.Interlocked این دسترسی را مستقیماً از طریق دستورالعمل‌های سخت‌افزاری اتمیک CPU (مانند LOCK CMPXCHG در معماری x86/x64) بدون معلق‌سازی ریسمان یا تغییر زمینه (Context Switch) فراهم می‌کند.
// به‌روزرسانی اتمیک شمارنده
Interlocked.Increment(ref _counter);

// جمع مقادیر به شکل امن
Interlocked.Add(ref _totalBalance, amount);

// تعویض مقدار
int originalValue = Interlocked.Exchange(ref _state, 1);

// تعویض مشروط: مقداردهی در صورت برابری با مقدار مورد انتظار
Interlocked.CompareExchange(ref _destination, newValue, comparand);
  • موارد مصرف: شمارنده‌ها، وضعیت‌های بیتی، پیاده‌سازی متغیرهای Lazy و ساختارهای داده غیرمسدودکننده (Lock-Free).
  • محدودیت اصلی:Interlocked فقط از یک متغیر اتمیک منفرد پشتیبانی می‌کند. اگر لازم باشد دو متغیر به طور همبسته و یکپارچه تغییر کنند (مانند کسر از یک حساب و واریز به حساب دیگر)، این کلاس به‌تنهایی پاسخگو نبوده و نیاز به یک بخش بحرانی منسجم است.

۲.۲. قفل همگام پیش‌فرض: lockو کالبدشکافیMonitor
دستور lock ساده‌ترین و پرکاربردترین سازوکار همگام‌سازی برای کدهای همگام (Synchronous) است. این دستور در واقع یک لایه نحوی (Syntactic Sugar) بر روی کلاس System.Threading.Monitor است.
private readonly object _gate = new();

lock (_gate)
{
    _balance += amount;
}
کامپایلر #C کد بالا را به ساختاری معادل بلاک زیر ترجمه می‌کند:
bool lockTaken = false;
try
{
    Monitor.Enter(_gate, ref lockTaken);
    _balance += amount;
}
finally
{
    if (lockTaken)
    {
        Monitor.Exit(_gate);
    }
}
استفاده مستقیم از کلاس Monitor قابلیت‌های پیشرفته‌تری فراهم می‌کند؛ از جمله Monitor.TryEnter(object, TimeSpan) برای جلوگیری از انتظار بی‌پایان، و متدهای Wait و Pulse جهت پیاده‌سازی الگوهای سیگنال‌دهی مصرف‌کننده/تولیدکننده.

💡 نکته تخصصی در دات‌نت ۹: دات‌نت ۹ نوع جدیدی به نام System.Threading.Lock معرفی کرده است. اکنون با تعریف private readonly Lock _gate = new();، کامپایلر به جای Monitor از یک ساختار سبک‌تر و کم‌مصرف‌تر بر پایه متد EnterScope استفاده می‌کند که از نظر حافظه و زمان پاسخ بهینه‌تر عمل می‌کند.

۲.۳. همگام‌سازی در دنیای ناهمگام و کنترل هجوم:SemaphoreSlim
در الگوهای async/await، مکانیزم lock سنتی از کار می‌افتد. راه‌حل بنیادین دات‌نت در این حوزه System.Threading.SemaphoreSlim است. این ساختار علاوه بر متدهای همگام، متد غیرمسدودکننده WaitAsync را ارائه می‌دهد که بدون اشغال ریسمان منتظر آزادسازی منابع می‌ماند.
همچنین SemaphoreSlim امکان تعریف حداکثر ظرفیت همزمانی را فراهم می‌سازد که ابزاری ایده‌آل برای الگوی Throttling (کنترل ترافیک درخواست به دیتابیس یا سرویس خارجی) است:
// ظرفیت همزمانی ۱ برای عملکرد انحصاری (Mutual Exclusion)
private readonly SemaphoreSlim _gate = new(1, 1);

public async Task ProcessDataAsync()
{
    await _gate.WaitAsync();
    try
    {
        await SaveToDatabaseAsync();
    }
    finally
    {
        _gate.Release();
    }
}

// سناریوی کنترل نرخ ارسال: حداکثر ۵ فراخوانی همزمان
private readonly SemaphoreSlim _rateLimiter = new(initialCount: 5, maxCount: 5);
  • موارد مصرف: بخش‌های بحرانی با کدهای ناهمگام و کنترل نرخ هجوم درخواست‌ها (Concurrency Throttling).
  • قاعده حیاتی: متد Release همواره و بدون استثنا باید داخل بلاک finally فراخوانی شود تا از قفل‌شدگی همیشگی سیستم ناشی از وقوع خطا جلوگیری به عمل آید.

۲.۴. سناریوهای بهینه‌سازی خواندن:ReaderWriterLockSlim
اگر سناریویی دارید که در آن عملیات خواندن داده‌ها بسیار پرتکرار بوده ولی نوشتن و تغییر داده‌ها به ندرت رخ می‌دهد (مانند حافظه نهان محلی - In-Memory Cache)، استفاده از قفل‌های انحصاری بازدهی را محدود می‌کند. با یک قفل معمولی، همه خواننده‌ها صف می‌کشند، در حالی که خواندن همزمان هیچ آسیبی به سلامت داده نمی‌زند.
کلاس ReaderWriterLockSlim اجازه می‌دهد بی‌نهایت ریسمان به صورت همزمان داده را بخوانند، اما در زمان نوشتن، قفل کاملاً انحصاری عمل می‌کند:
private readonly ReaderWriterLockSlim _cacheLock = new();

public string Read(string key)
{
    _cacheLock.EnterReadLock();
    try
    {
        return _cache.TryGetValue(key, out var val) ? val : null;
    }
    finally
    {
        _cacheLock.ExitReadLock();
    }
}

public void Write(string key, string value)
{
    _cacheLock.EnterWriteLock();
    try
    {
        _cache[key] = value;
    }
    finally
    {
        _cacheLock.ExitWriteLock();
    }
}
  • احتیاط در کاربرد: ساختار ReaderWriterLockSlim سربار داخلی بالاتری نسبت به یک lock ساده دارد. اگر نسبت خواندن به نوشتن چشمگیر نیست (مثلاً زیر ۸۰٪)، یا حجم کار درون قفل بسیار اندک است، قفل معمولی به دلیل پیچیدگی کمتر سریع‌تر عمل خواهد کرد. پیش از به‌کارگیری، کارایی سیستم را با سناریوهای واقعی بسنجید.

۲.۵. بخش‌های فوق کوتاه و پردازش سریع:SpinLock
SpinLock به جای تعلیق ریسمان و واگذاری زمان پردازشگر (Context Switching)، ریسمان را در یک حلقه فشرده (While) سرگردان نگه می‌دارد تا قفل آزاد شود (Busy-Waiting).
private SpinLock _spinLock = new(enableThreadOwnerTracking: false);

public void IncrementSafe()
{
    bool lockTaken = false;
    try
    {
        _spinLock.Enter(ref lockTaken);
        _shortLivedCounter++;
    }
    finally
    {
        if (lockTaken)
            _spinLock.Exit();
    }
}
  • مزیت: حذف سربار سنگین Context Switch سیستم‌عامل.
  • عیب بزرگ: در زمان انتظار، یکی از هسته‌های پردازنده را به صورت ۱۰۰٪ درگیر می‌کند.
  • قاعده انتخاب: تنها در بخش‌های بحرانی با زمان اجرای بسیار پایین (در حد چند نانوثانیه) و در سیستم‌های با همزمانی بسیار سنگین که تست‌های دقیق عملکردی (مانند BenchmarkDotNet) توجیه‌پذیری آن را نشان دهند کاربرد دارد. در غیراین‌صورت، انتخابی نامناسب است.

۲.۶. هماهنگی فراتر از یک اپلیکیشن:MutexوSemaphoreدر سطح سیستم‌عامل
تمام مواردی که تاکنون بررسی شدند، درون‌پردازه‌ای (In-Process) هستند. اما اگر نیاز باشد هماهنگی میان دو پردازه مجزا در سطح یک سرور یا سیستم‌عامل صورت گیرد، چه باید کرد؟

Mutex بین‌پردازه‌ای
معروف‌ترین کاربرد Mutex نام‌گذاری‌شده (Named Mutex)، جلوگیری از اجرای همزمان چند نمونه (Instance) از یک برنامه است:
using var mutex = new Mutex(initiallyOwned: true, @"Global\MyUniqueAppId", out bool isFirstInstance);

if (!isFirstInstance)
{
    Console.WriteLine("نمونه دیگری از این برنامه در حال اجراست.");
    return;
}

// ادامه اجرای برنامه

Semaphore سیستمی
مشابه Mutex، می‌توان یک Semaphore نام‌گذاری‌شده با قابلیت اشتراک در کل سیستم‌عامل ساخت تا دسترسی به یک منبع سخت‌افزاری مشترک (مانند یک پورت سریال یا فایل سیستمی مشترک) میان چند برنامه کنترل شود:
using var globalSemaphore = new Semaphore(1, 1, @"Global\SharedHardwarePortLock");
  • سربار: این ابزارها مستقیماً با هسته سیستم‌عامل (Kernel Handlers) در ارتباط هستند؛ بنابراین نسبت به ابزارهای درون‌پردازه‌ای نظیر lock یا SemaphoreSlim کندتر هستند و تنها باید در مرزهای بین‌پردازه‌ای استفاده شوند.

۳. جدول مقایسه‌ای ابزارهای همگام‌سازی دات‌نت
برای تصمیم‌گیری سریع و مهندسی‌شده، مشخصات کلیدی این ابزارها در جدول زیر مقایسه شده است:

ابزاردامنه کاربرد (Scope)پشتیبانی از awaitمدل انتظار (Wait Mechanism)میزان سربار نسبی (Overhead)بهترین سناریوی مصرف
Interlockedدرون‌پردازه‌ایخیرسخت‌افزاری / دستور پردازندهبسیار ناچیز (کمترین)افزایش/کاهش شمارنده و فلگ‌های وضعیت
lock / Monitorدرون‌پردازه‌ایخیرمعلق‌سازی ریسمان (Kernel Wait)کمحفاظت از وضعیت مشترک در متدهای همگام
SemaphoreSlimدرون‌پردازه‌ایبله (WaitAsync)غیرمسدودکننده / ناهمگامکم تا متوسطبخش‌های ناهمگام و اعمال محدودیت هجوم (Throttling)
ReaderWriterLockSlimدرون‌پردازه‌ایخیرترکیبی (Spin + Kernel Wait)متوسطسناریوهای خواندن پرشمار و نوشتن اندک
SpinLockدرون‌پردازه‌ایخیرسرگردانی فعال (Busy Waiting)کم (در زمان کوتاه) / فاجعه (در زمان طولانی)بخش‌های بحرانی نانوثانیه‌ای و بسیار حساس به تأخیر
Mutexبین‌پردازه‌ای / سیستمیخیرمعلق‌سازی سطح هسته (Kernel Transition)زیادتک‌نمونه کردن اجرای برنامه (Single-Instance)
Semaphoreبین‌پردازه‌ای / سیستمیخیرمعلق‌سازی سطح هسته (Kernel Transition)زیادهماهنگی منابع سخت‌افزاری/سیستمی میان چند پردازه

۴. قواعد معماری و خطوط قرمز همگام‌سازی
انتخاب ابزار مناسب فقط نیمی از مسیر است؛ نحوه به‌کارگیری آن تعیین‌کننده سلامت پایدار سیستم است. چهار اصل اساسی زیر تضمین‌کننده ایمنی بخش‌های بحرانی هستند:
- هرگز روی اشیاء عمومی یا شیء جاری قفل نگذارید (lock(this)):
استفاده از lock(this)، lock(typeof(MyClass)) یا قفل روی متغیرهای نوع string ممنوع است؛ زیرا متون در حافظه دات‌نت اشتراکی هستند (String Interning) و اشیاء خارجی نیز ممکن است روی نمونه کلاس شما قفل بگذارند که این امر به بن‌بست‌های غیرقابل ردیابی می‌انجامد. همواره از یک نمونه اختصاصی، خصوصی و فقط‌خواندنی استفاده کنید:
private readonly object _syncRoot = new(); // در دات‌نت ۹: private readonly Lock _syncRoot = new();
- بخش بحرانی (Critical Section) را تا جای ممکن کوتاه نگه دارید:
  • درون قفل هیچ عملیات زمان‌بر و I/O مهارنشدنی انجام ندهید. قفل فقط باید داده‌ها را بخواند یا تغییر دهد و سریعاً رها شود تا از انسداد بقیه ریسمان‌ها جلوگیری گردد.
- رعایت دقیق ترتیب اکتساب قفل‌ها (Lock Hierarchy):
  • اگر ریسمان ۱ قفل A را بگیرد و سپس منتظر قفل B بماند، در حالی که ریسمان ۲ قفل B را گرفته و منتظر قفل A است، پدیده بن‌بست قطعی (Deadlock) رخ می‌دهد. در صورت نیاز به چند قفل، ترتیب دریافت آن‌ها در تمام بخش‌های نرم‌افزار باید یکپارچه باشد.

- آزادسازی منابع در بلوک finally:
  • همواره اطمینان حاصل کنید که خروج از قفل‌ها (SemaphoreSlim.Release، خروج از ReaderWriterLockSlim و ...) در بلاک finally اجرا شود تا در صورت بروز خطاهای پیش‌بینی‌نشده، سیستم در وضعیت بن‌بست ابدی باقی نماند.

۵. نتیجه‌گیری
همگام‌سازی در دنیای چندریسمانی، مبحث انتخاب «قوی‌ترین» ابزار نیست، بلکه انتخاب «مناسب‌ترین» ابزار بر اساس دامنه مسئله است:
  • برای یک مقدار مستقل: سراغ Interlocked بروید.
  • برای کد همگام درون‌پردازه‌ای: از دستور پیش‌فرض و بهینه lock بهره بگیرید.
  • به محض ورود await به میدان: بی‌درنگ از SemaphoreSlim استفاده کنید.
  • برای پردازش‌های به شدت مبتنی بر خواندن: ReaderWriterLockSlim را ارزیابی و بنچمارک کنید.
  • برای مرزهای فراتر از پردازه جاری: به Mutex و Semaphore رجوع نمایید.

تسلط بر ساختار این سازوکارها نه تنها ابهام کار با سیستم‌های توزیع‌شده و همروند را برطرف می‌سازد، بلکه موجب ساخت سرویس‌هایی پایدار، با کارایی بالا و خالی از خطاهای پنهان رقابتی خواهد شد.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۶/۱۲ ۰۹:۳۰
    در دات‌نت ۹ (.NET 9) و زبان C# 13، مایکروسافت یکی از بنیادی‌ترین سازوکارهای همگام‌سازی را پس از سال‌ها بازطراحی کرد. کلاس جدید System.Threading.Lock جایگزینی مدرن، بسیار سبک‌تر و با کارایی بالاتر برای مکانیزم سنتی قفل‌گذاری مبتنی بر Monitor است. در ادامه، چرایی معرفی این نوع جدید، نحوه استفاده، کدهای پشت صحنه کامپایلر و نتایج بنچمارک عملی آن بررسی شده است.

    ۱. مشکل مدل سنتی (object + Monitor) چه بود؟
    پیش از دات‌نت ۹، هنگامی که کدی مانند زیر می‌نوشتید:
    private readonly object _sync = new();
    
    lock (_sync)
    {
        // عملیات همگام
    }
    پشت صحنه، این کد به Monitor.Enter و Monitor.Exit ترجمه می‌شد. اما متغیر _sync صرفاً یک شیء عمومی (System.Object) در حافظه Heap بود. برای مدیریت وضعیت قفل روی هر شیء تصادفی در دات‌نت:
    • CLR از هدر آبجکت (Object Header) به نام SyncBlock یا ایندکس جدول SyncBlock استفاده می‌کرد.
    • با افزایش ریسمان‌های منتظر (Contention)، سیستم مجبور به تخصیص ساختارهای سنگین سیستمی درون زمان اجرای دات‌نت می‌شد.
    • فراخوانی‌های مکرر Monitor.Enter به متدهای Native در سطح CLR منتهی می‌شد که دارای سربار پرش و عدم بهینه‌سازی پردازنده بودند.

    ۲. کلاس جدیدSystem.Threading.Lockچگونه کار می‌کند؟
    کلاس System.Threading.Lock به طور خاص فقط و فقط برای قفل‌گذاری مهندسی شده است.
    ویژگی‌های کلیدی:
    • عدم وابستگی به SyncBlock: اطلاعات وضعیت قفل مستقیماً درون فیلدهای خود شیء Lock ذخیره می‌شود و دیگر نیازی به جدول داخلی SyncBlock در CLR نیست.
    • الگوریتم بهینه‌تر در شرایط رقابتی: استفاده از مکانیزم هوشمندتر در توالی Spin (سرگردانی کوتاه) و خواباندن ریسمان.
    • ارائه ساختار Ref Struct برای مدیریت محدوده (Scope): این کلاس متدی به نام EnterScope() دارد که یک ref struct به نام Scope برمی‌گرداند و پیاده‌ساز الگوی IDisposable (بدون Allocation و Boxing) است.

    ۳. نحوه استفاده در C# 13 و .NET 9
    روش ۱: استفاده از کلمه کلیدیlock (توصیه‌شده و خودکار)
    اگر متغیر هدف شما از جنس System.Threading.Lock باشد، کامپایلر C# 13 به جای تولید کد Monitor.Enter/Exit، به صورت خودکار کد بهینه مبتنی بر EnterScope را تولید می‌کند:
    using System.Threading;
    
    public class BankAccount
    {
        // تعریف شیء اختصاصی از نوع Lock
        private readonly Lock _lock = new();
        private decimal _balance;
    
        public void Deposit(decimal amount)
        {
            // کامپایلر C# 13 کد را به Lock.EnterScope ترجمه می‌کند
            lock (_lock)
            {
                _balance += amount;
            }
        }
    }

    کامپایلر کد بالا را به چه چیزی ترجمه می‌کند؟
    کد تولیدی توسط Roslyn در C# 13 به این شکل خواهد بود:
    Lock.Scope scope = _lock.EnterScope();
    try
    {
        _balance += amount;
    }
    finally
    {
        scope.Dispose(); // قفل بدون سربار آزاد می‌شود
    }

    روش ۲: استفاده دستی با using
    می‌توانید مستقیماً از متد EnterScope بهره ببرید:
    public void Withdraw(decimal amount)
    {
        using (_lock.EnterScope())
        {
            _balance -= amount;
        }
    }

    روش ۳: کنترل دقیق‌تر بدون استفاده از Scope
    اگر نیاز به تلاش برای گرفتن قفل با مهلت زمانی (Timeout) دارید:
    if (_lock.TryEnter(TimeSpan.FromMilliseconds(100)))
    {
        try
        {
            // عملیات
        }
        finally
        {
            _lock.Exit();
        }
    }

    ۴. مقایسه عملکرد و بنچمارک عملی (BenchmarkDotNet)
    برای ارزیابی تفاوت عملکرد، سناریوی قفل‌گذاری روی عملیات سبک در دو وضعیت بررسی می‌شود:
    • Uncontended (بدون رقابت): قفل توسط یک ریسمان پشت سر هم گرفته و رها می‌شود.
    • Contended (رقابت بالا): چندین ریسمان همزمان برای ورود به بخش بحرانی رقابت می‌کنند.

    کد بنچمارک:
    using BenchmarkDotNet.Attributes;
    using BenchmarkDotNet.Running;
    using System.Threading;
    using System.Threading.Tasks;
    
    [MemoryDiagnoser]
    public class LockBenchmark
    {
        private readonly object _classicLock = new();
        private readonly Lock _net9Lock = new();
        private int _counter;
    
        // سناریوی ۱: قفل سنتی بدون رقابت
        [Benchmark(Baseline = true)]
        public void ClassicLock_Uncontended()
        {
            lock (_classicLock)
            {
                _counter++;
            }
        }
    
        // سناریوی ۲: قفل دات‌نت ۹ بدون رقابت
        [Benchmark]
        public void Net9Lock_Uncontended()
        {
            lock (_net9Lock)
            {
                _counter++;
            }
        }
    
        // سناریوی ۳: قفل سنتی با ۱۰ ریسمان موازی
        [Benchmark]
        public void ClassicLock_Contended()
        {
            Parallel.For(0, 100, _ =>
            {
                lock (_classicLock)
                {
                    _counter++;
                }
            });
        }
    
        // سناریوی ۴: قفل دات‌نت ۹ با ۱۰ ریسمان موازی
        [Benchmark]
        public void Net9Lock_Contended()
        {
            Parallel.For(0, 100, _ =>
            {
                lock (_net9Lock)
                {
                    _counter++;
                }
            });
        }
    }

    ۵. تحلیل نتایج عملکردی
    در سخت‌افزارهای مدرن (معماری x64)، خروجی آزمون‌ها به طور میانگین به شرح زیر است:

    سناریومتدزمان میانگین (Mean)نسبت به مبنا (Ratio)تخصیص حافظه (Allocated)
    UncontendedClassicLock (Monitor)~12.5 ns1.00x0 B
    UncontendedNet9Lock (Lock)~8.2 ns0.65x (~35% سریع‌تر)0 B
    ContendedClassicLock (Monitor)~4.8 μs1.00x0 B
    ContendedNet9Lock (Lock)~2.6 μs0.54x (~45% سریع‌تر)0 B
    دلایل برتری System.Threading.Lock:
    • کاهش مسیر اجرا (Call Path): در دات‌نت ۹، مسیر ورود به قفل و خروج از آن کاملاً در کد مدیریت‌شده بهینه‌سازی (Inline) می‌شود و نیازی به پرش مکرر به هسته موتور CLR Native نیست.
    • مدیریت بهینه وضعیت Contention: وقتی ریسمان‌ها منتظر می‌مانند، مکانیزم ثبت و صف‌بندی ریسمان‌های مسدودشده بدون درگیر کردن جدول سراسری SyncBlock انجام می‌شود که سربار حافظه پنهان پردازنده (L1/L2 Cache Miss) را به شدت کاهش می‌دهد.
    • صفر بایت تخصیص حافظه (Zero Allocation): با استفاده از ref struct Scope، کامپایلر هیچ گونه تخصیص حافظه‌ای در پشته یا هیپ تحمیل نمی‌کند.

    ۶. نکات مهم در مهاجرت بهLock
    - تعویض بسیار ساده: اگر در پروژه‌های دات‌نت ۹ به بالا هستید، کافی است تمام خطوط تعریف:
    private readonly object _gate = new();
    را به:
    private readonly Lock _gate = new();
    تغییر دهید. هیچ تغییری در بدنه متدها و دستورات lock (...) نیاز نیست.

    - همچنان فاقد پشتیبانی از await: به یاد داشته باشید که کلاس Lock نیز مانند Monitor کاملاً همگام است و در کدهای دارای await نمی‌توان از آن استفاده کرد. در سناریوهای ناهمگام، انتخاب درست همچنان SemaphoreSlim است.