چیستی و چرایی مدیریت خودکار حافظه
Garbage Collection در پایتون چیست و چگونه کار می‌کند؟
پایتون یکی از محبوب‌ترین زبان‌های برنامه‌نویسی است که برای ساخت پروژه‌های مختلفی از آن استفاده می‌شود. این زبان در سال 2021 به دلیل استقبال عمومی در جایگاه سوم فهرست وب‌سایت TIOBE قرار گرفت. سهولت استفاده و پشتیبانی توسط جامعه بزرگی از برنامه‌نویسان پایتون، آن‌را به گزینه مناسبی در ارتباط با علم داده‌ها، ساخت برنامه‌های کاربردی وب‌محور و غیره تبدیل کرده است. یکی از مفاهیم مهمی که پیرامون زبان برنامه‌نویسی پایتون قرار دارد و کمتر به آن پرداخته شده، مفهوم « جمع‌آوری زباله» (Garbage Collection) است. یک برنامه‌نویس زبان پایتون باید شناخت دقیقی در ارتباط با اصول مدیریت حافظه و چرایی جمع‌آوری زباله در این زبان داشته باشد. بر همین اساس در این مقاله با نحوه کارکرد مکانیزم جمع‌آوری زباله در پایتون آشنا خواهیم شد و در ادامه به معرفی نکاتی خواهیم پرداخت که هنگام نوشتن برنامه‌های پایتون و استفاده از ویژگی مذکور باید به آن دقت کنید.

shabake-mag.jpg

جمع‌آوری زباله در پایتون چیست و چرا به آن نیاز داریم؟

اگر پایتون اولین زبان برنامه‌نویسی شما است، ممکن است اطلاع چندانی در مورد مفهوم جمع‌آوری زباله نداشته باشید، بنابراین بهتر است کار را با اصول اولیه آغاز کنیم.

مطلب پیشنهادی

شبکه منتشر کرد: کتاب الکترونیکی دوره مقدماتی آموزش پایتون
برای آن‌ها که از برنامه‌نویسی هیچ نمی‌دانند (و می‌دانند)

مدیریت حافظه

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

مقادیر اشیایی که برنامه کاربردی شما از آن‌ها استفاده می‌کند برای دسترسی سریع در حافظه ذخیره می‌‌شوند. در بیشتر زبان‌های برنامه‌نویسی، متغیر تعریف‌شده در یک برنامه، اشاره‌گری به آدرس یک شیء در حافظه است. هنگامی که متغیری در یک برنامه استفاده می‌شود، پردازه مربوط به برنامه کاربردی مقداری را که متغیر در حافظه به آن اشاره دارد خوانده و عملیاتی روی آن انجام می‌دهد. 

در زبان‌های برنامه‌نویسی اولیه، مسئولیت مدیریت حافظه مورد استفاده توسط برنامه کاربردی بر عهده برنامه‌نویسان بود. به‌طوری‌که قبل از ایجاد یک لیست یا یک شیء ابتدا باید حافظه برای متغیر مشخص می‌شد و پس از اتمام کار با متغیر از حافظه پاک می‌شد تا حافظه تخصیص داده‌شده آزاد شود. در صورت عدم انجام این‌کار، برنامه کاربردی به‌سرعت حافظه آزاد سیستمی را مصرف می‌کرد و عملکرد سیستم به‌دلیل عدم وجود حافظه آزاد با افت شدیدی روبه‌رو می‌شد. به‌طور مثال، در زبان برنامه‌نویسی سی هنگامی که اشاره‌گرهایی به‌شکل پویا تعریف می‌شوند باید پس از اتمام کار آن‌ها را پاک کنید تا حافظه مورد استفاده توسط اشاره‌گرها آزاد شده و به سیستم بازگردد. 

عدم آزادسازی حافظه چه پیامدهایی دارد؟ 

