Garbage Collector در CSharp - قسمت اول



Garbage Collection

Garbage Collection

فرض کنید متغییری را ایجاد کرده و به آن مقدار داده ‎‎اید:

string message = "Hello World!";

آیا تابحال به این موضوع فکر کرده اید که طول عمر متغییر message تا چه زمانی است و چه زمانی باید از بین برود؟
چه زمانی باید توسط کامپایلر ( یا بهتر بگوییم ، Runtime ) طول عمر این متغییر به پایان برسد و از حافظه حذف شود؟


زبان‎‌های برنامه نویسی به دو دسته Managed و Unmanaged تقسیم میشوند:

  1. Unmanaged: در این دسته از زبان‎ ها، وظیفه ایجاد اشیا، تشخیص زمان درست برای از بین بردن و از بین بردن آنها، برعهده شماست. زبان‌های C و ++C جز این نوع زبان‌ها میباشند.

  2. Managed: در این دسته از زبان ها، ایجاد اشیا مانند قبل بر عهده شماست، اما وظیفه تشخیص و از بین بردن آنها در زمان درست را Runtime برعهده میگیرد.
    در این نوع زبان ‎ها ما دغدغه حذف متغییری که در چندین خط بالاتر آن را ایجاد کرده و به آن مقدار داده‎، به چندین متد آن را پاس داده و انواع Manipulation را روی آن انجام داده‎ایم را نداریم. زبان‌های #C و Java جز این نوع زبان‌ها میباشند.

ایده کلی ایجاد زبان‌های Managed بر این است که شما درگیر مباحث مربوط به Memory Management نشوید و تمرکز اصلیتان روی Business باشد.

اکثر پروژه‌ها به اندازه کافی پیچیدگی‌های Business ای دارند و ترکیب کردن این نوع پیچیدگی‌ها با پیچیدگی‌های Low Level و Technical ای همچون مباحث Memory Management، در اکثر اوقات باعث میشود که نگهداری از پروژه کاری بسیار دشوار شده و به تدریج مانند بسیاری از پروژه‌های دیگر، نام Legacy را با خود به یدک بکشد.

این بدان معنی نیست که پروژه هایی که با زبان هایی همچون C و ++C نوشته میشوند، از همان روز اول Legacy بوده و Maintainable نیستند، بلکه بدین معناست که نگهداری کد چنین زبان هایی نسبت به زبان‌های Managed به مراتب مشکل‌تر است و همچنین قابل ذکر است که Maintainability یک پروژه به مباحث بسیار زیاد دیگری نیز بستگی دارد.

Legacy Code

نمونه ای از ترکیب این نوع پیچیدگی‌ها را با مثالی بسیار ساده در دو زبان C و #C بررسی میکنیم.

برنامه ای داریم که یک متغییر عددی از نوع int ایجاد میکند، عدد 25 را بعنوان مقدار به آن میدهد و سپس این متغییر به یک متد پاس داده میشود تا مقدارش چاپ شود.

کد این برنامه در زبان C :

#include <stdio.h>
#include <stdlib.h>

void printReport(int* data)
{
    printf("Report: %d", *data);
}

int main(void)
{
    int *myNumber;
    myNumber = (int*)malloc(sizeof(int));
    if (myNumber == 0)
    {
        printf("ERROR: Out of memory");
        return 1;
    }

    *myNumber = 25;
    printReport(myNumber);

    free(myNumber);
    myNumber = NULL;

    return 0;
}

هدف” و Business اصلی این برنامه، چاپ و یک گزارش ساده بود، اما مسائل بسیار بیشتری در این مثال دخیل شده اند:

1- Allocate و دریافت فضای مورد نیاز برای یک عدد ( int ) از Memory ، توسط تابع malloc

2- Cast کردن مقدار برگشت داده شده ( *void ) به type مدنظرمان یعنی *int

3- نگهداری آدرس متغییر allocate شده داخل یک pointer

4- بررسی موفقیت آمیز بودن عمل allocation روی memory ( اگر حافظه فضای کافی نداشته باشد، عدد 0 بعنوان آدرس و به معنای عدم موفقیت بازگردانده میشود. )

5- مقداردهی متغییر allocate شده با عدد 25

6- صدا زدن و پاس دادن pointer متغییر myNumber به تابع printReport

