מדריך תכנות אסינכרוני (async) בסי שארפ


תכנות אסינכרוני

הבסיס

עובדה מעניינת עלי, היא שלמעבד שלי יש ארבעה ליבות ושמונה מעבדים לוגיים. כל זה חמוד ממש אבל מה זה אומר ולמה זה רלוונטי? זאת כי על מנת להבין איך תכנות אסינכרוני עובד התחנה הראשונה שלנו תהיה המעבד.

מסעדת הבקתה

נניח והייתי רוצה לפתוח מסעדה, האם הייתי יכול לתפעל אותה באפקטיביות עם עובד אחד בלבד? בואו נבחן סיטואציה - העובד שלנו מבשל בדיוק מנה של רביולי ארטישוק ירושלמי ללקוחות נוכחים, ופתאום באים עוד לקוחות. העובד מפסיק לבשל את המנה, והולך לתת ללקוחות החדשים שולחן ולקחת את ההזמנה שלהם. עד שהוא חוזר לבישול, מנת הרביולי כבר נשרפה בתנור (אם שמים אותה בתנור... באמת שאין לי מושג). מהר מאוד נבין שלא משנה כמה מוכשר העובד, הוא לא יכול לתפעל את המסעדה לבד, כי הוא לא יכול לבצע כמה פעולות בו זמנית. דבר שהפעלת המסעדה דורשת.

כמו העובד במסעדה, גם המעבד שלנו נמצא בסיטואציה שבה הוא צריך לבצע מספר משימות בו זמנית. לדוגמא, אלחץ על כפתור להורדת שיר, אך תוך כדי שהשיר בתהליך הורדה, ארצה גם להמשיך להסתכל על סרטונים ביוטיוב. על מנת שהמעבד יוכל לבצע זאת, הוא מפוצל לכמה ליבות, וכל ליבה בדרך כלל לכמה מעבדים לוגיים. כל מעבד לוגי מסוגל לבצע משימה אחת בו זמנית.

תהליכונים

ידני

בסי שארפ קיימת היכולת לפצל את הרצת התוכנה שלנו, כך שכמה תהליכים יוכלו לרוץ במקביל. זאת ע"י פיצולם לthreads או בעברית - תהליכונים, שכל אחד מהם ירוץ על מעבד לוגי שונה. בפועל, נעשה זאת ע"י כתיבת קוד הירוץ על מעבד לוגי אחד, ויקרא לשאר המעבדים לבצע משימות היוגדרו על ידו. נקח מבט מהיר כיצד לבצע זאת בצורה "ידנית",

using System.Threading;
using System.Net;
____
void PrintPageCharSum(string websiteURL)
{
____WebClient client = new WebClient();
____Console.WriteLine(client.DownloadString(websiteURL).Length);
}
____
Thread t1 = new Thread(() => PrintPageCharSum("https://www.google.com"));
Thread t2 = new Thread(() => PrintPageCharSum("https://www.facebook.com"));
t1.Start(); t2.Start();

קוד זה מכיל,

  1. הגדרתי את הפעולה PrintPageCharSum, המקבלת כתובת אתר אינטרנט, ומדפיסה את כמות התווים בדף הhtml של האתר. בכדי לעשות זאת ייבאתי את ספריית הSystem.Net.
  2. הגדרתי שני תהליכונים המקבלים הפניה לפונקצייה (אם אינכם מכירים את נושא ההפניות לחצו כאן). הפונקציה מפעילה את פעולת הPrintPageCharSum עבור גוגל בthread הראשון ופייסבוק בשני. בכדי להשתמש בתהליכונים ייבאתי את ספריית הSystem.Threading.
  3. הפעלתי את התהליכונים בעזרת Start(), רק כאשר נקרא לפעולה זו התהליכונים יחלו לרוץ.

וזהו בעצם קוד בסיסי היוצר שני טרדים היריצו תהליכים כבדים יחסית במקביל. ההרצה במקביל תקח 825 מילי-שניות, במקום 1250. הפלט יהיה,

51410
49808

אוטומט

