לדלג לתוכן

פתרון בעלות ותיאום בין תהליכונים

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

הקטע הבא מצטרף לקוד ההרצאה אחרי Channel ומחליף את ה־main. הוא דורש את הכותרות המופיעות בתחילתו:

#include <algorithm>
#include <cassert>
#include <vector>

int main() {
    Channel channel(1);
    std::vector<int> first, second;
    auto consume = [&](std::vector<int>& values) {
        while (auto value = channel.pop()) values.push_back(*value);
    };
    std::jthread c1([&] { consume(first); });
    std::jthread c2([&] { consume(second); });
    std::jthread p1([&] {
        for (int i = 0; i < 100; ++i)
            if (!channel.push(i)) break;
    });
    std::jthread p2([&] {
        for (int i = 100; i < 200; ++i)
            if (!channel.push(i)) break;
    });
    p1.join();
    p2.join();
    channel.close();
    c1.join();
    c2.join();
    first.insert(first.end(), second.begin(), second.end());
    std::sort(first.begin(), first.end());
    assert(first.size() == 200);
    for (int i = 0; i < 200; ++i)
        assert(first[static_cast<std::size_t>(i)] == i);
}

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

סגירה בלי אובדן בעלות

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

צילום מצב צריך להחזיק את ה־mutex פעם אחת ולבנות ערך הכולל את שני השדות:

struct Snapshot { std::size_t size; bool closed; };

// Place inside Channel's public section.
Snapshot snapshot() {
    std::lock_guard lock(mutex_);
    return {queue_.size(), closed_};
}

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

התזמון הפגום

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

ה־invariant הוא שכל שינוי ובדיקה של התור ושל דגל הסגירה נעשים תחת אותו mutex. ההתראות אינן נושאות את העבודה; הן רק גורמות לבחינה מחדש של ה־predicate. ההבחנה הזו נשארת נכונה גם כשמחליפים את ספריית הסנכרון.