לכל ת'רד שרץ תחת מערכת ההפעלה יש עדיפות, שיכולה להקבע על ידי.. כל אחד.
אלה יכולים להיות אתם, המפתחים, שדואגים להעניק עדיפות מיוחדת לת'רד שיצרתם,
או שאולי זה יכול גם להיות משתמש שובב, שקם בבוקר והחליט לפתוח את ה-Task
Manager ולהכניס תהליך שלם לעדיפות Realtime (למעשה, לקבוע את ה-Priority
Class של התהליך, שבתורו משפיע על העדיפות שכל ת'רד באותו תהליך מקבל).
בתיאוריה,
כיול ומענק חכם של עדיפויות יכול להביא לשיפור בתגובתיות ובביצועים של
המערכת. אולם, במציאות.. סביר להניח שלא רק שהמשחקים האלה לא יביאו לשיפור
אמיתי בביצועים, אלא שהם יביאו עמם כל מיני התנהגויות "מוזרות" ו"בלתי
צפויות". כאלה מהסוג שאף אדם שפוי לא ירצה להתחיל לדבג בערב יום חמישי,
עמוק אל תוך הלילה.. (למזלנו, מסתובבים בינינו מספיק אנשים שלוקים מעט
בשפיותם.. :)
העובדה היא, שאין לכם שליטה מלאה על האפליקציה שלכם, או
מערכת ההפעלה. אתם לא _באמת_ יודעים מה קורה מסביבכם כשהתוכנית שלכם רצה
באתר הלקוח, 20 אלף קילומטרים מכם. רוב הזמן, אין לנו שליטה מלאה על
התהליכים שרצים במקביל אלינו, או אפילו באותו תהליך איתנו. אנחנו לא
מכירים את הדרישות שלהם, המבנה שלהם, מספר הת'רדים, עדיפויות וכו'.. כל זה
גורם לכל משחק העדיפויות הזה שקבענו במערכת שלנו למסוכן עוד יותר. זאת
הסיבה שאם תציצו לרגע ב-Task Manager אצלכם, תוכלו לראות שבאופן כמעט
מוחלט, כל התהליכים שרצים כרגע עובדים על עדיפות Normal. בצורה הזאת אנחנו
נותנים ל-Scheduler של Windows לעשות את העבודה שלו בצורה הטובה ביותר.
הוא ידאג בעצמו לתזמן את הת'רדים בזמן שנראה לו מתאים, על פי שיקולים הרבה
יותר טובים מאלו שאנחנו מוסגלים לעשות. כך שבאותה הרוח שלא מומלץ להפריע ל-Garbage Collector ולהתחיל לדחוף בו ולגרום לו להפעיל Collection'ים דרך קריאה ל-Collect מתי "שנראה לנו", אנחנו גם לא צריכים להיות "חכמים" ולהחליט בעצמנו איזה ת'רד כן צריך לקבל עדיפות, ואיזה לא.
Priority Boosts
למזלנו,
אותו Thread Scheduler שאחראי על חלוקת זמן המעבד בין הת'רדים השונים,
פותר בין היתר "Deadlock'ים בפוטנציה" שעלולים להגרם מתוך חלוקה מסוכנת של
עדיפויות לת'רדים שיש ביניהם אינטרקציה כלשהיא.
אפשר להדגים את אותו
סיכון דרך בעיה מוכרת הנקראת Priority Inversion. קחו לדוגמה את הסיטואציה
הבאה: יש לנו 3 ת'רדים במערכת, הראשון (A) עם עדיפות גבוהה, השני (B) עם
עדיפות בינונית, והשלישי (C) עם עדיפות נמוכה. עכשיו מה שקורה זה דבר כזה,
ת'רד C נכנס ל-Critical Zone ונועל את הכניסה לקטע הקוד. ת'רד A מגיע גם
כן לאותו קטע קוד, אבל נעצר תחת הנעילה (מחכה שת'רד C ישחרר אותה). והנה,
בלי ששמנו לב, נוצר לנו Deadlock. מה שקורה זה שכעת ת'רד B, בעל העדיפות
הבינונית, מקבל את כל זמן המעבד ורץ ללא הפסקה. זאת מאחר ות'רד A בעל
העדיפות הגבוהה מחכה בנעילה, ות'רד C בעל העדיפות הנמוכה לא מספיק אף פעם
לשחרר אותה (מאחר ות'רד B לא מפסיק לעבוד בגלל שיש לו עדיפות גבוהה על פני
ת'רד C).
אולם בפועל, הסיטואציה הזאת לא תקרה (או לפחות, סביר להניח
שלא תקרה, אלא אם כן עשיתם משהו מאוד רע שתכף אדבר עליו). מה שקורה, זה
שאותו Scheduler משתמש בת'רד פרטי ונסתר שמתעורר כל שניה וסוקר את כל
הת'רדים שממתינים לתזמון. באותו סיקור, הוא בודק אילו ת'רדים לא תוזמנו
כבר זמן ממושך למדי (באופן גס, מדובר על סדר גודל של 3.5~ שניות). בצורה
הזאת, הוא מבחין אילו ת'רדים יתכן וסובלים מ-Starvation (כפי שקורה בדוגמה
אצל ת'רד C). כשהוא מזהה מקרה שכזה, הוא מעניק לאותו ת'רד Priority Boost
שמעלה את ה-Priority שלו הישר ל-15 (Time Critical). בצורה הזאת, הוא כמעט
ומבטיח שאותו ת'רד יהיה בין הת'רדים הראשונים שיתוזמנו ויקבלו זמן לעבוד.
ביחד עם אותו Priority Boost, אותו ת'רד יכול לקבל גם Quantum Boost,
בגודל של פי 2 או אפילו פי 4 מה-Quantum הרגיל (תלוי בהאם התוכנית רצה על
עמדת לקוח או שרת). אותו Boost ימנע את ה-Deadlock שראינו, בכך שהוא מעניק
מספיק זמן מעבד לת'רד C, שיוכל לשחרר כעת את הנעילה ולתת לתוכניות להמשיך
לרוץ כרגיל.
מקרה אחר בו אנחנו יכולים להתקל באותה צרה, היא כאשר יש
לנו ת'רד בעל עדיפות גבוהה שרץ תחת איזשהו Spin Cycle שבכל הפעלה בודק
תנאי בוליאני שאומר לו האם הוא צריך להפסיק לעבוד, או להמשיך ל-Cycle נוסף
(בפועל, סוג של Busy Loop/SpinWait). הבעיה היא, שהת'רד האחר, שצריך לעדכן
את התוצאה של אותו תנאי בוליאני, הינו בעל עדיפות נמוכה יותר מזה של הת'רד
הראשון. לכן, יכול לקרות מצב בו הת'רד הראשון יעבוד ללא הפסקה, בעוד הת'רד
השני לעולם לא יספיק לעדכן את התנאי הבוליאני, ולעצור את ה-Busy Loop אליו
נכנס הת'רד הראשון. במקרה הזה, ניתן לפתור את הבעיה גם בשימוש ב-Sleep
(השם ישמור), אבל זה כבר נושא שאדון בו כבר בפוסט נפרד. ולחשוב.. שכל זה היה יכול להמנע אם היינו מתאפקים, ולא משחקים בעדיפויות הת'רדים.
צריך
לציין, שהדוגמאות האלו מושפעות גם באופן ישיר בשאלה האם התוכנית שלנו רצה
על תחנה בעלת מעבד בודד, או כזה בעל מספר ליבות/מעבדים. אבל בכל אופן,
המסר הוא אותו מסר - לא משחקים עם עדיפויות.
מוקדם יותר ציינתי שאותו Boost לא בהכרח מבטיח להוציא אותנו מהמבוי הסתום הזה. זה יכול לקרות במידה והגדרנו בעצמנו ת'רדים עם עדיפות גבוהה מ-15. במקרה כזה, יתכן שגם לאחר הענקת ה-Boost, הת'רד עדיין יהיה בעדיפות נמוכה יחסית לת'רדים עם העדיפות הסופר-גבוהה שהחלטנו לתת על דעת עצמנו.
בנוסף למנגנון למניעת
Starvation, ה-Scheduler יכול להחליט להעניק Boost'ים גם על פי מספר
פרמטרים אחרים. למשל, תוכניות בעלות חלון שנמצא ב-Foreground יקבלו עדיפות
גבוהה יותר מאשר תוכניות שרצות ב-Background (בפועל, ניתן לקנפג את הפרמטר
הזה תחת Windows). נוסף על כך, ת'רדים ממתינים שקיבלו Signal'ים אודות
WaitHandle כלשהו, יכולים לקבל Boost רגעי גם כן.
ישנם מאמרים
שיכולים לתת לכם "טיפים" וכל מיני שאר עצות אחיתופל לגבי סידור כדאי של
עדיפויות. למשל שת'רד שתפקידו לקבל קלטים כדאי שיהיה בעל עדיפות גבוהה,
ולעומתו ת'רד שרץ ברקע, ועושה שימוש כבד במעבד, כדאי לקבוע עם עדיפות
נמוכה (כך שיהיה ניתן לעצור אותו בקלות ולחלוק את זמן המעבד גם עם ת'רדים
אחרים). אולם, כל המשחקים הקטנים האלה יכולים להביא לכל כך הרבה התנהגויות
לא צפויות שאין לנו דרך בכלל להבין איך הן יכולות באמת להשפיע על התוכנית
שלנו, ובין כה וכה, ה-Scheduler יודע כבר מספיק טוב איך לתזמן בצורה יעילה
את הת'רדים השונים הקיימים במערכת, כך שאין לנו במה להתעסק כאן. בדרך כלל,
אני מטיל ספק באדם הממוצע שחושב שהוא יותר חכם מהאנשים שתכננו ומימשו את
ה-Thread Scheduler, אבל עם זאת, קיימים מקרים מעט חריגים בהם כמה נגיעות ותפירות של העדיפויות לצורכי האפליקציה יכולים להביא לשיפור חלוקת העבודה בין הת'רדים השונים. אבל כרגיל, מילת המפתח היא "בזהירות".