למרות היכולת שלנו להשתמש בטרדים בצורה ידנית, ברוב המקרים אין צורך להשתמש בהם בצורתם הגולמית. זאת, מכיוון שקיימים קונספטים המנצלים את יכולת הThreading ומנגישים אותה בצורה נוחה הרבה יותר. לכן, למרות שישנם יתרונות לשימוש ידני בתהליכונים אם משתמשים בהם נכון, קודם כל נתרכז בלמידת שני הקונספטים - Task,Parallel. ונחזור לנושא זה מאוחר יותר.

משימות

הבסיס

קונספט המשימות, או Tasks, מנגיש את קונספט הThreading בצורה של משימות. לדוגמא, אם נחזור רגע למסעדת הבקתה, בישול רביולי הארטישוק יוגדר כמשימה, הושבת הלקוחות בשולחן תהיה משימה, וכך גם לקיחת ההזמנה מהם.
אך מנוע המשימות יעבוד שונה מפשוט שיוך משימה לthread בצורה של שגר ושכח. הוא ישים לב אם לתהליכון מסויים משוייכת משימה, אך הוא כרגע לא מבצע פעולה אקטיבית. לדוגמא, אם ניתן לעובד מסויים משימה להכין רביולי, אך כעת הוא רק שם אותו בתנור ומחכה. במידה ויינתן מצב כזה הוא ייתן לו משימה אחרת הדורשת פעולה אקטיבית באופן אוטומטי, וברגע שהמשימה הראשונית שוב תדרוש פעולה אקטיבית, מנוע המשימות ישייך לה עובד פנוי. כך יכול להיווצר מצב שכמה טרדים שונים עבדו על אותה המשימה. בניגוד לשימוש בתהליכונים, בהם לכל טרד מוגדרת משימה אחת שהוא אמון על ביצועה מקצה לקצה.
לכן, אדגיש שתיאורטית, למרות ששימוש במשימות תומך בשימוש בטרדים שונים, ובו זמנית. הצורה שבה מנוע המשימות בנוי מאפשרת שמשימה תרוץ על אותו הטרד שהקוד הראשי רץ עליו במידה והוא לא דורש פעולה אקטיבית כרגע. כלומר שימוש במשימות יכול רק לתת את האשליה שמשימה פועלת על מעבד לוגי שונה בעוד זה כלל לא המצב.

הפרקטיקה

כעת נסתכל על דוגמא לשימוש בקונספט המשימות,

using System.Threading.Tasks;
using System.Net;
____
void PrintPageCharSum(string websiteURL)
{
____WebClient client = new WebClient();
____Console.WriteLine(client.DownloadString(websiteURL).Length);
}
int GetPageCharSum(string websiteURL)
{
____WebClient client = new WebClient();
____return client.DownloadString(websiteURL).Length;
}
____
Task tk1 = new Task(() => {
____WebClient client = new WebClient();
____Console.WriteLine(client.DownloadString("https://www.facebook.com").Length);
});
tk1.Start();
Task tk2 = Task.Run(() => PrintPageCharSum("https://www.google.com"));
Task<int> tk3 = Task<int>.Run(() => GetPageCharSum("https://www.youtube.com"));
Console.WriteLine(await tk3);

