לדלג לתוכן

פרסום נתונים וסדרי זיכרון

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

הצמד release ו־acquire יוצר קשר מתאים כאשר קריאת acquire רואה את הכתיבה הרלוונטית לפי כללי המודל. סדר הפעולות לפני הפרסום ואחרי הקבלה מקבל משמעות בין התהליכונים. עצם שימוש במילים release ו־acquire על שני אטומים שאינם קשורים אינו מספיק; צריך לזהות איזו קריאה ראתה איזו כתיבה.

נשתמש תחילה בנתון אטומי גם עבור ה־payload כדי שאפשר יהיה להשוות סדרים בלי להכניס data race רגיל. הקוד הבא הוא פרסום חד־פעמי בלבד.

use std::sync::{Arc, atomic::{AtomicBool, AtomicU64, Ordering}};
struct Packet { value: AtomicU64, ready: AtomicBool }
fn main() {
    let packet = Arc::new(Packet { value: AtomicU64::new(0), ready: AtomicBool::new(false) });
    let producer_packet = Arc::clone(&packet);
    let producer = std::thread::spawn(move || {
        producer_packet.value.store(42, Ordering::Relaxed);
        producer_packet.ready.store(true, Ordering::Release);
    });
    let consumer_packet = Arc::clone(&packet);
    let consumer = std::thread::spawn(move || {
        while !consumer_packet.ready.load(Ordering::Acquire) { std::hint::spin_loop(); }
        consumer_packet.value.load(Ordering::Relaxed)
    });
    assert_eq!(consumer.join().unwrap(), 42);
    producer.join().unwrap();
}

הכתיבה ל־value קודמת לפרסום ready. הקורא יוצא מהלולאה רק אחרי שראה את הערך true דרך acquire, ולכן הקשר הרלוונטי מאפשר לקריאה המאוחרת של value לראות את הנתון שפורסם. בדוגמה אין שימוש חוזר באותו דגל ואין כותב שני, ואלה הנחות חשובות של ההוכחה.

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

לולאת spin מתאימה כאן כדי להציג את נקודת הקריאה בצורה קצרה. היא אינה המלצה להמתנה ממושכת בשירות. אם המפיק נעצר או מתעכב, הקורא ממשיך לצרוך משאבי מעבד. בפרוטוקול אמיתי נשלב מנגנון המתנה ויקיצה שמונע lost wakeup, בלי לשנות את חובות פרסום הנתונים.

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

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

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

כדי להקטין ordering נכתוב קודם טיעון: מי כותב את הנתון, מתי הוא מתפרסם, איזו קריאה מקבלת את הפרסום ומתי מותר למחזר את האחסון. לאחר מכן נשתמש בכלי מודל ובבדיקות נגדיות. שינוי מ־SeqCst ל־Relaxed רק משום שהבנצ'מרק השתפר הוא ויתור על התחייבות שצריך להצדיק.

הפרטים של כל סדר נמצאים בתיעוד Ordering, ושל fences בתיעוד fence. בכל דוגמה נסמן את חץ הסנכרון בין הכתיבה לקריאה, ולא רק נצבע את שני הצדדים באותו שם ordering.