در مهندسی نرمافزار، مدیریت منابع (Memory و CPU) یکی از مهمترین دغدغههای توسعهدهندگان است. الگوی Lazy Initialization (مقداردهی اولیه تنبل یا تاخیری) تکنیکی است که در آن ساخت یک شیء، محاسبه یک مقدار یا اجرای یک فرآیند سنگین، تا زمانی که واقعاً به آن نیاز پیدا نشود، به تعویق میافتد.
تا پیش از معرفی داتنت فریمورک ۴.۰، برنامهنویسان مجبور بودند این الگو را به صورت دستی و با استفاده از کدهای پیچیده (مثل بررسی null بودن شیء و استفاده از قفلهای lock برای محیطهای چندنخی) پیادهسازی کنند. اما مایکروسافت با معرفی کلاس <Lazy<T>، این فرآیند را استاندارد، ایمن و بسیار ساده کرد.
کلاس <Lazy<T> یک کلاس Generic در فضای نام System است. وظیفه اصلی آن این است که یک لفاف (Wrapper) دور شیء شما ایجاد کند تا آن شیء تنها زمانی نمونهسازی (Instantiate) شود که برای اولین بار به خاصیت .Value آن دسترسی پیدا کنید.
ساختار پایه:
هنگام تعریف یک متغیر از نوع <Lazy<T>، شما مشخص میکنید که T چه نوع دادهای است. به صورت پیشفرض، <Lazy<T> از سازنده (Constructor) بدون پارامترِ کلاس T برای ساخت آن استفاده میکند. اما معمولاً توسعهدهندگان از یک Delegate (مانند Func<T>) برای تعیین دقیق نحوه ساخت شیء استفاده میکنند.
// تعریف یک شیء به صورت تنبل
Lazy<MyHeavyClass> lazyObject = new Lazy<MyHeavyClass>(() => new MyHeavyClass());
// در این نقطه، MyHeavyClass هنوز ساخته نشده است!
// اولین فراخوانی: شیء در اینجا ساخته میشود
MyHeavyClass actualObject = lazyObject.Value;
// فراخوانیهای بعدی: شیء از قبل ساخته شده بازگردانده میشود
MyHeavyClass sameObject = lazyObject.Value;
کلاس <Lazy<T> دو خاصیت بسیار مهم دارد:
Value: این خاصیت، مقدار یا شیء اصلی را برمیگرداند. اگر شیء قبلاً ساخته نشده باشد، ابتدا آن را میسازد (اجرای delegate) و سپس برمیگرداند. اگر قبلاً ساخته شده باشد، همان نمونهی کششده (Cached) را در کسری از ثانیه برمیگرداند.
IsValueCreated: یک خاصیت boolean است. اگر شیء قبلاً مقداردهی شده باشد true و در غیر این صورت false برمیگرداند. این خاصیت برای بررسی وضعیت بدون تحریک کردن شیء برای ساخته شدن، بسیار مفید است.
استفاده از این کلاس در سناریوهای زیر به شدت توصیه میشود:
اشیاء سنگین (Heavy Objects): زمانی که ساخت یک شیء نیازمند پردازش بالا، خواندن فایلهای حجیم از دیسک، یا دریافت داده از شبکه/دیتابیس است، اما مشخص نیست که آیا کاربر در طول اجرای برنامه واقعاً به آن نیاز پیدا میکند یا خیر.
بهینهسازی زمان اجرای اولیه (Startup Time): اگر در زمان لود شدن برنامه (مثلاً در یک اپلیکیشن دسکتاپ یا یک سرویس وب) دهها شیء سنگین ساخته شوند، برنامه کند بالا میآید. با <Lazy<T> میتوان ساخت آنها را به زمان استفاده موکول کرد.
مدیریت وابستگیهای حلقوی (Circular Dependencies): در برخی معماریها (مانند تزریق وابستگی - DI)، دو سرویس ممکن است به یکدیگر نیاز داشته باشند. استفاده از <Lazy<T> میتواند به شکستن این حلقه در زمان مقداردهی اولیه کمک کند.
یکی از قدرتمندترین ویژگیهای <Lazy<T> مدیریت فوقالعادهی آن در برابر همزمانی (Concurrency) است. در یک برنامه Multi-thread، ممکن است چند ریسمان (Thread) همزمان تلاش کنند به Value دسترسی پیدا کنند. <Lazy<T> با استفاده از LazyThreadSafetyMode سه حالت مختلف را برای مدیریت این شرایط ارائه میدهد:
این امنترین حالت است. داتنت از یک قفل (Lock) داخلی استفاده میکند تا تضمین کند تابع مقداردهی اولیه فقط و فقط یک بار اجرا میشود. سایر Threadها منتظر میمانند تا Thread اول کارش تمام شود و سپس همگی همان شیء ساخته شده را دریافت میکنند.
کاربرد: زمانی که ساخت شیء بسیار گران است و به هیچ وجه نباید دو بار ساخته شود.
در این حالت، هیچ قفلی روی اجرای تابعِ مقداردهی وجود ندارد. بنابراین اگر چند Thread همزمان به Value دسترسی پیدا کنند، ممکن است هر کدام نمونهی خودشان را بسازند. اما در نهایت، اولین Threadای که کارش تمام شود، نمونهاش را در <Lazy<T> ثبت (Publish) میکند. بقیه نمونههای ساخته شده توسط سایر Threadها دور ریخته میشوند (و به Garbage Collector سپرده میشوند).
کاربرد: زمانی که منتظر ماندن Threadها (Lock Contention) هزینه بیشتری نسبت به ساخت چندبارهی شیء داشته باشد و شیء ما اثرات جانبی (Side Effects) در هنگام ساخت نداشته باشد.
هیچگونه ایمنی ریسمانی (Thread Safety) وجود ندارد. اگر چند Thread همزمان به آن دسترسی پیدا کنند، رفتار برنامه غیرقابل پیشبینی خواهد بود (ممکن است Exception رخ دهد یا دادهها خراب شوند).
کاربرد: منحصراً برای زمانی که مطمئن هستید برنامه شما کاملاً Single-Threaded است. این حالت بالاترین پرفورمنس را دارد زیرا هیچ سربارِ قفلگذاری (Locking Overhead) در آن نیست.
// مثال تعیین Thread Safety
Lazy<MyClass> lazySafe = new Lazy<MyClass>(
() => new MyClass(),
LazyThreadSafetyMode.PublicationOnly
);
یکی از نکات ظریف و بسیار مهم در <Lazy<T> نحوه برخورد آن با خطاها (Exceptions) است.
اگر در زمان اجرای تابعِ سازنده (Delegate)، یک استثنا رخ دهد، این استثنا کش (Cache) میشود. به این معنی که اگر در آینده دوباره سعی کنید Value را فراخوانی کنید، <Lazy<T> دوباره تلاش نمیکند شیء را بسازد، بلکه دقیقاً همان خطای قبلی را دوباره پرتاب (Throw) میکند.
نکته تخصصی: این رفتار در حالت ExecutionAndPublication رخ میدهد. اگر از حالت PublicationOnly استفاده کنید، استثناها کش نمیشوند و در فراخوانی بعدی، <Lazy<T> دوباره شانس خود را برای ساخت شیء امتحان میکند.
الگوی سینگلتون (Singleton) تضمین میکند که از یک کلاس فقط یک نمونه ساخته شود. از <Lazy<T> میتوان برای پیادهسازی بسیار تمیز و Thread-Safe الگوی سینگلتون استفاده کرد:
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy =
new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance { get { return lazy.Value; } }
private Singleton() { } // سازنده خصوصی
}
داتنت کلاس دیگری به نام LazyInitializer در فضای نام System.Threading دارد. تفاوت آنها این است که <Lazy<T> یک شیء جدید تخصیص میدهد (Allocation) و وضعیت را درون خود نگه میدارد. اما LazyInitializer از متدهای static استفاده میکند تا متغیرهای موجود را به صورت تنبل مقداردهی کند و سربار (Overhead) ساخت یک شیء Wrapper اضافی را ندارد. استفاده از LazyInitializer پیچیدهتر است اما در سناریوهای به شدت حساس به حافظه (Micro-optimization) کاربرد دارد.
کلاس <Lazy<T> در داتنت یک ابزار ضروری برای نوشتن کدهای بهینه، تمیز و کارآمد است. با به تعویق انداختن تخصیص منابع تا لحظه نیاز واقعی، به طور چشمگیری باعث کاهش مصرف حافظه و افزایش سرعت اجرای اولیه برنامهها میشود. شناخت دقیق حالتهای Thread Safety آن به توسعهدهندگان این قدرت را میدهد تا برنامههای چندنخی و مقیاسپذیر را با کمترین میزان باگ و بنبست (Deadlock) طراحی کنند. استفاده هوشمندانه از این الگو، نشاندهنده بلوغ و تخصص یک توسعهدهنده در اکوسیستم .NET است.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.