עכשיו נקח נשימה ארוכה, ונעבור על הקוד בו מוגדרים,

  1. הפעולה PrintPageCharSum, שרשמנו כבר בחלק הקודם.
  2. הפעולה GetPageCharSum הזהה לפעולה מסעיף אחד, רק שבמקום להדפיס את מספר התווים, היא מחזירה אותו.
    • הגדרנו עצם מסוג Task בשם tk1 המייצג משימה שנרצה לבצע. על מנת להשתמש במשימות כאלו נייבא את הספרייה System.Threading.Tasks. ניצור את האובייקט בעזרת הפעולה הבונה של Task, המקבלת דלגייט מהסוג Action, כלומר פעולה הלא מחזירה ומקבלת פרמטרים.
    • נזין אליה פונקצייה אנונימית המבצעת קוד הזהה לזה בפעולה PrintPageCharSum, פרט לזה שבמקום לשלוח את הכתובת המתבקשת כפרמטר, נרשום אותה ישירות בקוד. כעת מוגדר לנו הקוד באובייקט המשימה.
  3. אמנם הגדרנו את tk1, אך ההפעולה שרשמנו עדיין לא רצה, רק יצרנו את האובייקט. על מנת להריץ אותו נשתמש כעת בStart().
    • נגדיר את המשימה tk2 שתקבל את ערכה מהפעולה Task.Run(), המהווה קיצור דרך שכן היא מתפקדת כפעולה בונה למשימה - אך גם מריצה אותה ישרות. מה שחוסך לנו את השימוש בStart().
    • נרצה שהטאסק יקרא לפונקצייה PrintPageCharSum, וישלח כפרמטר את כתובת גוגל. על מנת לעשות זאת, נדרש לשלוח הפניה אל הפונצייה, אך, בגלל שהיא מקבלת פרמטר היא לא תהיה מהסוג Action אלא Action<T>. ולא ניתן לשלוח סוג כזה אל הפעולה הבונה (אלא אם כן מדובר בAction<Object> מה שברוב במקרים המוחלט לא המצב).
    • לכן, על מנת לקרוא לה, נבנה פונקצייה אנונימית התפעיל את הפונקצייה ותשלח לה את הפרמטרים.
    • נגדיר את המשימה tk3 מהסוג Task<int>. סוג זה בנוי כמשימה התחזיר ערך כלשהו בסיומה, כך שסוג האובייקט המוחזר יוזן ל<>. גם אותה נריץ עם Task.Run(), ולפעולה הבונה נזין דלגייט מהסוג Func<T>.
    • מכיוון שגם GetPageCharSum הינה פעולה הדורשת פרמטרים, בדומה למצב בסעיף הקודם, פשוט נקרא לה בעזרת פונקצייה אנונימית, שבה נזין את הכתובת של יוטיוב.
  4. נדפיס את הערך שחזר מtk3. על מנת לעשות זאת נזין לפעולת ההדפסה את הפקודה - await tk3. עליה נסביר בחלק הבא,

עברנו על הרבה דברים בשבע הנקודות האלו, אז מה שצריך לזכור בתמצות הוא,

  • עצם הTask מייצג משימה כלשהי.
  • על מנת ליצור משתנה משימה, מהסוג Task,Task<TResult>, נעביר לפעולה הבונה דלגייט של הקוד שנרצה שיורץ מהסוג Action,Func בהתאמה.
  • יצירת העצם לא בהכרח מריצה את המשימה אוטומטית. נשתמש בTask.Run() על מנת ליצור עצם משימה וגם להריץ אותה ישר.
  • אם נדרש להעביר פרטמטרים לפונקצייה נעשה זאת באמצעות הגדרת פונקצייה אנונימית.

מחכים

אמנם קיימת האפשרות שלפעמים ניצור ונריץ משימות ולא יהיה אכפת לנו מה קורה איתן, אבל ברוב המוחלט של המקרים, נרצה לדעת מתי המשימות שהגדרנו בוצעו, ומה לעשות לאחר שבוצעו. על מנת להבין איך לעשות זאת, קודם כל נבחן קצת את עצם המשימה.

בנוסף להכלת דלגייט לפונקצייה שתתבצע כמשימה, עצם הטאסק מכיל עוד מאפיינים מעטפתיים להקלת העבודה. לדוגמא,

  • Status - מכיל את סטאטוס המשימה כEnum , מתוך האופציות הללו,
    1. Created - משימה שנוצרה כאובייקט, אך עדיין לא נשלחה להרצה.
    2. WaitingForActivation - המשימה נשלחה להרצה, ובהתליך ביניים פנימי של C# המכניס אותה לתור המשימות שצריכות לרוץ.
    3. WaitingToRun - המשימה נקבעה להרצה, מחכה בתור למעבד לוגי פנוי שיריץ אותה.
    4. Running - המשימה בתהליך הרצה.
    5. WaitingForChildrenToComplete - המשימה רצה, וכעת מחכה שיסתיימו משימות שהיא עצמה יצרה.
    6. RanToCompletion - המשימה רצה בהצלחה.
    7. Canceled - המשימה התבטלה, בהמשך נלמד כיצד לבטל משימה שנשלחה.
    8. Faulted - צפה שגיאה במשימה במהלך ההרצה.
    מתוך מצבים אלו גם ניתן ללמוד על "מעגל החיים" של משימה.
  • קיצורים לסטטוס - מוגדרות כמה פעולות בוליאניות המנגישות את הStatus בצורה נוחה.
    • IsCompletedSuccessfully - האם המשימה בוצעה בהצלחה.
    • IsFaulted - האם הייתה שגיאה במהלך הרצת המשימה.
    • IsCanceled - האם המשימה בוטלה.
  • Result - מכיל את התוצאה של המשימה לאחר שבוצעה, עבור משימות המחזירות תוצאה.
  • Exception - מכיל את השגיאה שקרתה, במידה וקרתה.

