بستار یا Closure در C# (قسمت دوم)
توسط: سيد منصور عمراني - سه شنبه ١٧ دي ١٣٩٢ ساعت ١٠:٣٢
|
|
|
برچسب ها:
C#
Closure
بستار
|
با وجودی که به نظر میرسد زبان C# از .NET 2.0 به بعد از بستار پشتیبانی میکند، اما واقعیت چنین نیست و زبان C# به هر حال یک زبان غیر تابعی یا non-functional است. چیزی که رخ میدهد این است که کامپایلر C# در پشت صحنه قابلیت بستار را شبیهسازی میکند. برای این کار کامپایلر برای بستار یک کلاس تعریف کرده و متد ناشناس مربوط به بستار را در آن قرار میدهد. سپس به ازای هر یک از متغیرهای بیرونی یک فیلد در این کلاس تعریف میکند. برای نمونه در خصوص مثال قبلی چنین کلاسی تولید میکند.
بستار در C# چگونه کار میکند؟
با وجودی که به نظر میرسد زبان C# از .NET 2.0 به بعد از بستار پشتیبانی میکند، اما واقعیت چنین نیست و زبان C# به هر حال یک زبان غیر تابعی یا non-functional است. چیزی که رخ میدهد این است که کامپایلر C# در پشت صحنه قابلیت بستار را شبیهسازی میکند. برای این کار کامپایلر برای بستار یک کلاس تعریف کرده و متد ناشناس مربوط به بستار را در آن قرار میدهد. سپس به ازای هر یک از متغیرهای بیرونی یک فیلد در این کلاس تعریف میکند. برای نمونه در خصوص مثال قبلی چنین کلاسی تولید میکند:
[CompilerGenerated] private sealed class <>c__DisplayClass1 { public int a; public voidb__0() { Console.WriteLine("The value of 'a' is " + a); } }
همچنین کُدی که کامپایلر برای برنامه تولید میکند بدین صورت است:
class Test { public Action Foo() { c__DisplayClass1 V_1 = new c_DisplayClass1(); V_1.a = 5; Action V_0 = new Action(V_1.b__0); return V_0; } public static void Main() { Action action = Foo(); action(); // writes: The value if 'a' is 5 } }
همان گونه که میبینید متد b__0() در کلاس c__DisplayClass1 فیلد a در همین کلاس را استفاده میکند که کاملا به آن دسترسی دارد. اما نکتهی بسیار مهمی در اینجا وجود دارد. برای متد Foo() دیگر متغیر محلی a تعریف نمیشود. با وجودی که متغیر V_1 که داخل Foo() تعریف شده یک متغیر محلی است، اما به دلیل این که یک نوع دادهی ارجاعی یا reference type است، در حافظهی هیپ (و نه پشته) تخصیص داده میشود. لذا بر خلاف متغیرهای محلی از جنس مقداری (value type) که بر روی پشته تعریف میشوند، موقع پایان اجرای متد Foo() و اتمام حوزهی دید این متد، شیء V_1 از حافظه پاک نمیشود. پاکسازی این شیء بر عهدهی آشغال جمعکن CLR است و زمانی این کار را انجام میدهد که اطمینان حاصل کند فرد دیگری به آن نیاز ندارد.
تفاوت بستار در C# با سایر زبانها
تعریف و پیادهسازی بستار در زبانهای برنامهنویسی مختلف ممکن است کمی متفاوت باشد. در برخی زبانها مانند ML بستارها مقدار متغیرهای بیرونی را تسخیر میکنند نه خود آنها را. یعنی یک کپی از مقدار متغیرهای بیرونی در قالب یک متغیر محلی مخفی که داخل خودشان تعریف شده در اختیار آنها قرار داده میشود. اما زبان C# چنین نیست و خود متغیرهای بیرونی به تسخیر بستار در میآیند. به عنوان مثال به کُد زیر توجه کنید:
int a = 1; Action closure = delegate { Console.WriteLine("{0} + 1 = {1}", a, a + 1); }; a = 10; closure();
به نظر شما بعد از اجرای این تکه کُد چه چیزی در خروجی نوشته میشود؟ ممکن است تصور کنید عبارت 1 + 1 = 2 نمایش داده میشود. در زبانهایی که بستارها در آنها، مقدار متغیرهای بیرونی و نه خود متغیرهای بیرونی را استفاده میکنند همین مساله رخ میدهد. اما زبان C# چنین نیست و closure خود متغیر بیرونی a را تسخیر میکند. لذا چیزی که در خروجی نمایش داده خواهد شد 10 + 1 = 11 است (زیرا ابتدا دستور نسبتدهی a = 10 و سپس دستور فراخوانی بستار اجرا میشود).
در برخی زبانها مانند PHP نیز در دو حالت تسخیر مقدار متغیر بیرونی یا خود آن وجود دارد و برنامهنویس خودش میتواند مشخص کند چه چیزی به تسخیر بستار در بیاید.
پیامدها
بستار قدرت زیادی در برنامهنویسی فراهم میکند، اما در عین حال زمینهی پیچیدگی و اشتباهات و خطاهای بزرگی را به ویژه در برنامههای چندنخی و موازی مهیا میکند. به عنوان مثال به کُد زیر توجه کنید:
for (var i = 1; i <= 5; i++) { new Thread(() => Console.WriteLine(i)).Start(); }
در این مثال ساده، در هر تکرار حلقهی for یک نخ ایجاد میکنیم که به طور ساده مقدار متغیر حلقه را در خروجی مینویسد. با وجودی که ممکن است انتظار داشته باشید در خروجی، اعداد 1 تا 5 را مشاهده کنید، چنین چیزی محقق نمیشود. برای نمونه خروجی زیر یکی از حالتهای مختلف خروجی این تکه کُد است:
2
2
4
4
5
مساله دقیقا از همان خاصیت تسخیر متغیرهای بیرونی و نه مقدار آنها در C# نشات میگیرد. در واقع پیش از آن که برخی نخها بتوانند اجرا شوند، حلقهی for پیش رفته و شمارندهاش را افزایش میدهد. لذا مقداری که نخها نمایش میدهند لزوما با مقدار شمارندهی حلقه در زمانی که آنها ایجاد شدهاند یکسان نیست.
برای بر طرف کردن مشکل کُد بالا دو راه وجود دارد:
· داخل حلقهی for یک متغیر محلی تعریف کنیم، شمارنده را در آن کپی کرده و داخل نخها، مقدار متغیر محلی جدید را نمایش بدهیم:
for (var i = 1; i <= 5; i++) { int temp = i; new Thread(() => Console.WriteLine(temp)).Start(); }
در این حالت در هر تکرار حلقه، یک نسخهی جدید از متغیر محلی temp ایجاد میشود که به تسخیر نخی که حلقه در تکرار خود ایجاد کرده در میآید. در واقع اگر از قسمت قبل به یاد داشته باشید، کامپایلر اساسا متغیر temp را به صورت محلی روی پشته ایجاد نمیکند. بلکه آن را به صورت یک فیلد، در کلاس مخفی بستار نخها تعریف میکند. از آنجایی که هر بستار، با نمونهسازی از این کلاس مخفی اجرا میشود، پنج نسخه از temp وجود خواهد داشت.
· روش بالا با وجود درست بودنش، کمی گنگ است. زیرا رفتار برنامه با آنچه در ظاهر دیده میشود کمی فرق دارد. در واقع، برنامه پیچیده شده و زمینهی بروز خطاهای منطقی انسانی فراهم میشود. راه دیگر و بهتر این مثال به خصوص این است که از نسخهی دیگری از سازندهی کلاس Thread استفاده کنیم که در آن، تابع مورد اجرا توسط نخ میتواند پارامتر بپذیرد.
for (var i = 1; i <= 5; i++) { new Thread((n) => Console.WriteLine(n)).Start(i); }
در اینجا از سازندهی Thread(ParametherizedThreadStart action) برای ایجاد نخها استفاده کردهایم. سپس موقع اجرای نخها و فراخوانی متد Start() آنها، شمارنده را به متد Start() پاس میدهیم. در این حالت کپی شمارنده به دست بستار نخها میرسد. لذا برنامه به درستی کار میکند (البته باز هم تاکید میکنم. توابع ناشناس این نخها دیگر بستار نیست. زیرا از متغیر بیرونی استفاده نکرده است).
روش دوم یک مزیت دیگر هم دارد. از آنجایی که بستار نخها از متغیر بیرونی استفاده نکرده و چیزی را به تسخیر خود در نمیآورند، کامپایلر دیگر کلاسی برای آنها ایجاد نمیکند. در نتیجه برای اجرای بستار، شیای در حافظه تخصیص داده نمیشود که قرار باشد بعدا توسط آشغال جمعکن پاکسازی شود. این مساله تاثیر و بهبود خود را از نظر سرعت اجرا در تکرار بسیار زیاد حلقهی for نشان میدهد. به کُدهای زیر و زمان اجرای آنها توجه کنید:
Code A
int MAX = 1000; Task[] t = new Task[MAX]; Stopwatch sw = new Stopwatch(); sw.Start(); for (var i = 0; i < MAX; i++) { t[i] = Task.Factory.StartNew(() => Consume(i)); } Task.WaitAll(t); sw.Stop(); Console.WriteLine(sw.Elapsed.TotalSeconds); // OUTPUT: // 0.1236375
Code B
int MAX = 1000; Task[] t = new Task[MAX]; Stopwatch sw = new Stopwatch(); sw.Start(); for (var i = 0; i < MAX; i++) { t[i] = Task.Factory.StartNew((x) => Consume((int)x), i); } Task.WaitAll(t); sw.Stop(); Console.WriteLine(sw.Elapsed.TotalSeconds); // OUTPUT: // 0.0167616
در اینجا به جای Thread از Task که در .NET 4.0 در کتابخانهی TPL معرفی شد استفاده کردهایم. همان گونه که میبینید Code B به دلیل این که متد ناشناس تحویل داده شده به Task ها از متغیر بیرونی استفاده نمیکند، حدود 8 برابر سریعتر از کُد قبلی اجرا شده است. به منظور آشنایی بیشتر با کتابخانهی TPL در .NET 4.0 و برنامهنویسی موازی و غیر همزمان (Asnc Programming) میتوانید به کتاب برنامه نویسی وظیفهای در .NET 1.0 - 4.0 با استفاده از TPL و PLINQ از همین نویسنده مراجعه کنید.
این روش نه فقط در حلقههای با تکرار زیاد، بلکه در برنامههایی مانند برنامههای وب که زیر بار قرار میگیرند بسیار موثر است. زیرا به دلیل کاهش تعداد اشیاء موجود در حافظه، کمتر وقت آشغال جمعکن را میگیرد. توجه کنید آنچه مهمتر است صرفهجویی در وقت آشغالجمع کن است، نه صرفهجویی در مصرف حافظه. زیرا حتی اگر یک میلیون Task هم ایجاد شود، اشیاء اضافی ایجاد شده ناشی از بستار آنقدرها حافظه مصرف نمیکند که بخواهد فشاری به سرور وارد کند.
توصیه میشود همیشه از روش دوم که هم کارایی بهتری دارد و هم خواناتر بوده و زمینهی خطای انسانی کمتری فراهم میکند استفاده کنید.
جمعبندی
در این مقاله به بررسی مفهوم بستار در زبان C# پرداختیم. ابتدا تعریف و پیشینهی بستار را دیدیم. سپس نحوهی تعریف بستار در C# را با استفاده از متدهای ناشناس و عبارتهای لامبدا مشاهده کردیم. پس از آن کارکرد متغیرهای بیرونی و به تسخیر در آمدن آنها توسط بستار در C# را دیدیم. در نهایت پس از مرور مختصر نحوهی پیادهسازی بستار در زبان C# توسط کامپایلر، پیامدهای بستار را بیان کردیم. البته مفهوم بستار از نظر علمی مفصلتر است. جهت کسب اطلاعات بیشتر در خصوص مفهوم بستار میتوانید به منبع نخست این مقاله مراجعه کنید.
منابع
1. http://en.wikipedia.org/wiki/Closure_(computer_programming)
2. http://www.blackwasp.co.uk/CSharpClosures.aspx
3. http://www.codethinked.com/c-closures-explained
4. http://diditwith.net/PermaLink,guid,235646ae-3476-4893-899d-105e4d48c25b.aspx
سيد منصور عمراني
١٣٩٤/٠٣/٣٠ ساعت ١٦:٠٦ this is a test 2
|
ثبت نظر جدید | |
ثبت |