C# Threading: מקביליות ובו זמניות
-
לאחרונה אני מרפרף על JS ואני מאוד מסתבך מתכונת הדגל של השפה: אסינכרוניות.
התכונה כ"כ מובנית בשפה, שמסובך בה לפעול להפך.בC# הכל בברירת מחדל סינכרוני, וכשרוצים אסינכרוני זה נחשב "נושא מתקדם".
אז אני רוצה לסכם הפעם מהקל אל הכבד את הנושא של מקביליות ובו זמניות בC#.מתי יש צורך בזה? למה שהמחשב לא יעשה קודם א' ואח"כ ב'
הסיבות הנפוצות הם:- UI חי תוך כדי פעולה. בWPF או WINFORMS.
- ביצוע בו זמני מהר יותר בפעולות: בפעולות עיבוד כשיש יותר ממעבד אחד במחשב, IO (קלט פלט) כשבזמן ההמתנה אפשר לבקש ואף לעבד את הבקשה הבאה בתור.
בדרך כלל הפעלת קטעי קוד שונים בו זמנית נעשים ע"י הטכנלוגיית "ריבוי-תהליכים" - Multi-Threading.
Thread - תהליך
לכל יישום שרץ במחשב יש לפחות תהליך אחד, תהליך ראשי. אבל יכול להיות ליישום עוד תהליכים שחולקים משאבים של זיכרון ועוד, אבל מבודדים מבחינת הריצה שלהם - כל אחד רץ כשלעצמו, כלומר עצירה או תקיעה של אחד לא מפריעה לשני.איך יוצרים Thread (נוסף) בC#?
למה נוסף? כי האפליקציה עצמה היא כבר Thread אחד. אז בשביל ליצור אחד יש ליצור אובייקט Thread בקוד ו"להריץ" אותו.
הבנאי של Thread מקבל כמה גרסאות פרמטרים, הפשוטה שבהם זה delegate מסוג void ללא פרמטרים.
הנה דוגמה. ייבאתי את מרחב השמות System.Threading. אני יוצר טריד חדש, וכפרמטר אני נותן לו את שם המתודה Do שהיא void וללא פרמטרים.private static void Main(string[] args) { Thread t = new Thread(Do); Console.WriteLine(t.ThreadState); //Unstated t.Start(); Console.WriteLine(t.ThreadState); //Running Console.ReadLine(); } static void Do() { Console.WriteLine("do do do!"); }
פורסם במקור בפורום CODE613 ב13/05/2015 18:19 (+03:00)
-
עצם יצירת הטריד לא עושה מאומה, עד שקוראים למתודה Strat. החל ממתי שקוראים למתודה Start אפשר להשהות או לעצור את הטריד, עד שהוא נגמר.
המתודה Start היא חד פעמית. אחרי שטריד הורץ א"א להריץ אותו פעם נוספת.
לThread יש מאפיין כן/לא בשם IsAlive שמחזיר חיובי במידה והטריד עדיין פועל, לא נגמר וכבר התחיל.
יש גם מאפיין מפורט יותר בשם ThreadState שמחזיר את מצבו. המצבים הקלאסיים הם Unstarted ללפני ההרצה ע"י Start, בעת ההרצה זה Running ואם זה מושהה Suspend, נעצר (ע"י מתודה שנראה בהמשך) Aborted.Join
אם רוצים לחכות בטריד אחד לסיומו של טריד אחר, אפשר להשתמש בפונקציה Join. היא בעצם תוקעת את הטריד הנוכחי עד שהטריד הזר גמר.
למתודה join יש גירסה שמקבלת פרק זמן קצוב, שאם לא נגמר הטריד בפרק הזה, התוכנית כן ממשיכה לרוץ. הפונקציה Join מחזירה ערך בוליאני שמציין האם ה"איחוד" עם הטריד הצליח, כלומר נגמר בפרק הזמן הקצוב.Sleep
המתודה הסטטית Thread.Sleep() היא משהה את הטריד הנוכחי. היא מקבלת פרמטר מספרי שהוא מס' המילישניות בהם הטריד "ישן" (אפשר להעביר למתודה פרמטר TimeSpan במקום מס' מילישניות). זה פועל על הטריד הנוכחי, לא על טריד אחר.פורסם במקור בפורום CODE613 ב13/05/2015 20:25 (+03:00)
-
[size=110:14bu1mwd]דרכים להעביר ולקבל מידע מהThread[/size:14bu1mwd]
כמו שראינו הטריד מקבל פרמטר שהוא מסוג דלגייט פשוט שככה נראה:public delegate void ThreadStart();
זה מתודה שלא מקבלת פרמטרים (יש גירסה שמקבלת פרמטר מסוג ParameterizedThreadStart שכשמו הוא מקבל פרמטר אחד מסוג אוסייקט. אבל נשאיר את זה כי זה שיירים עלובים מתקופת האבן של דוט נט, כעת זה מיותר לגמרי).
מה אם אנחנו רוצים להעביר לטריד מידע? הדרך לאינראקציה עם Thread זה פשוט גישה לאותו משאב (משתנה/אובייקט) של התהליך הקורא או כלל התהליכים. זה מה שנקרא Shared Data.
Shared Data - שיתוף זכרון בין תהליכים
איך עוישם זאת? איך נגשי ם מטריד אחד למשאב של טריד אחר?
בעיקרון זה מידי קל. לכן, כלל ראשון במעלה: תהליך שניגש למידע משותף המשמש יותר מטריד אחד, הוא מצב שצריך למנוע אותו ככל האפשר, ובעצם זה שער לכל הבעיות של ריבוי-תהליכים.
פשוט מאוד. אם המתודה שמועברת לטריד פונה למשאב חיצוני (לא לוקלי מבחינת סקופ) למשל למאפיין/שדה החבר באותה המחלקה, או יותר מזה, למאפיין/שדה סטטי של מחלקה כל שהיא, אז בעצם הטריד פונה למידע הנגיש (לעיתים לקריאה בלבד לעיתים גם לכתיבה) לטריד אחר באותה מידה (והכי גרוע אולי באותה העת).
המצב האידאלי הוא שהמשאב הזה המשותף מוקדש כל כולו לצרכי הטריד הזר, ולא משתמשים בו בטריד הנוכחי כל עוד הטריד הזר בפעולה.
המצב שמומלץ להתרחק ממנו ככל האפשר, הוא משאב אחד המשמש בו זמנית שתי תהליכים.
טוב קיללתי בין השיטין הרבה את הדרך הזאת אז זה הזמן לומר שלפעמים זה לגיטימי לגמרי. נדבר בהמשך על ההשלכות של SharedData.דוגמה לאינטראקציה בין התהליך הראשי לתהליך חדש:
private static void Main(string[] args) { Thread t = new Thread(Do); Console.WriteLine("enter min value to check: "); minValue = int.Parse(Console.ReadLine()); Console.WriteLine("enter max value to check: "); maxValue = int.Parse(Console.ReadLine()); t.Start(); Console.WriteLine("started!"); Console.WriteLine("press any key for recive the status\result... for abort press X"); while (t.IsAlive) { if (Console.ReadKey().Key == ConsoleKey.X && t.IsAlive) { t.Abort(); return; } Console.WriteLine("the thread is busy (the current results count is " + results.Count + "). retry later!"); } Console.WriteLine(" finsh! \n result count is: " + results.Count + "\n results list:"); results.ForEach(Console.WriteLine); Console.ReadLine(); } private static int minValue; private static int maxValue; private static List<int> results; private static void Do() { results=new List<int>(); for (int i = minValue; i < maxValue; i++) { bool exsistDivisor = false; for (int j = 2; j < (i / 2); j++) { if (i % j == 0) { exsistDivisor = true; break; } } if (!exsistDivisor) results.Add(i); } }
מה יש כאן? מתודה שבודקת על טווח מספרים אם הם ראשוניים. היא בעצם ניזונה משתי פרמטרים minValue וmaxValue שהם לא לוקליים בתוך הפונקציה, אלא חברים במחלקה (סטטיים במקרה שלנו). והפלט? נוסף לתוך אובייקט ליסט שגם הוא חבר במחלקה. בעצם ישנה גישה בו זמנית לתהליך הראשי ולחדש שרץ ברקע לכל המידע הזה.
זו דוגמה גרועה מאוד כי ממש מגושם ומעצבן לייחד מאפיין/משתנה ברמת המחלקה על כל פיסת מידע שרוצים להעביר לתהליך.
הדרך המומלצת כיום (פעם (NET 2) זה היה ParameterizedThreadStart אבל אפשר לשכוח ממנו) זה להשתמש בביטוי למדה.פורסם במקור בפורום CODE613 ב14/05/2015 19:03 (+03:00)
-
Lambda כדרך להעברת פרמטרים לטריד
אם משתמשים בביטוי למדה המצב הוא אלגנטי בהרבה:private static void Main(string[] args) { Console.WriteLine("enter min value to check: "); var min = int.Parse(Console.ReadLine()); Console.WriteLine("enter max value to check: "); var max = int.Parse(Console.ReadLine()); List<int> results = new List<int>(); Thread t = new Thread(() => Do(min, max, results)); ... ... } private static void Do(int minValue, int maxValue, List<int> results) { for (int i = minValue; i < maxValue; i++) { bool exsistDivisor = false; for (int j = 2; j < (i / 2); j++) { if (i % j == 0) { exsistDivisor = true; break; } } if (!exsistDivisor) results.Add(i); } }
מה זה ביטוי למ(ב)דה? זה לא כ"כ המקום להסביר אבל בשתי מילים: דרך מקוצרת ליצור מתודה אנונימית לשימוש מקומי, רק נגדיר את הצורך והכללים:
הצורך - אני במהלך פונקציה מסויימת צריך לבדוק חמש פעמים את האורך של טקסט לחלק ל12. אז במקום לכתוב כמה פעמים את התהליך אז אני רוצה פונקציה (מה שנקרא Reuse -לא לכתוב קוד פעמיים). אין שום טעם ליצור פונקציה חיצונית כשהמקום היחידי בה היא באה לשימוש זה בתוך הפונקציה שלי.
האופן: מבנה פונקציה רגילה בC# היא ככה- מציין גישה (פרטי ציבורי וכו')
- ערך מוחזר
- שם
- פרמטרים נדרשים - סוג ושם
- גוף הפונקציה שכללי הגוף הם: א. מוקף בסוגריים מסולסלים חובה, ב. חובה לכלול return כשמחזיר ערך ג. כל פקודה אפי' יחידה מסתיימת בנקודה פסיק.
ובכן למדה תמיד מכניסים לתוך משתנה/פרמטר שהוא כבר הגדיר את חתימת הפוקנציה - מה היא מחזירה ומקבלת (2, 4). וגם השם מיותר (3) (כי הרי בפרמטר אין לנו צורך להעניק שם ובמשתנה הלא אנו "מחזיקים ביד" את המשתנה). אבל יש צורך לתת שם לפרמטרים. כי אנחנו צריכים לדעת איך להתייחס אליהם מגוף הפונקציה. מציין גישה (1) ודאי שלא צריך. אז נשאר: שמות פרמטרים (במידה ויש יותר מאחד מופסקים בפסיק וחובת עטיפתם בסוגריים) ובלוק הפונקציה. אבל שתי דברים :
א. בין שמות הפרמטרים שלגוף התחביר הוא לשים =< היינו שווה ואח"כ "גדול מ" . (זה בא לומר הקלט משמאל (הפרמטרים מועברים לגוף הפונקציה מימין)
ב. בגוף הפונקציה עצמו ישנה "קולות" למקרה בו הפונקציה מכילה שורה אחת בלבד, 1 .לא צריך לעטוף בסוגריים מסולסלות 2. לא צריך נקודה פסיק בסוף הפקודה 3. לא צריך לפרש return להחזרת הערך אלא פשוט כותבים אותו וזהו.
פורסם במקור בפורום CODE613 ב14/05/2015 19:58 (+03:00)
-
Foreground/Background Thread
טריד יכול להיות טריד רקע - Background והמשמעות היחידה של זה היא שכשהתהליך הראשי באפליקציה גומר הוא מפסיק אותו באמצע. לעומת זאת בטריד Foreground (וזה הברירת מחדל של טריד) האפליקציה ממתינה לסיומו.
הגדרת תהליך כBackground נעשית ע"י הצבת true למאפיין IsBackground.Thread Priority
ניתן לתת לThread רמת עדיפות ביחס לטרידים האחרים באפליקציה שלנו (ל ביחס לתהליכים של אפליקציות אחרות במחשב. זה ניתן לשינוי אבל זה לא קשור לנושא שלנו). ע"י הצבה של ערך במאפיין Priority הערכים האפשריים הם: Lowest, BelowNormal, Normal, AboveNormal, Highest.Thread Safety
הבעיה מס' 1 בריבוי טרידים זה מה שנקרא Thread Safety. כלומר בטיחות המידע מפני גישה של תהליכים בו זמנית.
המצב שתהליך פונה למשאב משותף ומשנה אותו בה בעת שתהליך אחר משתמש בו עשוייה להביא לתוצאות בלתי צפויות. דוגמה פשוטה: כשאנחנו עושים מונה בלולאה או בכל מקום אחר בקוד אנחנו כותבים משתנה++. המשמעות המלאה של זה היא: 1. קח את ערך המשתנה, 2. הוסף לו אחד, 3. ואכלס את ערך התוצאה במשתנה הזה (תוך דריסת הערך הקודם). עכשיו בריבוי תהליכים יכול להיות מצב שתהליך א' עושה את הפרצדורה 1 ו2, ובה בעת תהליך אחר גם כן. כעת כששניהם אוחזים בשלב 3, במקום שהמשתנה "יגדל" בשניים, הוא יגדל רק באחד. ייתכן שתהליך אחד הגדיל את המשתנה כבר בשתי פעימות, ותהליך אחר יחזיר אותו אחרוה ע"י השמת החישוב של משתנה+1 המעודכן ללפני כמה מילישניות.הנה המחשה:
static int result = 0; //משאב משותף לשתי התהליכים private static void Main(string[] args) { //יצירת שתי טרידים. var t1 = new Thread(Do); var t2 = new Thread(Do); //הרצת התהליכים. t1.Start(); t2.Start(); //הרצת המתודה גם בתהליך הראשי/נוכחי Do(); //וידוא שהתהליכים יגמרו לפני הדפסת התוצאה t1.Join(); t2.Join(); //הדפסת התוצאה Console.WriteLine(result); Console.ReadLine(); } static void Do() { for (int i = 0; i < 10; i++) { Thread.Sleep(1); //האטת התהליך הנוכחי כדי שתהיה חפיפה בזמני הריצה. result++; } }
הקוד הזה מריץ את הפונקציה Do משתי תהליכים ובנוסף מהתהליך הראשי. הפונקציה פונה שוב ושוב, 10 פעמים, למשתנה result בוחנת את ערכו ומשימה בו את הערך אות בחנה + 1. לפי החשבון המשתנה result צריך להכיל לבסוף את הערך 30, שהרי זה מס' הפעמים בהם הועלה ערכו ב1. אבל בגלל ההתנגשויות בין התהליכים הערך יהיה כל פעם משהו אחר נמוך מ30 בדרך כלל.
כמובן שהקוד הזה חסר תכלית אז קשה להציע בו שינוי מועיל.
בהודעה הבאה אציע מקרה ממחיש יותר. בו תהיה הבעיה ופתרון הLocking - בלוק קוד שרק תהליך אחד יכול להיכנס אליו בו זמנית. זה פותר את הבעיה של משתנה++. כי מתחילת התהליך עד סופו, יש רק תהליך אחד שעושה את הפעולה ברגע נתון.רגע, מה אם JS? וNode.js? איך הם מתגוננים מכזה דבר? התשובה היא שהם עובדים על טריד אחד תמיד. האסינכרוניות שלהם עובדת על אירועים ותזמון. ממילא לעולם לא רצים במקביל שתי פקודות שעלולות לשבש או לסתור אחת את השניה. תמיד יש אחת ראשונה ואחת שניה. אכן, זה חולשה של JS בעיבוד, אבל זה לא כ"כ התפקיד שלו
פורסם במקור בפורום CODE613 ב18/05/2015 12:53 (+03:00)