לדלג לתוכן

העברת בעלות ושיתוף בין תהליכונים

נניח שטיפוס מחזיק מצביע לידית זרה, ואנחנו רוצים להעביר אותו לתהליכון אחר. העובדה שהמצביע הוא מספר אינה מוכיחה שהספרייה הזרה מאפשרת זאת. ייתכן שהידית שייכת לתהליכון שיצר אותה, או שההשמדה שלה חייבת לקרות שם. חוזי Send ו־Sync מאפשרים לראסט לייצג את ההבטחות האלה ברמת הטיפוסים.

החוזה Send עוסק באפשרות להעביר ערך בין תהליכונים. החוזה Sync עוסק באפשרות לשתף הפניה משותפת, ובקשר הטיפוסי שלפיו &T יכולה לעבור בין תהליכונים כאשר T מתאים לשיתוף. אלה אינם שמות נרדפים לנעילה, וגם טיפוס ללא mutex יכול להיות מתאים לשיתוף אם הוא immutable או משתמש במנגנון נכון אחר.

הספרייה מסיקה את התכונות האוטומטיות האלה לפי המבנה והחוזים של השדות, כל עוד לא התערבנו עם מימוש ידני. למשל, Rc אינו מתאים לשיתוף כזה משום שמונה ההפניות שלו אינו מספק את הסנכרון הדרוש. החלפת Rc ב־Arc מטפלת במונה ההפניות, אבל אינה הופכת כל תוכן פנימי לבטוח לשינוי מקביל.

הקוד הבא משתף מאגר immutable באמצעות Arc. כל עובד מחזיק בעלות משותפת שמבטיחה שהמאגר יישאר חי עד שיסיים להשתמש בו.

use std::sync::Arc;
fn main() {
    let values = Arc::new(vec![10u64, 20, 30, 40]);
    let weak = Arc::downgrade(&values);
    let first_data = Arc::clone(&values);
    let second_data = Arc::clone(&values);
    let first = std::thread::spawn(move || first_data[..2].iter().sum::<u64>());
    let second = std::thread::spawn(move || second_data[2..].iter().sum::<u64>());
    assert_eq!(first.join().unwrap() + second.join().unwrap(), 100);
    let owned = Arc::try_unwrap(values).expect("unexpected strong owner");
    assert_eq!(owned.len(), 4);
    assert!(weak.upgrade().is_none());
}

ההפניות החלשות אינן שומרות את הערך הפנימי חי. לכן אחרי שהבעלים החזקים נעלמים, upgrade מחזירה None. המידע הניהולי של ההקצאה יכול להישאר כל עוד קיימות הפניות חלשות, אבל זאת אינה הרשאה לקרוא שוב את הערך שכבר הושמד.

המתודה try_unwrap דורשת מצב מתאים של בעלות חזקה ומחזירה את הערך בבעלות יחידה כאשר אפשר. אין להחליף אותה בבדיקת strong_count == 1 ואחריה פעולה לא בטוחה שמניחה שהמצב נשאר כך. במערכת עם פעולות מקבילות, ספירה אבחונית אינה בהכרח פרוטוקול שמקבע את מצב הבעלות לאורך פעולה.

שינוי נתונים מאחורי Arc דורש טיפוס פנימי שמייצג את כללי הגישה, למשל נעילה או אטומיות מתאימה. הטיפוס Arc<RefCell<T>> אינו פתרון כללי לשינוי משותף בין תהליכונים, משום שבדיקת ההשאלות של RefCell אינה הסנכרון הנדרש למצב הזה. בפרק הבא נשתמש ב־Mutex כדי לחבר אינווריאנט לוגי למקטע קריטי.

מימוש unsafe impl Send או unsafe impl Sync הוא התחייבות של מחבר ההפשטה לכל לקוח בטוח. צריך להוכיח לא רק שהמתודה הראשית עובדת, אלא שגם Drop, callbacks, כשל חלקי וכל אפשרויות השימוש הציבוריות מכבדים את החוזה. אם הספרייה הזרה מגבילה השמדה לתהליכון המקורי, עטיפה שמאפשרת להעביר את הבעלים מפרה את החוזה גם אם הקריאות הרגילות אינן משנות דבר.

כאשר טיפוס צריך להישאר מקומי לתהליכון, אפשר לייצג זאת באמצעות שדה marker מתאים, כמו PhantomData<Rc<()>>, כפי שעשינו בעטיפת FFI. המטרה אינה להוסיף אובייקט בזמן ריצה, אלא לחבר את ההגבלה לתכונות האוטומטיות של הטיפוס. אל תסירו marker כזה רק כדי שהקוד יתקמפל בלי לבדוק את הסיבה שבגללה נוסף.

גם Arc אינו חינם. שכפול והשמדה מעדכנים מונים אטומיים ויכולים ליצור contention על קו מטמון משותף. אם כל פעולה קטנה משכפלת כמה בעלים, נמדוד את העלות ונבדוק אם אפשר להעביר בעלות אחת לשלב שלם או להשאיל בתוך scope. שינוי כזה צריך לשמור על אותם חיי נתונים, ולא להחליף בעלות במספר כתובת שנשמר בלי הגנה.

החוזים מפורטים בתיעוד Send, בתיעוד Sync ובתיעוד Arc. בסקירת קוד נפריד בין שמירת חיים של הערך לבין סנכרון הגישה לתוכן שלו.