הבלוג של ארתיום
בלוג על לינוקס, תוכנה חופשית, מוזיקה, סלסה, ומה לא!
לוותר על gc או לוותר על destructor?
היום, יש נטייה להשתמש בכל שפה אפשרית ב־GC -- כי היא פותרת המון בעיות בניהול זיכרון. כמעט כל השפות פופולריות (למעט C/C++/Pascal) מממשות אותה בצורה זו או אחרת.
כמעט כולם (למעט CPython, Perl ו־vala) משתמשות בשיטה של "בדיקת נגישות" (Reachability). בשיטה זו מנוע GC פועל אחת לזמן מה, מחפש כל מופעי אובייקטים שאינם נגישים יותר ומוחק אותם.
השיטה הפחות נפוצה, שמתשמשים בה למשל ב־CPython, היא שימוש ב"ספירת הפניות" (Reference counting), אבל היא קשה למימוש ובעייתית באופן כללי, בגלל טיפול בלולאות סגורות, כאשר אובייקט יכול להפנות לעצמו, ובכך "מספר הפניות" לעולם לא ירד מתחת ל־0.
שימוש ב־GC שיטה הראשונה פותרת את בעיית ניהול הזיכרון, יחד עם זו היא מציבה בעיה הגדולה אחרת: destructors או finalizers --- מתודות שמבצעות "מחיקה" של האובייקט, הופכות לכמעט ולא רלוונטיות. כי אי אפשר לדעת מתי ישוחרר האובייקט. כלומר ב־Java, C#, IronPython, PHP, D וכמעט כל שפות אחרות שמשתמשות בשיטה הזו, destructors הם חסרי משמעות.
שימוש ב־GC מאפשר ניהול חכם של משאב אחד והורס אפשרות ניהול חכמה של כל משאבים אפשריים אחרים כמו: socket, קובץ, mutex, thread או כל משאב אחר הדורש ניהול מסודר.
כל אלה, הם גם משאבים. כל אלה דורשים פעולות מסודרות של סיום העבודה אתם ושחרורם. בשפות כמו C++/Pascal, בהם אובייקטים קיימים בדיוק הזמן הדרוש, ניתן לנהל משאבים ב־constructors וב־destructors כמו כל משאב אחר, בפרט זיכרון.
הכלליות הזו לא קיימת, בשפות שמשתמשות ב־GC, כי בד"כ למשאבים האלה, אין תחליף זמין.
הנה קוד C++ ו־Java שמציג "סקיצה" של שרת קבצים פשוט שממומש בשתי השפות האלה. ב־Java אני נדרש לנהל משאבים באופן מפורש, ב־C++ אני נותן את התפקיד ל־destructors. למשל, עם מחיקת אובייקט lock הנעילה משתחררת, עם מחיקת אובייקט קובץ הוא נסגר ועוד.
קוד C++:
class connecton {
...
void run()
{
string fname;
buffer b;
bool read_flag
while((fname=get_header(read_flag))!="") {
if(read_flag) {
file input(fname);
while(input.read_some(b)) {
write_some(b);
}
}
else {
lock_file locker(fname);
file out(fname);
while(read_some(b)) {
out.write_some(b);
}
}
}
}
};
int main()
{
threads_mgr mgr;
try {
acceptor ac("localhost",2000);
for(;;) {
try {
shared_ptr<connection> con(ac.accept());
mgr.new_thread(con);
}
catch(ordinary_error &e) {
cerr<<"Problem:"<<e.what()<<endl;
}
}
}
catch(fatal_error &e) {
cerr<<e.what()<<endl
return 1;
}
return 0;
}
קוד Java:
class connecton {
...
void run()
{
string fname;
buffer b;
bool read_flag
file input;
file out;
try {
while((fname=get_header(read_flag))!="") {
if(read_flag) {
input=new File(fname);
input.openread();
while(input.read_some(b)) {
write_some(b);
}
input.close();
}
else {
lock_file(fname);
try{
out=new File(fname);
out.openwrite();
while(read_some(b)) {
out.write_some(b);
}
out.close();
}
finally{
unlock_file(fname);
}
}
}
}
finally {
input.close()
out.close();
close_connection();
}
}
};
class server {
public static void main(string s[])
{
threads_mgr mgr=new threads_mgr;
try {
acceptor ac=new acceptor("localhost",2000);
for(;;) {
connection con;
try {
con=ac.accept();
mgr.new_thread(con);
}
catch(ordinary_error e) {
sys.out.println(e.what());
}
finally {
con.close();
}
}
}
catch(fatal_error e) {
sys.out.println(e.what());
}
finally {
mgr.stop_all_threads();
acceptor.stop_litening();
}
}
}
השאלות:
- מהו הקוד המסודר יותר?
- מהו הקוד הקריא יותר?
- מהו הקוד שעלול להכיל יותר באגים?
- מהו הקוד שקל יותר לעשות בו זליגת משאבים?
אז אולי בכל זאת GC אינו רעיון כל־כך טוב?
תגובות
בC# אפשר לכתוב destructor
גם ב־Java אפשר, אבל השימושיות שלהם מאוד מוגבלת.
הבעיה היא שאתה לא יודע מתי הוא ייקרא. או ליתר דיוק, הוא ייקרא רק כאשר GC יפעל, לכן, אתה לא יכול לדעת מתי המשאב ישתחרר.
לדוגמה: אם יש לך socket שקשור לאובייקט, אז הוא ייסגר רק כש־GC יפעל. זאת אומרת, שכאשר יש לך מגבלה ממוצעת של 1024 סוקטים, אז מהר מאוד, בלי סגירה מסודרת שלהם, אתה תבזבז את כל המשאב. ותגיע למצב בו אתה לא יכול ליצור סוקט נוסף, רק בגלל ש־GC לא פעל ולא מחק את האובייקטים.
לכן, לא ממש ניתן להסתמך על destructor של C#/Java לשחרור משאבים.
ראוי לציין שגם perl 5 משתמשת ב-reference counting ויש בה destructors.
תודה, תיקנתי
פייתון (לפחות CPython) משלב את השניים ומשתמש ב-reference counting יחד עם GC מחזורי (אמנם אופציונלי, אך מאופשר בברירת מחדל) לטיפול בהפניות מעגליות:
http://www.python.org/doc/2.5.2/ext/refcounts.html http://en.wikipedia.org/wiki/Reference_counting#Python
ב-#C יש ממשק שנקרא IDisposable, ומגדיר מתודה בשם Dispose; ופקודה בשפה, using, שמשתמשת בממשק כדי לתת לך אפקט דומה לזה של destructor ב-++C. כלומר, אם יש לך class שמממש את הממשק, נגיד connection מה-Java שלך, אז הקוד
שקול לקוד ה-Java
שזה, כמעט לגמרי, מה שרצית.
(זה מתקשר גם לפוסט של שלומיל מהערב: #C היא לא סתם Java, היא מתקנת הרבה מהשגיאות).
אה, וכמובן, שכחתי:
עידו עוד מעט יבוא לספר לך שבפסקל יש מנגנון דומה לזה של #C (שכמובן, אנדרס לקח מפסקל ל-#C ולא להיפך), ובפייתון מאז 2.5 יש פקודת with ופרוטוקול context manager. ב-Lisp, השפה שבמידה רבה הביאה לעולם את ה-GC, יש unwind-protect עוד מהתקופה ש-++ היה אופרטור ולא חלק משם של שפה.
ל-using של #C יש קצת שגעונות, ואני לא מכיר את המנגנונים של פסקל מספיק כדי להעיר עליהם (מעבר לכך שהם קיימים), אבל התחבירים של פייתון וליספ לעניין הזה הרבה יותר מוצלחים ממה שיש ל-++C להציע.
שלומי: פרל משתמשת ב־reference counting ברוב המקרים.
http://search.cpan.org/dist/perl/pod/perlobj.pod#Two-Phased_Garbage_Collection______
הבעיה שכל הטריקים כמו using או try/finally עובדים אך ורק כאשר יש flow מאוד ברור. (וגם עדיין צריך לציין אותם במפורש.
מה קורה כשאנחנו עובדים עם תוכנה שהיא מטבעה event driven כמו GUI או שרת אסיכנרוני, בהם אין flow ברור? לדוגמה, בקוד הזה, זה פשוט בלתי אפשרי להשתמש בטריקים כאלה. זהו פסיאודו קוד C++ בעבודה עם סוקטים עם ספריית asio אסינכרונית. הוא תקף באותה מידה ל־GUI או דברים אחרים.
פה לא ניתן למצוא מקומות לשים try/catch block
עד כמה שאני יודע, השימוש המועיל ב-destructor לשחרור משאבים כמו שכתבת בפוסט -- מה שקוראים RAIC -- מסתמך על בלוקים בשפה. כלומר, ה"קסם" נוצר בגלל שהשפה מבטיחה לך שה-destructor-ים ייקראו ביציאה מ-block. במקום שבו אתה יכול לשים בלוק רגיל ב-++C, אתה יכול לשים בלוק try/finally ב-Java (אם כי זה יכאב לך), בלוק with בפייתון, או בלוק using ב-#C.
לא התעמקתי יותר מדי בפסודו-קוד שלך -- אני לא רואה שם אף destructor, ולא מבין איפה אחד היה תורם משהו; אבל האתגר שלקחת על עצמך בהערה האחרונה הוא להראות מקום שבו:
א. חשוב שה-destructor ייקרא בזמן שהמתכנת קובע
ב. אתה יכול לגרום לזה לקרות ב-++C בלי קריאות מפורשות ל-delete
ג. אתה לא יכול לעשות את זה במסגרת של בלוק.
לדעתי, אתה לא יכול לעמוד באתגר.
או קיי תחשוב... לכל מחלקה מוסג file/socket/acceptor יש destructor שיודע לעשות cleanup.
ברגע שהאובייקט נמחק... (זה מבטיח shared_ptr) כל הרחיבים שלו נסגרים בצורה מסודרת.
זה בדיוק האתגר שבו עמדתי בקוד שהצגתי. המחלקות מסוג connection נמחקו כש:
וכל זה... בלי בכלל לכתוב בלוק try-catch יחיד ;) (טוב, למעט אחד הראשי)
אה, ref-counting. נכון שזה לא קיים ב-Java או #C, אבל (כמו שמאיר כבר העיר) זו שיטת העבודה הכללית של פייתון.
השורה התחתונה של כל מה שאני מנסה להגיד היא שהכותרת של הפוסט שלך שגויה: בהחלט אפשר לקבל גם זיכרון מנוהל (GC) וגם destructor-ים שנקראים מתי שאתה רוצה. אם אני לא טועה, יש אפשרות כזו אפילו ב-++C (שם אתה צריך "לבקש" ניהול זכרון ע"י שימוש במצביעים חכמים, ואני לא חושב שזה בספרייה הסטנדרטית, אבל אולי אני טועה או לא מעודכן). בודאי שהאפשרות קיימת בתפלצת דמוית ++C של Microsoft.Net, המכונה ++Managed C, ושאני לא ממליץ לגעת בה עם מקל, אבל בכ"ז.
השימוש בזיכרון מנוהל לא מונע, אינהרנטית, את השימוש בפונקציות ניקוי ושחרור. Java היא שפה גרועה, זה הכל.
נכון, אבל הבעיה שזה קיים בשתיים וחצי שפות: CPython ו־Perl אבל הן נמצאות בליגה אחרת -- dynamic/duck typed languages. יש ref-counting גם ב־vala אבל היא יחסית חדשה ועדיין עוף מוזר.
אבל השפות ה־static typed הגדולות הפופולריות C#/Java, כל שפות ה־managed כולל IronPython ו־D שצוברת פופולריות לאחרונה, בחרו השיטת GC אחרת --- שהיא הנפוצה והמקובלת שלא מאפשרת הפעלה של destructors מסודרים.
אכן, ראה את מה שכתבתי כאן.
בגדול, זה כבר "כמעט סטנדרט". tr1/shared_ptr כבר קיים בגרסאות gcc 4.1 וגם ב־visual studio 2008 (עם service pack).
בנוסף, boost נותן אותו כבר מזמן וגם את המימוש המוצלח ביותר. גם קיימים הרבה מימושים חילופיים אחרים כמעט בכל toolkit.
כך שבפועל זו השיטה הסטנדרטית.
נ.ב. לגבי הכותרת... העניין הוא ש־ref-counting הוא בהחלט לא GC מלא, אם כי נותן פתרון ב־95% מהמקרים. לכן, ref-counting הוא לא בדיוק GC (בגלל נושא הלולאות).
לא, התכוונתי למשהו כזה:
http://www.hpl.hp.com/personal/Hans_Boehm/gc/gcinterface.html
לא מכיר את זה, בכל אופן, הטענה שלי, שלא ממש צריך GC. בעבודה חכמה ref-counting מספיק מעל ומעבר.
הוסף תגובה:
חובה לאפשר JavaScript כדי להגיב.