ובעצם, במאפיינים הלו נוכל להשתמש על מנת לבצע בקרה על המשימות שלנו. אחת הדרכים לעשות זאת היא באמצעות המילה await, הבעצם משהה את ביצוע הפעולה עד אשר המשימה שנגדיר תבוצע. כמו שביצענו למעלה, כשרשמנו -

Console.WriteLine(await tk3);

בשורה זו מטרתנו בעצם הייתה להדפיס את הערך המוחזר מהמשימה, וכיוצא מזה גם לחכות ראשית שהמשימה תסתיים. בכדי לעשות זאת רשמנו בתוך פקודת ההדפסה את פקודת הawait, המקשיבה לסטטוס עצם המשימה tk3. ברגע שהמשימה הושלמה, במידה והמשימה מהצורה Task<TResult>, מוחזר הערך ממנה אוטומטית. ובמידה והיא רק Task לא מוחזר כלום, כמו קריאה לפעולת Void. כלומר, ברגע שtk3 יסיים את הרצתו, הערך שהוא יחזיר אוטומטית ישלח לפונקציית ההדפסה ויודפס. וכך קילבנו את התוצאה שרצינו.

במידה והשתמשנו בפקודת הawait אך המשימה שהזנו כבר בוצעה, זוהי אינה שגיאה, ופשוט תוחזר תוצאת המשימה ישירות, או שפשוט לא יקרה כלום במידה ואינה מחזירה תוצאה. כעת נבחן את ההבדל בין await, לפעולה הדומה - Wait().

tk2.Wait();
await tk2;

אמנם שתי הפעולות עוצרות את ביצוע הקוד עד שמשימה מסויימת ששלחנו תתבצע. אך יש בניהם הבדל מהותי. בעוד שכאשר נשתמש בawait על מנת להשהות את הקוד המעבד הלוגי יתפנה לבצע משימות אחרות, כשנשתמש בWait() המעבד הלוגי יחסם ולא יותר לשימוש אחר עד שהמשימה הסתיימה. לכן, מומלץ להמנע משימוש בו.
אז... למה הוא קיים בכלל? זאת מכיוון שאנחנו כלל יכולים להשתמש בawait וביתרונות שהיא מציעה בפעולה רגילה.

פעולות אסינכרוניות

פעולה לא רגילה

על מנת להשתמש במילת הקוד await, נדרש להגדיר מראש שהקוד שנריץ הוא אסינכרוני. זאת עקב השוני עבור סביבת סי שארפ שבהרצת פעולה שיכולה לעבור בין כמה מעבדים לוגיים, אל בין פעולה רגילה שתעבוד בצורה סנכרונית על מעבד לוגי אחד. נגדיר אותה על ידי שימוש במילת הקוד async, לדוגמא,

async Task ServeTable(Table CurTable)
{
____Console.WriteLine("Welcome To Hubikta Resturant!");
____Order CurOrder = await Task.Run(() => GetOrder(CurTable));
____Food OrderedFood = await Cook.CookFood(CurOrder);
____await CurTable.Eat(OrderedFood);
____Cashier.GivePayment(await Task.Run(() => GetPayment(CurOrder)));
}