اگر پس از اتمام استفاده از حافظه اصلی سیستم آن‌را آزاد نکنید، مشکل «نشت حافظه» (Memory Leak) به‌وجود می‌آید. این مشکل به‌مرور زمان باعث می‌شود تا برنامه کاربردی بیش‌ازاندازه از حافظه استفاده کند و اگر برنامه برای مدت زمان طولانی استفاده شود مشکلات امنیتی به‌وجود می‌آورد. در این حالت، برنامه کاربردی هنوز در حافظه است و برای انجام برخی فعالیت‌ها به حافظه نیاز دارد، اما هیچ حافظه رزروشده‌ای در دسترس برنامه نیست.

مشکل دیگری که وجود دارد، آزادسازی اشتباه حافظه تخصیص‌داده‌شده است. به بیان دقیق‌تر، اگر حافظه‌ای که توسط برنامه کاربردی استفاده شده، بدون تمهیدات خاصی آزاد شود، بازهم مشکلاتی برای برنامه کاربردی ایجاد می‌کند. این مسئله دو مشکل بزرگ به‌وجود می‌آورد؛ برنامه ممکن است از حرکت بازایستد یا داده‌های مورد استفاده توسط برنامه کاربردی خراب شوند. متغیری که به حافظه آزادشده اشاره کند، «اشاره‌گر آویزان» (Dangling Pointer) نامیده می‌شود. مشکلات این‌چنینی باعث شدند تا شرکت‌ها به‌فکر راهکارهای جدیدی برای مدیریت خودکار حافظه در زبان‌های برنامه‌نویسی مدرن باشند؛ رویکردی که در نهایت باعث شد فناوری «مدیریت خودکار حافظه و جمع‌آوری زباله» ابداع شود. 

مطلب پیشنهادی

۱۰ سایت آموزش رایگان پایتون به زبان فارسی
پایتون، یک زبان همه‌کاره و محبوب

مدیریت خودکار حافظه و جمع‌آوری زباله

با مدیریت خودکار حافظه، برنامه‌نویسان دیگر نیازی به مدیریت حافظه ندارند و مولفه‌ای که «زمان اجرا» (Runtime) نام دارد این فرآیند را مدیریت می‌کند. روش‌های مختلفی برای مدیریت خودکار حافظه وجود دارد. محبوب‌ترین روش «شمارش مرجع» (Reference Counting) است. در روش شمارش مرجع، مولفه زمان اجرا تمام ارجاعات به یک شیء را ردیابی می‌کند. هنگامی‌که یک شیء هیچ ارجاعی نداشته باشد، توسط مولفه زمان اجرا «غیرقابل استفاده» برچسب‌گذاری می‌شود تا امکان حذف آن وجود داشته باشد. 

مدیریت خودکار حافظه چند مزیت مهم در اختیار برنامه‌نویسان قرار می‌دهد. برنامه‌نویسان می‌توانند بدون آن‌که مجبور شوند روی جزئیات سطح پایین حافظه متمرکز شوند، تنها روی منطق تجاری برنامه کار کنند. مدیریت خودکار حافظه راهکاری کارآمد برای پیشگیری از بروز مشکل نشت حافظه و اشاره‌گرهای آویزان است. 

با این حال، مدیریت خودکار حافظه هزینه دارد. برنامه شما باید از حافظه و محاسبات اضافی برای ردیابی همه مراجع استفاده کند. علاوه بر این، بیشتر زبان‌های برنامه‌نویسی برای بهره‌مندی از تکنیک مدیریت خودکار حافظه باید از یک فرآیند stop-the-world برای جمع‌آوری زباله استفاده کنند. در این فرآیند، برخی عملیات اجرایی به‌شکل موقت متوقف می‌شوند تا مولفه جمع‌کننده زباله بتواند به‌شکل دقیقی اشیاء بدون استفاده را شناسایی و جمع‌آوری کند. 

به‌لطف قانون مور و پیشرفت‌های انجام‌شده در زمینه بهینه‌سازی هسته‌های پردازشی پردازنده‌های مرکزی و نصب ماژول‌های چندگیگابایتی حافظه اصلی در کامپیوترهای شخصی، مزایای مدیریت خودکار حافظه بیشتر از جنبه‌های منفی آن است. به همین دلیل است که بیشتر زبان‌های برنامه‌نویسی مدرن مانند جاوا، پایتون و گولنگ از مکانیزم مدیریت خودکار حافظه استفاده می‌کنند. البته، هنوز هم برخی زبان‌ها از مکانیزم مدیریت دستی حافظه استفاده می‌کنند که از آن جمله باید به C++ ،Objective-C و Rust اشاره کرد. اکنون که در مورد مدیریت حافظه و جمع‌آوری زباله اطلاعات کلی به‌دست آوریم، اجازه دهید مکانیزم جمع‌آوری زباله در پایتون را بررسی کنیم. 

