לדלג לתוכן

פתרון מודל הזיכרון של השפה

במקרה הראשון שני threads יכולים לקרוא את הערך חמש, לחשב שש ולכתוב שש. כל גישה למונה אטומית, אבל זוג הפעולות אינו פעולה אחת. התיקון הוא fetch_add כאשר תמיד מותר להגדיל, או לולאת CAS כאשר ההגדלה תלויה בתנאי.

הקוד הבא בודק מונה מוגבל. הוא אינו מפרסם payload, ולכן relaxed מספיק לפעולות על המונה עצמו:

#include <array>
#include <atomic>
#include <cassert>
#include <thread>

bool try_increment(std::atomic<unsigned>& value, unsigned limit) {
    unsigned expected = value.load(std::memory_order_relaxed);
    while (expected < limit) {
        if (value.compare_exchange_weak(
                expected, expected + 1,
                std::memory_order_relaxed,
                std::memory_order_relaxed))
            return true;
    }
    return false;
}

int main() {
    std::atomic<unsigned> value{0};
    constexpr unsigned limit = 10000;
    std::array<std::jthread, 4> workers;
    for (auto& worker : workers) {
        worker = std::jthread([&] {
            while (try_increment(value, limit)) {}
        });
    }
    for (auto& worker : workers) worker.join();
    assert(value.load() == limit);
    assert(!try_increment(value, limit));
    assert(value.load() == limit);
}

ה־expected מתעדכן בכישלון, ולכן הלולאה בוחנת מחדש את הגבול לפני החישוב הבא. כאשר expected קטן מן limit, הביטוי expected ועוד אחד אינו עובר את הגבול של unsigned, גם אם limit הוא הערך המרבי של הטיפוס. אין צורך להניח ש־CAS מצליח בכל ניסיון.

פרסום שחסרה בו חוליה

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

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

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

גבולות הבדיקה

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