כעת, נבחן את הקוד, המדמה טיפול של מלצר בשולחן במסעדת הבקתה, והשימוש בו בasync,await.

    • נגדיר את הפעולה ServeTable המקבלת שולחן בו יטפל מלצר. לפעולה נוסיף את המילה async על מנת לסמן שאינה סנכרונית.
    • כל פעולה אסינכרונית דרושה להחזיר Task, Task<TResult>, או void במידה וזהו עצם המסמל אירוע, אך נשים מקרה זה בצד בינתיים. נדרש להחזיר עצם משימה מכיוון שהרצת פונקצייה אסינכרונית בעצם מתפקדת כהרצת משימה. ולכן, כשנריץ אותה בעצם נחזיר אובייקט Task היטפל בכל המעטפת של הרצת המשימה. במידה והפעולה מחזירה ערך, נשתמש בTask<TResult>.
  1. נאמר ללקוחות ברוכית הבאים, ונקח את ההזמנה שלהם על ידי משימה המקבלת את פעולת לקיחת ההזמנה מהשולחן.
    • נריץ פעולה אסינכרונית של השף לבישול הזמנת הלקוחות, Cook.CookFood(), התחזיר לנו את עצם האוכל של הלקוחות.
    • קריאה לפעולה אסינכרונית תהיה זהה להפעלת משימה דרך Task.Run() שכן הפעלת פעולה אסינכרונית מריצה אותה, ומחזירה ישר בזמן הקריאה שלה עצם מהסוג משימה, המכיל את נתוני הפעלת הפעולה האסינכרונית. לכן נוכל לחכות לקריאה עם await ואפילו לאחסן אותה במשתנה כך,
      Task tk = hi();
      Console.WriteLine(tk.Status);
    • אדגיש שההבדל בין הרצת פעולה אסינכרונית להפעלת משימה בצורה שעשינו עד כה היא שהגדרנו כפונקציית המשימה פעולה סינכרונית. האמנם לא תחסום את ריצת התוכנה ותוכל לרוץ על מעבד לוגי שאינו מריץ את הקוד הראשי, אך היא תרוץ על מעבד לוגי יחיד מקצה לקצה. בעוד כנפעיל פעולות אסינכרוניות, הם יוכלו לעבור בין מעבדים לוגיים תוך כדי ביצוע המשימה.
    • וכך, משימות המריצות פעולות סינכרוניות, הינן בעצם הלבנים הכי קטנות מהם נבנה פעולות אסינכרוניות. שכן נוכל לאגד משימות אלו תחת משימה אסינכרונית שתחכה להם עם await. ורק בשלב זה, יתרון השימוש במנוע המשימות על פני תהליכונים גולמיים אותו הסברתי בהתחלה באמת יהיה רלוונטי.
  2. נקח את עצם האוכל שהלקוחות הזמינו, ונזין אותו לפעולה CurTable.Eat() שהינה פעולה אסינכרונית של עצם השולחן הלא תחזיר תוצאה, לאכילת האוכל. לאחר שהלקוחות אכלו את האוכל, נתן לקופאית את תשלום הלקוחות בפעולה Cashier.GivePayment(), לה נזין עצם מסוג PaymentMethod היוחזר מהמשימה לה הגדרנו הפעלה של הפונקצייהGetPayment(). וכך נסיים את הטיפול בשולחן.

וכך נבנה פעולה אסינכרונית בסיסית, שתדע לתפעל משימות שונות. בלי שימוש במילת הקוד await לניהול המשימות, אין טעם לקבוע פונקצייה כאסינכרונית והיא תרוץ כמו שפעולה רגילה תרוץ.

עוד יכולות

כעת נסקור עוד כמה דברים שניתן לבצע עם קלאס המשימה שיקלו עלינו בשימוש בהם.

ביטול משימה

נוכל לשלוח הוראה לביטול משימה כלשהי תוך כדי הביצוע שלה. דבר היתאפשר כך,

CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
tokenSource.CancelAfter(10000);
____
try {await PrintPagesCharSum(new string[] { "https://www.facebook.com", "https://www.google.com", "https://www.youtube.com" }, token);}
catch (OperationCanceledException) {Console.WriteLine("Too Much Time Passed, Cancelling Task");}
finally {tokenSource.Dispose();}
____
async Task PrintPagesCharSum(string[] websiteURLs, CancellationToken token)
{
____foreach (string websiteURL in websiteURLs)
____{
________await Task.Run(() => PrintPageCharSum(websiteURL));
________token.ThrowIfCancellationRequested();
____}
}

על מנת לבטל משימה, ביצענו,

    • הגדרנו אוביקט מהסוג CancellationTokenSource שישמש ליצירת טוקן ביטול.
    • ניצור טוקן ביטול דרך המקור שיצרנו, אותו אנו עתידים להעביר למשימה - התקשיב בדרך שנראה בהמשך לטוקן, במידה ונעביר פקודת ביטול לטוקן, המשימה תתבטל. נוכל להעביר את הטוקן למספר משימות, ולבטל אותן במקביל.
  1. נשתמש בפקודה CancelAfter() על אובייקט מקור טוקן הביטול, התשלח פקודת ביטול לטוקן שיצרנו לאחר זמן הנגדיר מראש במילי שניות. במקרה זה הגדרתי שתשלח לאחר עשר אלף מילי שניות - עשר שניות. נוכל לשלוח פקודת ביטול ישירות עם הפעולה Cancel(), בה נשתמש לדוגמא אם לוחצים על כפתור ביטול.
    • נשתמש בבלוק try-catch על מנת להפעיל המשימה, שכן אם נבטל את המשימה דרך הטוקן תוצף שגיאה (אם אינכם מכירים, מדריך בנוגע לניהול שגיאות זמין כאן).
    • נקרא ונחכה למשימה, המדפיסה את מספר התווים בקוד המקור של כל כתובת אתר מרשימה - הפונקציה האסינכרונית PrintPagesCharSum, לה נעביר את רשימת האתרים שנרצה להוריד, ואת טוקן הביטול.
    • במידה ועלתה שגיאה שהמשימה בוטלה, נדפיס זאת. לאחר מכן נפטר ממקור טוקן הביטול עם Dispose().
  2. נגדיר את הפעולה PrintPagesCharSum, המקבלת מערך של כתובות, וטוקן ביטול. עבור הדפסת כל כתובת נשתמש בפעולה PrintPageCharSum לה נקרא דרך משימה. את המשימה נפעיל בלולאה על כל כתובת. בנוסף, בכל פעם שנעבור על כתובת נבדוק אם לא נשלחה פקודת ביטול לטוקן הנשלח בעזרת הפעולה ThrowIfCancellationRequested() הגם תשלח שגיא, ביטול משימה אוטומטית במידה וזהו המצב. וכך נבטל משימות.

מתי שכולם

דבר מאוד מעצבן בחלק הקודם בפעולה שרשמנו, PrintPagesCharSum, הוא שבלולאה כשאנו נריץ בכל פעם את משימת הדפסת מספר התווים, נחכה שהיא תסתיים, ורק אז נריץ את ההבאה. וזהו בזבוז מוחלט של היכולת שלנו להריץ משימות במקביל. על מנת לתקן זאת נשנה אותה ל,

async Task PrintPagesCharSum(string[] websiteURLs, CancellationToken token)
{
____List<Task> Tasks = new List<Task>();
____foreach (string websiteURL in websiteURLs)
____{
________Tasks.Add(Task.Run(() => PrintPageCharSum(websiteURL)));
________token.ThrowIfCancellationRequested();
____}
____await Task.WhenAll(Tasks);
____Console.WriteLine("Done!");
}

בדרך זו, בעצם יצרנו רשימה של משימות, ובלולאה הרצנו את פקודת הדפסת מספר התויים עבור כל כתובת, והכנסנו אותה לאותה הרשימה בלי לחכות לה. לאחר מכן, השתמשנו בפעולה Task.WhenAll() והזנו לה את רשימת המשימות. מה שפקודה זו בעצם עושה, הוא שהיא מחזירה עצם משימה, היבוצע מתי שכל המשימות ברשימה בוצעו. וכך נוכל לבצע בקרה מתי בוצעו המשימות, והאם הם רצו כהלכה, בלי להריץ אותם אחת אחת. נוכל לבצע פעולה דומה במידה ונרצה לדעת אם לפחות משימה אחת מהרשימה הושלמה, בעזרת Task.WhenAny().