چگونه پایتون از مکانیزم جمع‌آوری زباله استفاده می‌کند

فرض کنید پیاده‌سازی از پایتون را که CPython نام دارد روی سامانه‌ای نصب کرده‌ایم. CPython یکی از پرکاربردترین پیاده‌سازی‌های پایتون است که برنامه‌نویسان سراسر جهان از آن استفاده می‌کنند. البته پیاده‌سازی‌های دیگری از پایتون مثل PyPy ،Jython (مبتنی بر جاوا) یا IronPython (مبتنی بر سی‌شارپ) وجود دارند که کاربردهای خاص خود را دارند. برای این‌که ببینید چه پایتونی روی سیستم‌عامل‌تان نصب شده است، در ترمینال لینوکس دستور زیر را اجرا کنید:

>>>python -c ‘import platform; print(platform.python_implementation())’

Or, you can have these lines for both Linux and Windows terminals.

>>> import platform

>>> print(platform.python_imlplementation())

CPython

CPython بر مبنای دو روش Reference counting و Generational garbage collection به مدیریت حافظه و جمع‌آوری زباله‌ها می‌پردازد. 

شمارش مرجع (Reference counting) در CPython

مکانیسم اصلی جمع‌آوری زباله در CPython شمارش مرجع است. هر زمان یک شیء در CPython ایجاد می‌کنید، شیء ساخته‌شده هر دو ویژگی نوع پایتون (مثل لیست، لغت‌نامه یا تابع) و تعداد مرجع را دارد. 

در حالت پایه، هر زمان به یک شیء ارجاعی انجام می‌شود، شمارنده مربوط به ارجاعات یک واحد افزایش پیدا می‌کند و هنگامی‌که یک ارجاع به شیء خاتمه پیدا می‌کند یک واحد از شمارنده کاسته می‌شود. اگر تعداد ارجاع به یک شیء عدد 0 باشد، حافظه تخصیص‌داده‌شده به شیء آزاد می‌شود. دقت کنید که برنامه شما نمی‌تواند الگوی شمارش مرجع پایتون را غیرفعال کند. برخی از توسعه‌دهندگان ادعا می‌کنند ویژگی شمارش مرجع عملکردی ضعیف دارد و دارای نواقصی است. به‌طور مثال، چرخه شناسایی ارجاعات گاهی‌اوقات درست کار نمی‌کند. با این‌حال، شمارش ارجاعات عملکرد قابل قبولی دارد، زیرا می‌تواند بلافاصله یک شیء را زمانی که هیچ مرجعی به آن وجود ندارد حذف کرده و حافظه مربوط به شیء را آزاد کند. 

مشاهده تعداد مراجع در پایتون

می‌توانید از ماژول sys کتابخانه استاندارد پایتون برای بررسی تعداد مراجع برای یک شیء خاص استفاده کنید. نکته‌ای که باید در این زمینه به آن دقت کنید این است که فرآیند افزایش تعداد ارجاع‌ها به یک شیء شرایط خاصی به‌شرح زیر دارد:

  •  اختصاص یک شیء به یک متغیر.
  •  اضافه کردن یک شیء به یک ساختار داده‌ای، مانند افزودن به یک لیست یا افزودن به‌عنوان یک ویژگی در یک نمونه کلاس.
  •  ارسال شیء به‌عنوان آرگومان به یک تابع.

برای آن‌که درک بهتری در این زمینه به‌دست آورید، در ادامه از یک Python REPL و ماژول sys برای بررسی دقیق‌تر موضوع استفاده می‌کنیم. ابتدا در ترمینال سیستم‌عامل، python را تایپ کنید تا پنجره Python REPL ظاهر شود. ماژول sys در REPL را وارد کنید، متغیری ایجاد کنید و تعداد مراجع آن‌را بررسی کنید:

