ב-Multithreading קיימים שני משפחות של סוגי נעילות – Exclusive Locks ו-Non-Exclusive, בפרק הקודם ביצענו נעילה אקסקלוסיבית באמצעות Lock.
בפרק זה של המדריך ננעל מקטע קריטי באמצעות Monitor
לאחר שהבנו את עקרון הסינכרוניות בתכנות מקבילי, נוכל להיכנס לנושא בצורה יותר עמוקה.
כפי שהוסבר בפרק הקודם, קיימים מספר סוגים של נעילות אקסקלוסיביות ומספר סוגים של נעילות שאינן אקסקלוסיביות.
בפרקים הקרובים נעבור על כל הסוגים, כאשר נתחיל מהנעילות האקסקלוסיביות – מלבד Lock שבו דנו בפרק הקודם.
מחלקת Monitor
באמצעות מחלקת Monitor נוכל לספק לתוכנית שלנו מעין מכניקה אשר אחראית על סנכרון גישה לאובייקט.
בדומה ל-Lock, גם Monitor יאפשר גישה לאובייקט ל-Thread אחד כל פעם בזמן נתון.
מחלקה סטטית זו שייכת ל- System.Threading, וכמחלקה סטטית היא מספקת לנו מתודות סטטיות שונות לצורך ניהול הגישה:
כפי שניתן לראות, למחלקת Monitor קיימות מספר מתודות שלחלקן מספר העמסות.
כעת נבאר את אופן הפעולה של כל אחת מהן.
Enter
- Enter(object obj) – במתודה זו נשתמש על מנת להחיל נעילה אקסקלוסיבית על אובייקט מסוים.
במידה והאובייקט שנשלח כפרמטר יהיה שווה ל-Null, תיזרק שגיאת ArgumentNullException.
- Enter(object obj, ref bool lockTaken) – באמצעות העמסה זו גם נחיל נעילה אקסקלוסיבית על אובייקט,
רק שכאן יוגדר ערך בוליאני שנשלח כ-ref parameter בנוסף שיגיד לנו אם האובייקט נעול כרגע.
כלומר שהערך חייב להיות False על מנת שתינתן גישה לאובייקט,
משום שבמידה והערך הוא True, סימן שהאובייקט נעול כרגע.
TryEnter
- public static bool TryEnter(object obj) – מנסה להחיל נעילה אקסקלוסיבית על אובייקט, מחזיר bool.
- public static bool TryEnter(object obj, TimeSpan timeout) – מנסה להחיל נעילה אקסקלוסיבית על אובייקט במשך זמן שהגדרנו (יחידות זמן TimeSpan), מחזיר bool.
- public static bool TryEnter(object obj, int millisecondsTimeout) – מנסה להחיל נעילה אקסקלוסיבית על אובייקט במשך זמן שהגדרנו (מילי-שניות), מחזיר bool.
- public static void TryEnter(object obj, int millisecondsTimeout, ref bool lockTaken) – מנסה להחיל נעילה אקסקלוסיבית על אובייקט במשך זמן שהגדרנו (מילי-שניות).
רק שכאן יוגדר ערך בוליאני שנשלח כ-ref parameter בנוסף שיגיד לנו אם האובייקט נעול כרגע, מחזיר void.
- public static void TryEnter(object obj, ref bool lockTaken) – מנסה להחיל נעילה אקסקלוסיבית על אובייקט,
גם כאן יוגדר ערך בוליאני שנשלח כ-ref parameter בנוסף שיגיד לנו אם האובייקט נעול כרגע, מחזיר void.
- public static void TryEnter(object obj, TimeSpan timeout, ref bool lockTaken) – מנסה להחיל נעילה אקסקלוסיבית על אובייקט במשך זמן שהגדרנו (יחידות זמן TimeSpan).
רק שכאן יוגדר ערך בוליאני שנשלח כ-ref parameter בנוסף שיגיד לנו אם האובייקט נעול כרגע, מחזיר void.
כלומר שכל המתודות מסוג Enter ו-TryEnter אחראיות על תחילת סימון מקטע קריטי אשר נעול באופן אקסקלוסיבי עבור אובייקט.
Wait
- public static bool Wait(object obj) – משחרר את הנעילה מהאובייקט הנוכחי וחוסם אותו עד אשר תחזור הגישה.
- public static bool Wait(object obj, TimeSpan timeout) – משחרר את הנעילה מהאובייקט הנוכחי וחוסם אותו עד אשר תחזור הגישה,
או שהגיע זמן ה-Timout (יחידות זמן TimeSpan) – מה שמגיע קודם.
- public static bool Wait(object obj, int millisecondsTimeout) – אותו הדבר רק שכאן יחידות הזמן ל-Timeout נמדדות במילי-שניות.
- public static bool Wait(object obj, TimeSpan timeout, bool exitContext) – משחרר את הנעילה מהאובייקט הנוכחי וחוסם אותו עד אשר תחזור הגישה,
או שהגיע זמן ה-Timout (יחידות זמן TimeSpan) – מה שמגיע קודם.
ניתן לצאת מסנכרון ולחזור אליו לאחר מכן.
- public static bool Wait(object obj, int millisecondsTimeout, bool exitContext) – אותו הדבר רק שכאן יחידות הזמן ל-Timeout נמדדות במילי-שניות.
כל המתודות מסוג Wait נועדו לשחרר נעילה מהאובייקט ולחסום את ה-Thread הזה מלרוץ עד אשר תוסר הנעילה שוב.
שימו לב כי כל ההעמסות של Wait מחזירות ערך בוליאני.
הן מחזירות לנו True במידה והנעילה שוחררה והאובייקט השיג את הגישה מחדש.
Pulse & PulseAll
- Pulse(object obj) – מתודה זו מודיעה ל-Thread כאשר חל שינוי בתור ההמתנה לגישה למקטע הקריטי.
במידה והאובייקט שנשלח כפרמטר יהיה שווה ל-Null, תיזרק שגיאת ArgumentNullException.
- PulseAll(object obj) – מתודה זו מודיעה לכל ה-Threads כאשר חל שינוי בתור ההמתנה לגישה למקטע הקריטי.
במידה והאובייקט שנשלח כפרמטר יהיה שווה ל-Null, תיזרק שגיאת ArgumentNullException.
Exit
- Exit(object obj) – משחרר את הנעילה האקסקלוסיבית של האובייקט הנוכחי.
במידה והאובייקט שנשלח כפרמטר יהיה שווה ל-Null, תיזרק שגיאת ArgumentNullException.
IsEntered
- IsEntered(object obj) – בודק אם ה-Thread הנוכחי מחזיק את הנעילה למקטע הקריטי, מחזיר True אם כן.
במידה והאובייקט שנשלח כפרמטר יהיה שווה ל-Null, תיזרק שגיאת ArgumentNullException.
שימוש במחלקה
שימו לב ל-Code Snippet הבא שבו נדגים כיצד לנהל גישה למקטע קריטי על ידי שימוש במתודות Enter ו-Exit של מחלקת Monitor:
using System.Threading; namespace ThreadingDemo { class Program { static readonly object lockObject = new(); static void Main(string[] args) { Thread[] threads = new Thread[3]; for (int i = 0; i < 3; i++) { threads[i] = new(PrintNumbers); threads[i].Name = "Child Thread " + i; } foreach (Thread t in threads) { t.Start(); } } public static void PrintNumbers() { Console.WriteLine(Thread.CurrentThread.Name + " Trying to enter"); Monitor.Enter(lockObject); Console.WriteLine(Thread.CurrentThread.Name + " Entered into the critical section"); for (int i = 0; i < 5; i++) { Thread.Sleep(100); Console.Write(i + ","); } Console.WriteLine(); Monitor.Exit(lockObject); Console.WriteLine(Thread.CurrentThread.Name + " Exit from critical section"); } } }
כפי שניתן לראות בדוגמא פשוטה זו, הגישה למקטע הקריטי ניתנה לכל Thread בצורה מבוקרת,
כלומר שה-Threads קיבלו את הגישה לפי תור, כאשר בכל פעם Thread אחד בודד היה בעל גישה למשאב המשותף.
קיימים מצבים שבהם נרצה לנהל בצורה אחרת את הגישה למקטע הקריטי.
שימו לב לדוגמא הבא שבה נדפיס סדרה בצורה מסודרת,
כאשר Thread אחד יהיה אחראי על מספרים זוגיים, ו-Thread נוסף על האי-זוגיים.
נוכל להשתמש ב-Pulse ו-Wait על מנת להפסיק\להמשיך פעולה של Thread לפי הצורך:
using System.Threading; namespace ThreadingDemo { class Program { const int numberLimit = 20; static readonly object lockMonitor = new(); static void Main(string[] args) { Thread evenThread = new(PrintEvenNumbers); Thread oddThread = new(PrintOddNumbers); evenThread.Start(); Thread.Sleep(100); oddThread.Start(); oddThread.Join(); evenThread.Join(); } static void PrintEvenNumbers() { Monitor.Enter(lockMonitor); for (int i = 0; i <= numberLimit; i += 2) { Console.Write($"{i} "); Monitor.Pulse(lockMonitor); bool isLast = false; if (i == numberLimit) { isLast = true; } if (!isLast) { Monitor.Wait(lockMonitor); } } Monitor.Exit(lockMonitor); } static void PrintOddNumbers() { Monitor.Enter(lockMonitor); for (int i = 1; i <= numberLimit; i += 2) { Console.Write($"{i} "); Monitor.Pulse(lockMonitor); bool isLast = false; if (i == numberLimit - 1) { isLast = true; } if (!isLast) { Monitor.Wait(lockMonitor); } } Monitor.Exit(lockMonitor); } } }
על מנת שנוכל לקבל את התוצאה הבא:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
מה קרה כאן הרגע?
- יצרנו 2 Threads – אחד עבור הדפסת המספרים הזוגיים, ואחד עבור האי-זוגיים (שורות 11-12)
- בשורה 13 התחלנו את evenThread
- שימו לב כי הקפאנו את התכנית ל 10 מילי-שניות מיד לאחר מכן,
זה כדי למנוע מצב שבו ה-Thread השני יכנס לפעולה קודם (שורה 15) - ובשורה 17 התחלנו את oddThread
- בתוך הפונקציה PrintEvenNumbers (שורה 30) הפעלנו את המתודה pulse,
כאמור, תפקידה להודיע על שינוי בתור לגישה למקטע הקריטי - לאחר הדפסת המספר ובדיקה אם מדובר במספר האחרון – כל עוד לא מדובר במספר האחרון,
תופעל המתודה Wait, כך שה-Thread יקפיא את פעילותו וימתין ל-Pulse שהמקטע הקריטי שוב פנוי (שורות 32-40) - כך שלאחר שהודפס המספר האחרון ה-Thread יצא מהמקטע הקריטי באמצעות Exit (שורה 43)
- הפונקציה PrintOddNumbers בנויה באותו האופן בדיוק, למעט הבדל אחד –
פונקציה זו תדפיס את המספרים האי-זוגיים.
החסרונות בעבודה עם Lock ו-Monitor
באמצעות העבודה עם Lock ו-Monitor אנו מוודאים שהקוד שלנו הוא Thread-Safe,
כלומר שהמקטע שמסומן כקריטי ינוהל על ידי Thread בודד אחד בכל פעם.
למרות זאת, קיימות מספר מגבלות בשימוש זה.
שיטה זו אכן מגנה על המקטע מפני Threads אחרים אשר קיימים בתוך התהליך הנוכחי,
אולם במידה ומקור ה-Thread שינסה להיכנס למקטע הוא מאפליקציה אחרת או תהליך אחר למשל,
ל-Lock ו-Monitor לא תהיה שליטה עליהם.
בפרק הבא נכיר את מחלקת Mutex שבאה לפתור לנו בעיה זו בדיוק.
לקריאה מורחבת על Thread ו-Threading באתר של מייקרוסופט יש ללחוץ כאן.