8.7 - ביטול והצטרפות למשימות תלויות - פתרון
פתרון חיי הפעולה¶
פתרון ראשון - מצב מחוץ לעתיד המתבטל¶
אפשר להחליף את גוף הפונקציה הראשית בניסוי הבא ולהשאיר את הטיפוס Frame ואת הפונקציה fill מההרצאה. אנחנו כותבים כל מקטע בעצמנו לפני יצירת העתיד, ולכן אין תלות בתזמון תהליכון נוסף.
#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
let (mut reader, mut writer) = tokio::io::duplex(64);
let mut frame = Frame::default();
for (part, expected) in [(b"ab", 2), (b"cd", 4)] {
writer.write_all(part).await?;
tokio::select! {
biased;
result = fill(&mut reader, &mut frame) => {
panic!("unexpected completion: {result:?}");
}
_ = std::future::ready(()) => {}
}
assert_eq!(frame.filled, expected);
}
writer.write_all(b"ef").await?;
fill(&mut reader, &mut frame).await?;
assert_eq!(frame.filled, 6);
assert_eq!(&frame.bytes, b"abcdef");
Ok(())
}
הבאפר וההיסט חיים מחוץ לענף הבחירה, ולכן השמדת העתיד אינה משמידה אותם. הקריאה הבאה ממשיכה מההיסט השמור. אם היינו יוצרים Frame::default() בכל ניסיון, היינו מאבדים את ההתקדמות הלוגית למרות שהזיכרון עצמו היה בטוח.
פתרון שני - בקשת ביטול ואישור ניקוי¶
התוכנית הבאה משתמשת בתכונות טוקיו rt, macros, sync ו־time. הערוץ מוודא שהמשאב אכן נוצר לפני שהילד הראשון מדווח על כשל.
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use tokio::task::JoinSet;
struct Resource(Arc<AtomicUsize>);
impl Drop for Resource {
fn drop(&mut self) { self.0.fetch_add(1, Ordering::SeqCst); }
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let drops = Arc::new(AtomicUsize::new(0));
let (started, ready) = tokio::sync::oneshot::channel();
let mut children: JoinSet<Result<(), &'static str>> = JoinSet::new();
let child_drops = drops.clone();
children.spawn(async move {
let _resource = Resource(child_drops);
started.send(()).unwrap();
std::future::pending::<()>().await;
Ok(())
});
children.spawn(async move {
ready.await.unwrap();
Err("planned failure")
});
let first = children.join_next().await.unwrap().unwrap();
assert_eq!(first, Err("planned failure"));
children.abort_all();
while let Some(result) = children.join_next().await {
assert!(result.unwrap_err().is_cancelled());
}
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
הבדיקה של מונה ההשמדות מתבצעת אחרי ההצטרפות, ולא מיד אחרי בקשת הביטול. כך היא בודקת את סיום הניקוי בפועל. בתוכנית כללית אין להניח שכל תוצאה שנותרה היא ביטול, משום שילד אחר יכול להספיק להשלים במקביל לבקשת העצירה; כאן מבנה הניסוי מבטיח שהילד הנותר ממתין לנצח.
פתרון שלישי - אי ידיעה היא מצב תקין¶
אחרי שליחה בלי אישור, הלקוח אינו יודע אם השרת טרם קיבל, קיבל אך לא ביצע, ביצע אך לא שלח אישור, או שלח אישור שאבד. ניסיון חדש בלי זהות משותפת יכול להיראות לשרת כפעולה נוספת ולהגדיל את המונה פעמיים.
אפשר לצרף מזהה בקשה יציב ולשמור בשרת את התוצאה לצד אותו מזהה, כך שחזרה על אותה בקשה תחזיר את התוצאה הקודמת לפי חוזה שמירת המזהים. צריך להגדיר גם משך שמירה, התאוששות מקריסה וזהות לקוח. הוספת מספר אקראי לבקשה לבדה אינה מספקת את כל ההבטחה. עד שהפרוטוקול מברר את התוצאה, המצב הנכון לדיווח הוא תוצאה לא ידועה, ולא הצלחה ולא ביטול ודאי.