>>> import sys

>>> a = ‘my-string’

>>> sys.getrefcount(a)

2

در قطعه کد بالا، دو مرجع به متغیر a وجود دارد. مرجع اول در زمان ساخت متغیر ایجاد می‌شود و مرجع دوم هنگامی که متغیر a را به‌عنوان متغیر برای تابع ()sys.getrefcount ارسال می‌کنیم. اگر متغیر را به یک ساختار داده مانند فهرست یا لغت‌نامه اضافه کنید، باز هم شاهد افزایش تعداد مراجع خواهید بود. قطعه کد زیر این موضوع را نشان می‌دهد:

>>> import sys

>>> a = ‘my-string’

>>> b = [a] # Make a list with a as an element.

>>> c = { ‘key’: a } # Create a dictionary with a as one of the values.

>>> sys.getrefcount(a)

4

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

اکنون که دیدیم مکانیزم عملکردی شمارنده مرجع به چه صورتی کار می‌کند، وقت آن رسیده تا به سراغ تکنیک generational garbage collector برویم که دومین ابزار پایتون برای مدیریت حافظه است.

generational garbage collector

علاوه بر استراتژی شمارش مرجع برای مدیریت حافظه، پایتون از مکانیزم generational garbage collector استفاده می‌کند. ساده‌ترین راه برای درک این‌که چرا ما به ویژگی فوق نیاز داریم، ذکر مثالی است. در بخش قبل مشاهده کردیم که افزودن یک شیء به یک آرایه یا به یک شیء تعداد مراجع آن‌را افزایش می‌دهد، اما اگر یک شیء را به خودش اضافه کنید چه اتفاقی می‌افتد؟ به قطعه کد زیر دقت کنید:

>>> class MyClass(object):

...     pass

...

>>> a = MyClass()

>>> a.obj = a

>>> del a

در مثال بالا کلاس جدیدی تعریف کردیم. سپس، یک نمونه از کلاس ایجاد کردیم و به آن نمونه‌ای اختصاص دادیم که یک ویژگی روی خود شیء است. در نهایت، نمونه را حذف کردیم. با حذف نمونه، دیگر امکان استفاده از آن در برنامه پایتون وجود ندارد. با این حال، پایتون این نمونه را از حافظه حذف نکرده است و به همین دلیل تعداد مراجع نمونه برابر با مقدار صفر نیست، زیرا شیء به خودش ارجاع دارد.

ما این نوع مشکل را «چرخه مرجع» می‌نامیم و متاسفانه مشکل فوق از طریق مکانیزم شمارش مرجع قابل حل نیست. این درست همان زمانی است که ویژگی generational garbage collector  به میدان وارد می‌شود. یک ویژگی کاربردی که از طریق ماژول gc در کتابخانه استاندارد پایتون در دسترس قرار دارد. 

اصطلاحات کاربردی 

هنگامی که درباره ویژگی Generational garbage collector  صحبت می‌کنیم، دو اصطلاح مهم وجود دارد که باید در مورد آن اطلاع داشته باشید. اصطلاح اول، generation و اصطلاح دوم threshold است. 

زباله‌جمع‌کن همه اشیاء را در حافظه نگه می‌دارد. چرخه حیات یک شیء جدید با نسل اول زباله‌جمع‌کن آغاز می‌شود. اگر پایتون یک فرآیند جمع‌آوری زباله را روی یک نسل اجرا کند و یک شیء فعال باشد به نسل دوم هدایت می‌شود. مکانیزم جمع‌آوری زباله پایتون در مجموع دارای سه نسل است و هر زمان که یک شیء از فرآیند جمع‌آوری زباله در نسل فعلی خود جان سالم به در ببرد، به نسل بعدی منتقل می‌شود.

برای هر نسل، ماژول جمع‌آوری زباله دارای تعدادی «آستانه» (threshold) اشیاء است. اگر تعداد اشیاء از آن آستانه فراتر رود، جمع‌آورنده زباله فرآیند جمع‌آوری را اجرا می‌کند. هر شیء که از این فرآیند جان سالم به در می‌برد، به نسل بعدی هدایت می‌شود.