אירוע אסינכרוני

כשנרצה ליצור פעולה אסינכרונית המתאימה לחתימה של EventHandler, לא נוכל להחזיר משימה שכן לפי חתימתו נדרש להחזיר void. לכן, במקרה של אירוע נדרש ליצור פונקצייה אסינכרונית המחזירה void. לדוגמא,

async void button_Click(object sender, EventArgs e)
{
____Console.WriteLine("Sending Urls to print");
____await Task.Run(async () =>
____{
________await PrintPagesCharSum(new string[] { "https://www.facebook.com" }, default);
________Console.WriteLine("Printed all urls");
____}).ConfigureAwait(true);
____button.Text = "Already Pressed";
}

וכך נוכל לקרוא לפעולות אסינכרוניות ולחכות להם בתוך אירועים. פרט לאירועים, בהם אין ברירה, מומלץ מאוד לא להשתמש בפעולות אסינכרוניות המחזירות void שכן לא נוכל לחכות להם או להשתמש בשאר היתרונות שמעטפת קלאס הTask נותנת. אלא נפעיל אותן בצורה של שגר ושכח.

נסקור עוד שני קונספטים, הראשון הוא פעולה אנונימית אסינכרונית. על מנת לבנות כזאת, פשוט נוסיף את המילה async לפני הסוגריים, כמו שעשינו בקוד מעלה. השני הוא השימוש בפעולה ConfigureAwait() לפעולה זו נעביר משתנה בוליאני, היגרום ל,

  • במידה ונעביר true, הקוד שנריץ לאחר שהתקבלה התוצאה מהמשימה לה חיכינו, כלומר במקרה זה, button.Text = "Already Pressed"; ירוץ על אותו המעבד הלוגי שבו הרצנו את הפעולה לפני. הגדרה זו הינה ברירת המחדל, כלומר אם לא נשתמש בConfigureAwait() היא תבוצע בכל מקרה.
  • במידה ונעביר false, הקוד הבא יהיה חופשי לרוץ על כל מעבד לוגי פנוי. הגדרה שמאוד משפרת ביצועים היכן שניתן להשתמש בה. אז, היכן ניתן להשתמש בה? בכל מקום חוץ מקלאס המשמש לUI. וזאת מכיוון שאם נמשיך לעבוד על מעבד לוגי אחר, ולמשל במקרה זה נרצה לגשת לכפתור כלשהו ולשנות אותו, תוצף שגיאה. וזאת כי ננסה לגשת לכפתור הנוצר על מעבד לוגי מסויים, בעוד אנו כעת נמצאים על אחר. דבר הלא ניתן.

משימות המשך

נוכל להגדיר שמשימה כלשהי תרוץ אחרי אחרת, ותשתמש בנתוניה בעזרת ContinueWith() כך,

Task<int> FetchCharSum = Task<int>.Run(() => GetPageCharSum("https://www.facebook.com"));
await FetchCharSum.ContinueWith(antecedent => Console.WriteLine(antecedent.Result));

כך, נגדיר ראשית משימה כלשהי, הלא נחכה לה, לדוגמא כאן משימה השולפת את מספר התווים בקוד מקור האתר של פייסבוק. לאחר מכן, נגדיר לאותה המשימה משימת המשך התדפיס את תוצאת המשימה. משימת ההמשך תהיה חייבת לקבל משימה כפרמטר, אליו תוזן המשימה הקודמת. לפרמטר זה נהוג לקרוא antecedent, האומר "קודמת" באנגלית. כעת נשלח לפעולת ההדפסה את תוצאת המשימה הקודמת, ונחכה למשימת ההמשך.

לולאות מקבילות

הבסיס

עוד דרך לנצל את יכולת השימוש בטרדים, היא לולאות מקבילות. אלו, הינן בעצם כמו לולאות רגילות פרט להבדל המאוד מהותי, שכל הרצה תופעל בו זמנית ולא בסדר כרונולוגי. לדוגמא,

