1.8 - בעלות משותפת וחיים תלויים - הרצאה
כשבאמת יש יותר מבעלים אחד¶
בעלות יחידה קלה יחסית להסבר: אובייקט אחד אחראי לסיום חיי המשאב. לפעמים כמה עבודות צריכות לשמור אותו אובייקט חי באופן עצמאי, ולא ידוע מראש מי תסתיים אחרונה.
הטיפוס shared_ptr מנהל בעלות משותפת. כל בעלים מחזיק קשר ל־control block, שמנהל בין השאר את מספר הבעלים. האובייקט מנוקה כשהבעלות החזקה האחרונה נעלמת, גם אם משתנה שמחזיק אותו נמצא בתהליך אחר של פירוק scope.
הדוגמה הבאה מראה את ההבדל בין alias רגיל לבעלים נוסף:
#include <cstdio>
#include <memory>
struct Job {
~Job() { std::puts("job destroyed"); }
int value = 7;
};
int main() {
auto first = std::make_shared<Job>();
{
auto second = first;
first.reset();
std::printf("value=%d\n", second->value);
}
std::puts("after owners");
}
הקריאה reset מסיימת את הבעלות של first, אבל second עדיין מחזיק את האובייקט בחיים. ההשמדה מתרחשת בסוף הבלוק הפנימי, לפני ההדפסה האחרונה.
העתקת shared_ptr אינה העתקה של Job. שני הבעלים ניגשים לאותו אובייקט, ולכן שינוי value דרך אחד מהם נראה דרך השני בכפוף לכללי הסנכרון. אם רוצים שני ערכים עצמאיים, צריך ליצור שני אובייקטים ולא שני בעלים.
הבקרה אינה הנתון¶
ה־control block והאובייקט הם שתי ישויות לוגיות נפרדות. לפעמים make_shared מקצה אותם ביחד, אבל אין להסיק מכך שהם אותו דבר או שיש להם אותו מועד שחרור בדיוק.
הבעלות החזקה שומרת את האובייקט חי. קשר חלש יכול להשאיר את מידע הבקרה קיים כדי לבדוק אם ניתן עדיין לקבל בעלות, גם אחרי שהאובייקט עצמו הושמד. לכן יש להבחין בין הפעלת destructor של האובייקט לבין שחרור האחסון שמחזיק את כל תשתית הבקרה.
הפרטים האלה חשובים כשהאובייקט גדול או כשweak_ptrים נשארים זמן רב. בחירת make_shared אינה רק חיסכון בשורת קוד; היא יכולה להשפיע גם על דפוס ההקצאה והחזרת זיכרון למערכת.
מעגל שאין לו בעלים אחרון¶
נניח שאובייקט אחד מחזיק את הבא אחריו, והבא מחזיק את הראשון. אפשר ליצור זאת בקוד הבא:
#include <cstdio>
#include <memory>
struct Node {
std::shared_ptr<Node> next;
~Node() { std::puts("node destroyed"); }
};
int main() {
auto first = std::make_shared<Node>();
auto second = std::make_shared<Node>();
first->next = second;
second->next = first;
first.reset();
second.reset();
}
זאת דוגמת דליפה מכוונת לצורך ניתוח. בסוף main אין עוד שם מקומי שמאפשר להגיע לnodes, אבל כל אחד מהם עדיין מחזיק בעלות חזקה באחר. אין בעלים אחרון שמתאפס, ולכן ה־destructorים אינם נקראים.
המנגנון אינו garbage collector שמחפש כל גרף שאינו נגיש. הוא מנהל ספירה של בעלות. פתרון נכון דורש לקבוע אילו קשרים בגרף מבטאים בעלות ואילו מבטאים רק ניווט או צפייה.
קשר חלש אינו בעלים זול¶
הטיפוס weak_ptr מתאים לקשר שאינו אמור להאריך חיים. כדי להשתמש באובייקט צריך לנסות לקבל ממנו בעלות חזקה:
#include <cstdio>
#include <memory>
struct Job {
int value = 7;
~Job() { std::puts("destroy"); }
};
void inspect(std::weak_ptr<Job> weak) {
if (auto owner = weak.lock()) {
std::printf("%d\n", owner->value);
} else {
std::puts("job no longer exists");
}
}
int main() {
auto job = std::make_shared<Job>();
std::weak_ptr<Job> weak = job;
inspect(weak);
job.reset();
inspect(weak);
}
הפעולה lock מנסה ליצור בעלים חזק אחד. אם היא הצליחה, הבעלים המקומי שומר את האובייקט חי לאורך השימוש. אין צורך לבצע בדיקת expired ואז להניח שהמצב לא השתנה; רצף של בדיקה נפרדת ושימוש מאוחר יותר אינו אותו חוזה.
הקוד עדיין אינו מבטיח שהשדה value מוגן מפני כתיבה מקבילה. הפעולה lock שומרת על חיי האובייקט, לא על כל state פנימי שבו. פרק המקביליות יגדיר איך מגינים על הנתונים עצמם.
קריאה חוזרת שמחזיקה את עצמה - Callback¶
מעגל בעלות יכול להסתתר גם בתוך פונקציה. אם אובייקט מחזיק std::function, וה־lambda שבתוכה שומרת shared_ptr לאותו אובייקט, נוצר קשר מהאובייקט אל עצמו דרך closure.
הדוגמה הבאה היא גרסה מתוקנת של רישום callback:
#include <cstdio>
#include <functional>
#include <memory>
struct Session {
std::function<void()> callback;
int events = 0;
~Session() { std::puts("session destroyed"); }
};
int main() {
auto session = std::make_shared<Session>();
std::weak_ptr<Session> weak = session;
session->callback = [weak] {
if (auto owner = weak.lock()) {
++owner->events;
}
};
session->callback();
std::printf("%d\n", session->events);
}
ה־closure מחזיקה weak_ptr, ולכן אינה משמרת לבדה את session. בזמן הקריאה היא מבקשת בעלות זמנית. אפשר להעתיק את callback למערכת אחרת, ואם הקריאה תגיע אחרי השמדת session היא לא תבצע עבודה על אובייקט מת.
הפתרון אינו מבטל את הצורך להסיר subscriptions או לנקות משאבים שקשורים במערכת האירועים. הוא רק מגדיר מה מותר לעשות אם callback בכל זאת מגיע מאוחר.
מלכודת של שני control blocks¶
אסור ליצור shared_ptr חדש מ־get של shared_ptr קיים מתוך מחשבה שממשיכים את אותה בעלות. הבנאי מ־pointer גולמי ייצור מנגנון בעלות נפרד, וכל אחד מהבעלים עלול לנסות להשמיד את אותו אובייקט.
צריך להעתיק את shared_ptr הקיים או להשתמש במנגנון מתאים כמו enable_shared_from_this כאשר מחלקה באמת צריכה לקבל בעלות משותפת על עצמה. גם שם יש תנאים: האובייקט צריך כבר להשתייך לבעלות משותפת תקינה, ולכן קריאה מוקדמת מדי בבנאי אינה תחליף לפונקציית יצירה מסודרת.
מה בודקים בחקירה¶
מציירים גרף של קשרי בעלות חזקים בנפרד מקשרים חלשים. לכל closure ולכל תור עבודה בודקים אילו אובייקטים הם מחזיקים, ומתי הרישום מוסר.
המספר use_count יכול לעזור בניסוי מקומי, אבל אינו פרוטוקול סנכרון. אם צריך להחליט מי רשאי לשנות נתון, אין להסתמך על כך שבנקודת זמן אחת נצפה בעלים יחיד. צריך חוזה נפרד שמגן על הפעולה לאורך כל ביצועה.