نکته مثبتی که در ارتباط با تکنیک فوق وجود دارد این است که برنامه‌نویسان می‌توانند به‌صورت دستی فرآیند جمع‌آوری زباله را اجرا کنند یا فرآیند جمع‌آوری زباله را به‌طور کلی غیرفعال کنند. برای روشن شدن بحث اجازه دهید از ماژول gc برای بررسی عملکرد ویژگی مذکور استفاده کنیم.

 به‌کارگیری ماژول GC

در ترمینال لینوکس، python را وارد کنید تا به Python REPL بروید. ماژول gc را به نشست خود وارد کنید. برای آن‌که بتوانید نحوه جمع‌آوری زباله‌ها را بررسی کنید، متد get_threshold را فراخوانی کنید:

>>> import gc

>>> gc.get_threshold()

(700, 10, 10)

به‌طور پیش‌فرض، پایتون آستانه 700 را برای تازه‌ترین نسل و 10 را برای دو نسل قدیمی تعیین کرده است. با متد get_count می‌توانید تعداد اشیاء در هر نسل را همانند قطعه کد زیر بررسی کنید:

>>> import gc

>>> gc.get_count()

(596, 2, 1)

همان‌گونه که مشاهده می‌کنید، پایتون قبل از این‌که برنامه را اجرا کنید به‌طور پیش‌فرض تعدادی شیء ایجاد می‌کند. با استفاده از متد gc.collect می‌توانید فرآیند جمع‌آوری دستی زباله را همانند قطعه کد زیر اجرا کنید:

>>> gc.get_count()

(595, 2, 1)

>>> gc.collect()

577

>>> gc.get_count()

(18, 0, 0)

اجرای یک فرآیند جمع‌آوری زباله باعث می‌شود بخش قابل توجهی از اشیاء بدون استفاده برنامه آزاد شوند. البته این امکان وجود دارد که از متد

set_threshold در ماژول gc برای تغییر آستانه‌ شروع جمع‌آوری زباله استفاده کنید:

>>> import gc

>>> gc.get_threshold()

(700, 10, 10)

>>> gc.set_threshold(1000, 15, 15)

>>> gc.get_threshold()

(1000, 15, 15)

در قطعه کد بالا، مقدار هر یک از آستانه‌های پیش‌فرض را افزایش داده‌ایم. افزایش آستانه، بار کاری زباله‌‌جمع‌کن را کاهش می‌دهد که باعث افزایش عملکرد می‌شود. البته مشکلی که تکنیک فوق دارد این است که برنامه پذیرای اشیاء بدون ارجاع بیشتری خواهد بود. اکنون که می‌دانیم شمارش مراجع و ماژول جمع‌آوری زباله چگونه کار می‌کنند، بهتر است در مورد نحوه استفاده از آن‌ها هنگام نوشتن برنامه‌های پایتون اطلاعاتی به‌دست آوریم. 

چرا نحوه کارکرد garbage collector پایتون برای کدنویسی بهتر ضروری است؟ 

اکنون که اطلاعات خوبی در ارتباط با نحوه مدیریت حافظه و چگونگی جمع‌آوری اشیاء‌بدون‌استفاده به‌دست آوردیم، بهتر است به‌سراغ این مبحث برویم که چگونه باید از این اطلاعات به‌عنوان توسعه‌دهنده برنامه‌های پایتون استفاده کنیم.

قانون کلی: به‌دنبال تغییر الگوی کاری garbage collector  نباشید. 

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

مدیریت حافظه به‌شیوه دستی بیشتر برای پروژه‌های خاص مفید است. اگر با محدودیت‌های عملکردی روبه‌رو شدید که فکر می‌کنید ممکن است مرتبط با مکانیزم‌های جمع‌آوری زباله پایتون باشد، بهتر است به‌جای تغییر دستی فرآیند جمع‌آوری زباله، روی بهبود الگوهای کدنویسی خود متمرکز شوید. در بیشتر موارد، اگر کدها را بازنویسی کنید و از اشیاء دیگری استفاده کنید به نتیجه دلخواه دست پیدا می‌کنید. همچنین، به این نکته دقت کنید که فرآیند جمع‌آوری دستی زباله با هدف آزاد کردن حافظه ممکن است نتایجی بر خلاف انتظار شما داشته باشد. 