7- خالی کردن مقدار allocate شده متغییر myNumber توسط تابع free در زمانی که دیگر به آن در برنامه نیازی نیست.

8- مقداردهی NULL به myNumber ( جهت جلوگیری از مشکلات Dangling Pointers )

چند مرحله از این مراحل ذکر شده واقعا نیاز Business ای برنامه ما بود؟ این مثال بسیار ساده و غیر واقعی بود اما تصور کنید با این روش، یک برنامه بزرگ با Business Rule‌ها و پیچیدگی‌های خودش، چه حجمی از کد و پیچیدگی را خواهد داشت.

کد این برنامه در زبان #C :

using System;

public class Program
{
    public static void Main()
    {
        int myNumber = 25;
        PrintReport(myNumber);
    }

    private static void PrintReport(int number)
    {
        Console.WriteLine($"Report: {number}");
    }
}

همانطور که میبینید در اینجا تمرکزمان روی هدف اصلی و Business است و درگیر پیچیدگی مباحث جانبی نظیر Manual Memory Management نشده ایم و Runtime زبان #C یعنی CoreCLR وظیفه Memory Management را در پشت صحنه برعهده گرفته است.


Transmission

تفاوت بین زبان‌های Managed و Unmanaged را میتوانیم به رانندگی با ماشین دنده ای و اتومات تشبیه کنیم.

اکثر اوقات هدف اصلی رانندگی، رفتن از یک مبدا به یک مقصد است. با استفاده از یک ماشین دنده ای، علاوه بر هدف اصلی یعنی رسیدن به یک مقصد، ذهن ما درگیر تعویض دنده در سرعت‎ مناسب در طول مسیر میشود و اینکار را ممکن است بیش از صدها یا هزاران بار انجام دهیم. در این روش طبیعتا ما کنترل بیشتری داریم و در بعضی مواقع بسیار بهتر به نسبت یک سیستم خودکار عمل میکنیم، اما از هدف اصلی خود یعنی رفتن از نقطه A به B دور شده ایم.

در سوی دیگر، با استفاده از یک ماشین اتومات، تمام تمرکز ما روی هدف اصلیمان یعنی رسیدن از یک مبدا به یک مقصد است. درگیر عوض کردن چندین باره دنده در طول یک مسیر نیستیم و این وظیفه را یک Engine خارجی برعهده گرفته است. هرچند که این روش، روش راحتتری نسبت به روش دستی و Manual است اما طبیعتا کنترل شما در این روش نسبت به روش قبل، کمتر است.


در زبان‎های Managed و Unmanaged هم دقیقا چنین تفاوت هایی وجود دارد.


در زبان‏‎های Unmanaged، شما کنترل کاملی روی طول عمر اشیا و مدیریت حافظه دارید و همه چیز برعهده شماست، اما علاوه بر هدف اصلیتان، درگیر مباحث جانبی دیگری نیز شده اید. اکثر اوقات قدرت زبان‏‎های Unmanaged را در Game Engine‌ها و Real-Time Processing System‌ها میتوانید ببینید که در آنها مدیریت حافظه بصورت دستی انجام میشود و برنامه نویس‎های آن سیستم، تعیین کننده اصلی این هستند که طول عمر اشیا تا چه زمانی باشد و چه زمانی از بین بروند که باعث اختلال یا کندی یک سیستم حتی برای چند میلی ثانیه نشوند.

در زبان‌های Managed، اکثر اوقات حتی نیازی نیست که شما درگیر مباحث جانبی مدیریت حافظه شوید و تمام کار را Runtime شما بصورت خودکار انجام میدهد، اما گاهی اوقات لازم است که قسمت‌های حساس برنامه ( اصطلاحا Hot-Path‌ها ) خود را پیدا کنید، از این قسمت‌ها Benchmark گرفته و مطمئن شوید که با حجم تعداد بالای درخواست و بار، به خوبی عمل میکند و همچنین قسمتی از برنامه، نشستی حافظه ( اصطلاحا Memory Leak ) ندارد. همانطور که گفتیم، گاهی اوقات یک انسان ( سیستم دستی ) بهتر از یک سیستم خودکار میتواند تصمیم بگیرد که در یک لحظه چه اتفاقی داخل برنامه رخ دهد.

در این سری مقالات قصد داریم وارد مبحث Memory Management در #C شده، با Garbage Collector آشنا و دیدی کلی از نحوه انجام کار آن داشته باشیم.