בפרק הקודם של המדריך למדנו כיצד להשתמש במאפיין IsAlive ובמתודה Join. בפרק הנוכחי נלמד כיצד ביכולתנו להגיע לסינכרוניות בתכנות מקבילי.
Thread Synchronization
כאשר אנו עוסקים בתכנות מקבילי, כפי שראינו בפרק הקודם – אנו עלולים להגיע למצב של חוסר תיאום בין ה-Threads השונים.
נניח וה-Thread הראשי מריץ 2 Threads שונים – thread1 & thread2.
נניח ושניהם ניגשים למשאב משותף כלשהו שוב ושוב,
כאשר thread1 קורא מתוך אותו המשאב, ו-thread2 כותב בו.
ללא טיפול סינכרוני ב-Threads הללו, עלול להיווצר מצב של חוסר תיאום ביניהם –
מה שעלול לגרום לשגיאות וחוסר דיוק בתוכנית.
פתרון אפשרי להגיע לסינכרוניות הוא לסמן את המשאב כמקטע קריטי,
כלומר שרק Thread בודד יוכל לנהל אותו בזמן נתון,
או במילים אחרות – לחסום גישה לThreads אחרים בזמן ש-Thread מסוים מנהל אותו.
סינכרוניות באמצעות Exclusive Lock ו- Non-Exclusive Lock
כאשר אנו מגדירים נעילת מקטע קריטי, יש לרשותנו 2 סוגי נעילות:
- Exclusive Lock – בנעילה זו נשתמש על מנת לוודא שהמקטע הקריטי ינוהל על ידי Thread או תהליך בודד בלבד.
- Non-Exclusive Lock – מגביל את הגישה למקטע הקריטי לקריאה בלבד.
נוכל לעשות זאת על ידי שימוש במחלקות – Semaphore, SemaphoreSlim ו-ReaderWriterLockSlim
אז איך ניתן להגיע לסינכרוניות באמצעות #C?
יש מספר דרכים שבאמצעותן נוכל להגיע לסינכרוניות ב-#C, אחת מהן היא באמצעות נעילת המקטע הקריטי (Lock),
כאשר נוכל להשתמש בתחביר הבא:
כלומר שכאשר נגדיר Lock על אובייקט מסוים ב-Thread מסוים,
רק אותו ה-Thread יהיה בעל גישת ניהול לאותו האובייקט בזמן נתון,
וכל שאר ה-Threads ייאלצו להמתין שהאובייקט יהיה פנוי.
שימו לב ל-Code Snippet הבא שבו נדגים מה קורה ללא טיפול סינכרוני:
using System.Threading; namespace ThreadingDemo { class Program { static void Main(string[] args) { Thread thread1 = new(MyMethod) { Name = "thread1" }; Thread thread2 = new(MyMethod) { Name = "thread2" }; Thread thread3 = new(MyMethod) { Name = "thread3" }; thread1.Start(); thread2.Start(); thread3.Start(); } public static void MyMethod() { Console.Write("[Hello, "); Thread.Sleep(1000); Console.WriteLine("World!]"); } } }
[Hello, [Hello, [Hello, World!]
World!]
World!]
כלומר שללא טיפול סינכרוני נקבל פלט מבולבל במקרה הזה.
כך שנאלץ לנעול את המתודה על מנת שנוכל לפתור בעיה זו:
using System.Threading; namespace ThreadingDemo { class Program { static object lockObject = new(); static void Main(string[] args) { Thread thread1 = new(MyMethod) { Name = "thread1" }; Thread thread2 = new(MyMethod) { Name = "thread2" }; Thread thread3 = new(MyMethod) { Name = "thread3" }; thread1.Start(); thread2.Start(); thread3.Start(); } public static void MyMethod() { lock(lockObject) { Console.Write("[Hello, "); Thread.Sleep(1000); Console.WriteLine("World!]"); } } } }
[Hello, World!]
[Hello, World!]
[Hello, World!]
מה קרה כאן הרגע?
אז כפי שניתן לראות, בדוגמא הראשונה כל ה-Threads של ה-Thread הראשי רצו במקביל,
אולם בדוגמא השנייה – כל אחד מהם ניגש למתודה בתורו, משום שהגדרנו את המתודה כמקטע קריטי.
בפרק הבא של סדרת מאמרים זו נלמד על Non-Exclusive Locks.
לקריאה מורחבת על Thread ו-Threading באתר של מייקרוסופט יש ללחוץ כאן.