غیرفعال کردن Garbage Collector

در برخی از پروژه‌ها مجبور می‌شوید فرآیند جمع‌آوری زباله را از حالت خودکار خارج کرده و به‌شکل دستی آن‌را مدیریت کنید. نکته‌ای که باید در این زمینه به آن اشاره کنیم این است که ویژگی شمارش مرجع، مکانیزم اصلی جمع‌آوری زباله در پایتون است که غیرفعال نمی‌شود. تنها مکانیزمی که قادر به تغییر رفتار آن هستید، generational garbage collector

در ماژول gc است. یک نمونه کاربردی جالب در این زمینه، اینستاگرام بود که ویژگی garbage collector  را به‌طور کلی غیرفعال کرد. اینستاگرام از جنگو، چارچوب وب محبوب پایتون، در زمینه توسعه برنامه‌های کاربردی وب خود استفاده می‌کند تا بتواند چند نمونه از برنامه وب خود را بر روی یک نمونه محاسباتی اجرا کند. این نمونه‌ها با استفاده از مکانیزم master-child اجرا می‌شوند که در آن، فرزند حافظه اشتراک‌گذاری‌شده با Master را استفاده می‌کند.

تیم توسعه‌دهنده اینستاگرام متوجه شد که حافظه مشترک پس از ساخت فرزند عملکردش به‌شدت کاهش پیدا می‌کند. ارزیابی‌ها نشان داد که مشکل Garbage Collector است. تیم اینستاگرام ماژول جمع‌آوری زباله را با صفر کردن آستانه‌ها برای همه نسل‌ها غیرفعال کرد که تغییر فوق باعث شد که برنامه‌های وب آن‌ها 10 درصد کارآمدتر عمل کنند. 

با آن‌که مثال فوق جالب توجه است، اما قبل از دنبال کردن این مسیر در پروژ‌های کاربردی خود ابتدا اطمینان حاصل کنید که مشکل عملکردی برنامه مرتبط با ویژگی جمع‌آوری زباله است. اینستاگرام یک اپلیکیشن در مقیاس وب است که به میلیون‌ها کاربر خدمات‌رسانی می‌کند. به همین دلیل برای آن‌ها ممکن است که برخی الگوهای رفتاری را ویرایش کنند و از مکانیزم‌های غیراستاندارد برای دستیابی به بهره‌وری بیشتر استفاده کنند. در بیشتر موارد، عملکرد استاندارد پایتون پاسخ‌گوی نیازهای کاری است. 

کلام آخر

اگر در نظر دارید فرآیند جمع‌آوری زباله در پایتون را به‌شیوه درستی مدیریت کنید، ابتدا باید تحقیقات جامعی در این زمینه انجام دهید. برای این منظور از ابزارهایی مانند Stackify’s Retrace برای ارزیابی عملکرد برنامه خود و مشخص کردن مشکلات استفاده کنید. هنگامی که مشکل را به‌طور کامل درک کردید، اقدامات لازم برای رفع آن‌را انجام دهید.

ماهنامه شبکه را از کجا تهیه کنیم؟
ماهنامه شبکه را می‌توانید از کتابخانه‌های عمومی سراسر کشور و نیز از دکه‌های روزنامه‌فروشی تهیه نمائید.

ثبت اشتراک نسخه کاغذی ماهنامه شبکه     
ثبت اشتراک نسخه آنلاین

 

کتاب الکترونیک +Network راهنمای شبکه‌ها

  • برای دانلود تنها کتاب کامل ترجمه فارسی +Network  اینجا  کلیک کنید.

کتاب الکترونیک دوره مقدماتی آموزش پایتون

  • اگر قصد یادگیری برنامه‌نویسی را دارید ولی هیچ پیش‌زمینه‌ای ندارید اینجا کلیک کنید.

ایسوس

نظر شما چیست؟