در برنامهنویسی همزمان، زمانی که چندین ریسه (Thread) برای دسترسی به منابع مشترک نیاز به قفل کردن چندین mutex دارند، خطر بروز بنبست (Deadlock) وجود دارد. بنبست وضعیتی است که در آن هر یک از ریسهها منتظر آزاد شدن قفلی هستند که توسط ریسهی دیگر در دست گرفته شده است و هیچیک نمیتوانند به کار خود ادامه دهند. کلاسیکترین مثال این وضعیت، زمانی رخ میدهد که دو ریسه همزمان سعی میکنند دو mutex را به ترتیب متفاوتی قفل کنند و هر کدام موفق به قفل کردن یکی از آنها میشوند و برای قفل کردن دومی منتظر میمانند. این مسئله در سیستمهای همزمان، یکی از چالشبرانگیزترین و سختترین خطاها برای ردیابی و رفع به شمار میرود .
معرفی std::scoped_lock
C++17 برای حل این مشکل، کلاس std::scoped_lock را در هدر <mutex> معرفی کرده است . این کلاس یک قفلگیر RAII (Resource Acquisition Is Initialization) است که میتواند یک یا چند mutex را بهطور همزمان مدیریت کند . مزیت اصلی scoped_lock نسبت به ابزارهای قدیمیتر مانند std::lock_guard، توانایی قفل کردن چندین mutex بهصورت یکجا و با استفاده از الگوریتمهای جلوگیری از بنبست است .
الگوریتم جلوگیری از بنبست
هنگامی که یک شیء از نوع std::scoped_lock
با چندین mutex ساخته میشود، در سازندهی آن، از یک الگوریتم هوشمند برای
قفل کردن تمام mutexها استفاده میشود که دقیقاً معادل تابع std::lock عمل میکند .
این الگوریتم، با استفاده از یک روش داخلی که ممکن است شامل چرخش و تلاش
مجدد باشد، تضمین میکند که در نهایت همهی قفلها بدون ایجاد بنبست گرفته
شوند. حتی اگر ترتیب دریافت mutexها در فراخوانیهای مختلف متفاوت باشد،
این الگوریتم از بروز وضعیت منتظر ماندن دایرهای جلوگیری میکند .
عملکرد RAII و مدیریت خودکار
معماری scoped_lock بر اساس اصول RAII طراحی شده است که مدیریت منابع را بهشدت ساده و ایمن میکند. با ایجاد یک شیء scoped_lock، تمام mutexهای داده شده به آن قفل میشوند. با خروج شیء از محدودهی تعریف (Scope)، دستور تخریبکننده (Destructor) بهطور خودکار فراخوانی شده و تمام mutexها را آزاد میکند .
این سازوکار، آزادسازی قفلها را حتی در صورت پرتاب شدن استثنا
(Exception) تضمین میکند و از بروز قفلهای سرگردان (Orphaned Locks)
جلوگیری مینماید .
نحوهی استفاده و سازندهها
استفاده از std::scoped_lock
بسیار ساده و شهودی است. در سادهترین حالت، شیء را با ارسال mutexهای
مورد نظر به سازنده میسازیم و از آن پس تا پایان محدوده، قفلها در اختیار
ما هستند . این کلاس همچنین یک سازندهی دوم با پارامتر std::adopt_lock_t دارد که برای مواقعی که قفلها از قبل توسط ریسه گرفته شدهاند و فقط نیاز به انتقال مالکیت به شیء scoped_lock برای آزادسازی خودکار داریم، کاربرد دارد . البته در بسیاری از موارد، نیازی به این سازنده نیست و همان سازندهی اصلی کار را بهخوبی انجام میدهد.
مقایسه با std::lock_guard
ابزار std::lock_guard
که از C++11 وجود داشته، تنها توانایی مدیریت یک mutex را دارد و برای قفل
کردن چندین mutex، برنامهنویس باید بهصورت دستی از ترکیب std::lock و lock_guard استفاده میکرد . std::scoped_lock در C++17 بهعنوان یک جایگزین برتر معرفی شده است که هم از std::lock_guard سادهتر و هم ایمنتر است . حتی ابزارهای تحلیل کد مانند clang-tidy نیز تغییر از lock_guard به scoped_lock را پیشنهاد میدهند . بهعنوان یک قاعدهی کلی، اگر نیاز به قفل کردن بیش از یک mutex دارید، scoped_lock انتخاب ایدهآل است و حتی برای یک mutex نیز میتوان از آن استفاده کرد، هرچند lock_guard همچنان کاربردی است .
نکات مهم و خطاهای رایج
یک اشتباه رایج در استفاده از scoped_lock (و همچنین lock_guard)، نامگذاری نکردن شیء ساخته شده است . نوشتن کدی مانند std::scoped_lock(mtx); بهجای std::scoped_lock lock(mtx); باعث میشود یک شیء موقت ساخته شده و بلافاصله نابود شود و در نتیجه، mutex اصلاً قفل نمیگیرد . همچنین در شرایط نادر که نیاز به باز کردن یکی از قفلها زودتر از دیگری دارید، ممکن است همچنان به ترکیب std::lock با std::unique_lock نیاز داشته باشید، زیرا scoped_lock امکان باز کردن جداگانهی قفلها را نمیدهد .
کلیدواژه ها : std::scoped_lock-جلوگیری از بنبست در C++-RAII در برنامهنویسی همزمان-C++17 multithreading-مدیریت قفل در C++-std::lock-مقایسه scoped_lock و lock_guard-قفل چندگانه در C++-std::adopt_lock-پیشگیری از deadlock-آموزش std::scoped_lock-همزمانی در C++-نکات std::scoped_lock-بهبود عملکرد همزمانی-مدیریت منابع در C++