בפרקים הקודמים של המדריך למדנו כיצד ביכולתנו להגיע לסינכרוניות בתכנות מקבילי על ידי שימוש בנעילות אקסקלוסיביות (Lock, Monitor & Mutex).
בפרק זה נלמד על נעילות שאינן אקסקלוסיביות – Semaphore & SemaphoreSlim.
כפי שראינו, לנעילה אקסקלוסיבית יש את היכולת לנעול את המקטע באופן אקסקלוסיבי עבור Thread אחד בודד בזמן נתון,
אך מה אם היינו מעוניינים להעניק גישה ליותר מ-Thread אחד?
לשם כך, בשפת #C קיימות המחלקות Semaphore ו-SemaphoreSlim.
במאמר זה נבאר את אופן פעולתן של מחלקות אלו ונציג דוגמאות קוד.
מחלקת Semaphore
באמצעות Mutex הגבלנו את הגישה ל-Thread בודד בתוך תהליך בודד,
כאשר באמצעות מחלקת Semaphore נוכל לשלוט במספר ה-Threads שיכולים לגשת למשאב המשותף,
או במילים אחרות – נגביל את הגישה למקטע הקריטי ל-Thread אחד או יותר.
כעת נעבור ונכיר את הבנאים והמתודות של המחלקה.
הבנאים של מחלקת Semaphore
כפי שניתן לראות בתמונה, למחלקה זו 3 בנאים:
Semaphore(int initialCount, int maximumCount) – מאתחל מופע חדש של המחלקה, כאשר כפרמטרים נשלח לו מספר התחלתי ומספר מקסימלי של בקשות גישה למקטע.
Semaphore(int initialCount, int maximumCount, string? name) – מאתחל את המופע, כאשר כפרמטרים נשלח לו מספר התחלתי ומספר מקסימלי של בקשות גישה,
הפרמטר השלישי שנשלח הוא שם למופע.
Semaphore(int initialCount, int maximumCount, string name, out bool createdNew) – מאתחל מופע, כאשר כפרמטרים נשלח לו מספר התחלתי ומספר מקסימלי של בקשות גישה,
הפרמטר השלישי שנשלח הוא שם למופע והרביעי הוא Out Parameter בוליאני שנועד לווידוא כי אכן נוצר מופע חדש של Semaphore.
המתודות של מחלקת Semaphore
OpenExisting(string name) – במתודה סטטית זו נשתמש על מנת להקנות גישה לאובייקט Semaphore לפי השם שלו – במידה והוא קיים.
TryOpenExisting(string name, [NotNullWhen(true)] out Semaphore? result) – מתודה סטטית אשר משמשת להקניית גישה לאובייקט Semaphore לפי השם שלו – במידה והוא קיים.
אם האופרציה הצליחה יוחזר לנו האובייקט כתוצאה, במקרה ונכשלה יוחזר לנו Null.
()Release – יוצא מה-Semaphore ומחזיר לנו את ספירת הכניסות האחרונה שנרשמה.
Release(int releaseCount) – יוצא מה-Semaphore מספר פעמים (כמספר הפעמים שנשלח כפרמטר),
ומחזיר לנו את ספירת הכניסות האחרונה שנרשמה.
שימו לב כי מחלקה זו יורשת ממחלקת WaitHandle אשר לה מתודות שנועדו לניהול כניסה-המתנה של אובייקט למקטע קריטי,
אם כך נוכל להשתמש במתודות שלה לצרכינו, למשל ב-WaitOne:
()WaitOne – במתודה זו נוכל להשתמש על מנת להקנות גישה למקטע הקריטי בשילוב עם אובייקט ה-Semaphore שלנו,
כאשר אם מספר ה-Threads המנוהלים כרגע גדול מ-0.
שימו לב ל-Code Snippet הבא שבו נדגים כיצד מחלקת Semaphore מאפשרת לנו להכניס יותר מ-Thread אחד במקביל למקטע הקריטי:
using System.Threading; namespace ThreadingDemo { public class Program { public static Semaphore semaphore = new(2, 3); static void Main(string[] args) { for (int i = 1; i <= 10; i++) { Thread threadObject = new(DoSomething) { Name = "Thread " + i }; threadObject.Start(i); } } static void DoSomething(object id) { Console.WriteLine(Thread.CurrentThread.Name + " wants to enter the critical section for processing"); try { semaphore.WaitOne(); Console.WriteLine(Thread.CurrentThread.Name + " is Doing its work"); Thread.Sleep(1000); Console.WriteLine(Thread.CurrentThread.Name + "exits."); } finally { semaphore.Release(); } } } }
כפי שניתן לראות בדוגמא זו, הגישה למקטע הקריטי ניתנה ליותר מ-Thread אחד בודד בזמן נתון.
מחלקת SemaphoreSlim
במחלקת SemaphoreSlim נוכל להשתמש על מנת לסנכרן את הגישה גם ליותר מתהליך אחד בודד בזמן נתון בדומה ל-Semaphore.
זוהי למעשה אלטרנטיבה "קלה" יותר של Semaphore.
כעת ננסה לשנות את התוכנית שלנו כך שבמקום שנשתמש ב-Semaphore, נשתמש ב-SemaphoreSlim:
using System.Threading; namespace ThreadingDemo { public class Program { static SemaphoreSlim semaphore = new(initialCount: 4); static void Main(string[] args) { for (int i = 1; i <= 5; i++) { int count = i; Thread t = new(() => DoSomething("Thread " + count)); t.Start(); } } static void DoSomething(string name) { Console.WriteLine($"{name} waits to access"); semaphore.Wait(); Console.WriteLine($"{name} was granted access"); Thread.Sleep(2000); Console.WriteLine($"{name} is completed"); semaphore.Release(); } } }
כפי שניתן לראות, בשורה 7 הגדרנו שהמקסימום Threads שיקבלו גישה בו זמנית יהיה 4:
עד עתה ראינו כיצד ניתן לבצע נעילות אקסקלוסיביות ונעילות שאינן אקסקלוסיביות למשאב משותף,
אך תארו לכם מצב שבו למשל 2-Threads מחכים אחד לשני שיסיימו את הפעולה.
מקרה כזה נקרה DeadLock והפרק הבא של המדריך יוקדש לנושא זה.
לקריאה מורחבת על Thread ו-Threading באתר של מייקרוסופט יש ללחוץ כאן.