נעילה והמתנה לפי תנאי¶
נניח שעובד אחד מכין תוצאה ועובד אחר צריך להשתמש בה רק אחרי שהיא מוכנה. לולאה שקוראת שוב ושוב דגל יכולה לבזבז מעבד, והמתנה ללא בדיקת מצב יכולה להחמיץ יקיצה. נרצה לחבר בין מצב מוגן לבין מנגנון שמאפשר לשחרר את הנעילה ולהירדם בלי להשאיר חלון שבו ההודעה הולכת לאיבוד.
ה־Mutex מגן על אינווריאנט של נתונים, לא רק על שורת קוד בודדת. אם המצב אומר שתוצאה מוכנה, התוכן שלה צריך להיות עקבי באותו רגע. ה־Condvar מאפשר להמתין לשינוי שעשוי להפוך את התנאי לנכון. ההתראה עצמה אינה התוצאה ואינה מונה של כל שינוי שהתרחש.
הקוד הבא מדגים העברת תוצאה אחת. השדה Option הוא התנאי שעליו מחכים, והבדיקה נעשית תחת אותה נעילה שמגינה על הפרסום.
use std::sync::{Arc, Condvar, Mutex};
fn main() {
let shared = Arc::new((Mutex::new(None::<Result<u64, String>>), Condvar::new()));
let worker_state = Arc::clone(&shared);
let worker = std::thread::spawn(move || {
let result = "42".parse::<u64>().map_err(|error| error.to_string());
let (lock, ready) = &*worker_state;
let mut state = lock.lock().expect("state poisoned");
*state = Some(result);
drop(state);
ready.notify_one();
});
let (lock, ready) = &*shared;
let state = lock.lock().expect("state poisoned");
let mut state = ready.wait_while(state, |state| state.is_none()).expect("state poisoned");
let result = state.take().unwrap();
drop(state);
worker.join().unwrap();
assert_eq!(result, Ok(42));
}
ההמתנה מקבלת guard ומשחררת את הנעילה כחלק מהמעבר להמתנה. כאשר היא חוזרת, ה־guard שוב מחזיק את הנעילה והתנאי נבדק מחדש. אם הפרסום התרחש לפני שהקורא התחיל להמתין, הקורא יראה Some ולא יירדם. אם התרחשה יקיצה ללא תוצאה, הלולאה תמשיך להמתין.
זאת הסיבה ש־if בודד אינו מספיק במקום לולאה. יקיצה יכולה להיות spurious, או להתייחס לשינוי שעובד אחר כבר צרך. במערכת עם כמה צרכנים, העובדה שתהליכון התעורר אינה אומרת שהפריט עדיין שייך לו. ההחלטה מתקבלת רק אחרי רכישת הנעילה ובדיקת המצב.
ההתראה יכולה להישלח אחרי שחרור הנעילה, כמו בדוגמה, משום שהשינוי בתנאי כבר בוצע תחת הנעילה. אם נפריד את בדיקת התנאי ממנגנון ההמתנה בצורה לא נכונה, נקבל lost wakeup: המפיק משנה מצב ומודיע בזמן שהצרכן עדיין אינו ממתין, ואז הצרכן נרדם בלי לבדוק שוב.
נבחן כעת כשל. אם העובד נכנס לפאניקה לפני פרסום התוצאה, הדוגמה אינה כוללת מצב סגירה שיאפשר לממתין להפסיק. זהו גבול של פרוטוקול תוצאה יחידה שהכנו עבור מסלול הצלחה ושגיאת parse רגילה. בפרויקט נבנה סגירה וביטול מפורשים, ובתרגול נוסיף מצב סיום שאינו תלוי בקיום תוצאה.
פאניקה בתוך מקטע קריטי יכולה לסמן Mutex כ־poisoned. הסימון אומר שיש חשש שהמצב הלוגי נשאר חלקי, ואינו מתקן אותו. הקריאה into_inner על שגיאת poison מאפשרת להגיע לנתונים, אבל אינה הוכחה שהאינווריאנט נשמר. צריך לבדוק ולתקן את המצב או להחזיר כשל למעלה.
גם אסור לבסס soundness של קוד unsafe רק על ההנחה ש־poison תמיד יסמן כל מצב מסוכן. מנגנון poison הוא אזהרה לוגית עם מגבלות, ולא מערכת הוכחה שחוסמת את כל הגישות לנתונים אחרי כל סוג פאניקה. הפשטה בטוחה צריכה לשמור את חובות הזיכרון שלה גם כאשר לקוח בוחר לטפל בשגיאה.
המקטע הקריטי צריך להיות קצר במידה שמתאימה לעומס. אם נחזיק נעילה בזמן קלט ופלט חסום, כל מי שצריך את המצב יחכה גם להתקן או לרשת. לפעמים אפשר להוציא עבודה מהנעילה באמצעות snapshot בבעלות, לבצע אותה ואז לחזור ולאמת שמספר הגרסה עדיין מתאים. גם כאן צריך לתכנן מה קורה אם המצב השתנה בינתיים.
החוזים מופיעים בתיעוד Mutex ובתיעוד Condvar. לכל המתנה נכתוב במשפט אחד מהו התנאי שמאפשר התקדמות ומי מסוגל לשנות אותו ולהעיר אותנו.