לדלג לתוכן

8.6 - מנוע רב־תהליכוני וסביבת טוקיו - פתרון

פתרון גבולות המשימה

פתרון ראשון - המצב שחוצה השהיה

בפתרון הראשון המונה המשותף מתאים להעברה בין תהליכונים. בפתרון השני אין מונה משותף שנשמר בנקודת ההשהיה.

use std::sync::Arc;
use std::rc::Rc;

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        let value = Arc::new(7);
        tokio::task::yield_now().await;
        assert_eq!(*value, 7);
    }).await.unwrap();

    tokio::spawn(async {
        {
            let value = Rc::new(7);
            assert_eq!(*value, 7);
        }
        tokio::task::yield_now().await;
    }).await.unwrap();
}

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

פתרון שני - היתר בבעלות העובד

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

use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use std::time::Duration;

#[tokio::main]
async fn main() {
    let permits = Arc::new(tokio::sync::Semaphore::new(2));
    let active = Arc::new(AtomicUsize::new(0));
    let maximum = Arc::new(AtomicUsize::new(0));
    let completed = Arc::new(AtomicUsize::new(0));
    let mut handles = Vec::new();
    for _ in 0..6 {
        let permit = permits.clone().acquire_owned().await.unwrap();
        let active = active.clone();
        let maximum = maximum.clone();
        let completed = completed.clone();
        handles.push(tokio::task::spawn_blocking(move || {
            let _permit = permit;
            let now = active.fetch_add(1, Ordering::SeqCst) + 1;
            maximum.fetch_max(now, Ordering::SeqCst);
            std::thread::sleep(Duration::from_millis(20));
            active.fetch_sub(1, Ordering::SeqCst);
            completed.fetch_add(1, Ordering::SeqCst);
        }));
    }
    drop(handles.pop());
    for handle in handles { handle.await.unwrap(); }
    while completed.load(Ordering::SeqCst) != 6 {
        tokio::time::sleep(Duration::from_millis(1)).await;
    }
    assert!(maximum.load(Ordering::SeqCst) <= 2);
    assert_eq!(active.load(Ordering::SeqCst), 0);
}

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

פתרון שלישי - יקיצה בזמן ריצה

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

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