using System.Threading.Tasks;
____
Parallel.For(0, 10, i =>
{
____Console.WriteLine(i);
});
Console.WriteLine("Done");

זאת לולאת For התרוץ במקביל, לה נעביר,

  1. המספר ממנו הלולאה תתחיל.
  2. המספר שעבור המספר ההתחלתי ועד אליו הלולאה תרוץ.
  3. פעולה התרוץ בכל חזרה של הלולאה המקבלת int, אליו יוזן על ידי הלולאה "מספר החזרה" הנוכחי. רק אדגיש שהשימוש במילה חזרה לא ממש מדוייק שכן כולן ירוצו במקביל.

בלולאה שרשמתי, בעצם נעבור על המספרים מאפס עד עשר, נדפיס אותם ולבסוף נדפיס "Done". הפלט יהיה,

1
0
3
2
4
9
5
6
7
8
Done

ובעצם נראה שסדר ההדפסה אינו מאחד עד עשר, אלא רנדומלי. זאת, מכיוון שכל חזרה רצה במקביל ולא מחכה שהקודמת תבוצע. לכן הסדר בה תתבצע כל חזרה אינו ידוע מראש. בנוסף, נוכל לגזור מכך ש"Done" הודפסה אחרונה, שלולאה מקבילה ראשית תחכה שכל החזרות שהיא הפעילה יבוצעו, ורק אז הקוד ימשיך הלאה. מה שבאת יקרה. על מנת להשתמש בלולאות מקבילות נדרש לייבא את ספריית המשימות, שכן הן בנויות עליה.

בין משימות ללולאות מקבילות

מה שבפועל לולאה מקבילה תעשה הוא יצירת משימות והפעלתם בו זמנית, כך שכל משימה תתבצע על מעבד לוגי אחר מקצה לקצה. זאת במידה וישנם מספיק מעבדים לוגיים במעבד לכל המשימות, במידה ולא הנותרים יחכו לאחד פנוי. נוכל לגשת לאובייקט המשימה מתוך הפעולה הנשלחת ללולאה על ידי Task.CurrentTask.
ההבדל בשימוש בין לולאות מקבילות לבין תפעול משימות באופן שלמדנו עד כה הוא שהמטרה של שימוש במשימות הוא לאפשר לתוכנה לפעול בצורה שוטפת, בלי להשתמש בהרבה משאבים. בעוד המטרה של לולאות מקבילות הוא להריץ קוד במהירות המירבית, ולהשתמש בכמה שיותר משאבים לשם כך. לכן חשוב להבין מה מטרתנו לפני שימוש בכל אחד מהקונספטים האלו, ולבחור ביניהם בהתאם.
אדגיש כי האפאקטיביות של לולאות מקבילות תשתנה מאוד כמספר המעבדים הלוגיים הקיימים במחשב המריץ אותן.

הגדרות

נוכל להזין הגדרות להרצה במקביל, כך,

string[] people = new string[] {"Joe Devola", "Tomer Ashtamker","Hank Schrader"};
ParallelOptions CurParallelOptions = new ParallelOptions();
CurParallelOptions.MaxDegreeOfParallelism = 2;
____
Parallel.ForEach(people, CurParallelOptions, person =>
{
____if (person.Contains("Joe"))
________Console.WriteLine(person);
});

כאן בעצם ניצור לולאה מקבילה המאתרת אנשים בשם "Joe" ממערך. ללולאה זו נרצה לקבוע מראש בכמה מעבדים לוגיים היא תוכל להשתמש בו זמנית. לשם כך, ניצור עצם מסוג ParallelOptions ואת המאפיין MaxDegreeOfParallelism נקבע כשניים. וכך נוכל להגביל את המשאבים שלולאה זו תשתמש בהם. לאחר מכן ניצור לולאת ForEach התקבל את מערך השמות, אובייקט ההגדרות, ופעולת הלולאה, התבצע את מה שתכננו.
עד כאן, תכנות אסינכרוני (:




תגובה 1:

  1. רציתי סיכום קצר של כל החומר ובהחלט קיבלתי, כל הכבוד!

    השבמחק