שימוש נכון ב-await Task.Run ב-C#
-
אשמח אם מישהו יסביר לי קצת יותר מתי ואיך נכון להשתמש ב-await Task.Run.
כמו"כ קראתי כמה פעמים על משהו שנקרא תנאי תחרות (Race Condition) כשמשתמשים ב-Multithreading - אני חייב לומר שלא בדיוק הבנתי את הנושא.
לעת עתה כל מה שאני יודע שזה נותן לי אפשרות הריץ קוד בלי לחסום את ה-UI. או להריץ כמה דברים בו זמנית (שזה בעצם אותו עיקרון).שאלותיי הם:
מהם ההדרכות לכתיבת קוד אסינכרוני ב-C# וממה יש להיזהר?דוגמת קוד:
האם קוד זה טוב או בעייתי או טעון שיפור? אשמח גם לקבל הסבר מדוע:async Task LoadCommentries(IOrderedEnumerable<LinkItem> commentryList, int lineIndex) { Commentry = await Task.Run(async () => { var stringBuilder = new StringBuilder(); foreach (var commentry in commentryList) { stringBuilder.AppendLine(await GetCommentryLine(commentry.path_2, lineIndex)); } return stringBuilder.ToString(); }); } async Task<string> GetCommentryLine(string path, int lineIndex) { using (var reader = new StreamReader(path)) { int currentIndex = 0; while (!reader.EndOfStream && currentIndex < lineIndex) { await reader.ReadLineAsync(); currentIndex++; } return await reader.ReadLineAsync(); } }
-
@pcinfogmach כתב בשימוש נכון ב-await Task.Run ב-C#:
מהם ההדרכות לכתיבת קוד אסינכרוני ב-C# וממה יש להיזהר?
זה נושא מורכב שהרבה מסתבכים איתו ועושים טעויות, אני ממליץ לך לעבור על המסמך AsyncGuidance שהתודעתי אליו בעבר מהפורום.
-
מתי: אף פעם. כשצריך, תדע את זה. למשל אם פעולה גורמת לUI לקפוא - אמור להיות נדיר, כי IO יש כבר Task מובנה, ובCPU, צריך לעשות ממש דברים רציניים מאוד כדי שתהיה הצדקה לקפיאה.
יש שני סיבות למה להשתמש בכלל בTask:
א. לומר לתוכנה שהיא לא צריכה הרגע לחכות לתוצאה, אלא לכשתגיע נדבר.
ב. לנצל את ריבוי המעבדים.כשאתה מבצע קריאות מדיסק, המעבד לא עובד בכלל. אתה לא רוצה לעשות את זה במקביל, אתה סה"כ רוצה שהתוכנה תדע שאפשר בינתיים ללחוץ על כפתור אחר, או לגלול תצוגה וכדומה עד הסיום. זה נקרא פעולות מסוג IO.
בכל פעולות הIO הנפוצות יש כיום מתודה מובנית שמחזירה Task - הרעיון הוא להריץ פקודה, ולקבל כביכול להודעה שזה הסתיים.
בפונקציה הראשונה שלך, מה שלוקח זמן זה המתודה GetCommentryLine, והיא כבר Task. בגלל שיש בstringBuilder.AppendLine את הawait, בעצם מה שקורה זה שאתה אומר לGetCommentryLine: כשאת גומרת אנא הכניסי את השורה שיצא לתוך הstringBuilder, ואז תשלחי לביצוע את הcommentry הבא. זה מעולה בלי שום עטיפה של Task מיותר על הפעולה הזו.
הנה איך שהייתי כותב:async Task<string> LoadCommentris(IOrderedEnumerable<LinkItem> commentryList, int lineIndex) { StringBuilder stringBuilder = new StringBuilder(); foreach (var commentry in commentryList) { stringBuilder.AppendLine(await GetCommentryLine(commentry.path_2, lineIndex)); } return stringBuilder.ToString(); }
את הפונקציה השניה הייתי משתמש בכלים המובנים יותר כיום:
async Task<string> GetCommentryLine(string path, int lineIndex) { var index = 0; await foreach (var line in File.ReadLinesAsync(path)) if(index++ == lineIndex) return line; return null; }
-
@pcinfogmach כתב בשימוש נכון ב-await Task.Run ב-C#:
כמו"כ קראתי כמה פעמים על משהו שנקרא תנאי תחרות (Race Condition) כשמשתמשים ב-Multithreading - אני חייב לומר שלא בדיוק הבנתי את הנושא.
קח את הקוד הבא:
var count = 0; void Increment() { for (var i = 0; i < 1_000_000; i++) { count++; } } var task1 = Task.Run(Increment); var task2 = Task.Run(Increment); await Task.WhenAll([task1, task2]); Console.WriteLine(count);
הקוד הזה מריץ במקביל שני פונקיות שמקדמות את count במיליון, אם תצפה ש count יהיה שווה לשני מליון אחרי ההרצה, אז אתה תתפלא לראות שבכל הרצה מודפס ערך שונה, ואף פעם לא שני מליון.
הסיבה לזה טמונה לדרך הפעולות של המעבד. הקוד
++count
נראה פשוט אבל מורכב מכמה פעולות:
א. קריאת הערך של count
ב. חישוב הערך החדש
ג. כתיבת הערך החדש ל - countבגלל שהפעולות מתבצעות במקביל אז כשהמעבד עובר בין ה - threadים (בתהליך שנקרא context-switch) זה גם יכול לקרות באמצע הפעולות הנ"ל, לדוגמא אחרי פעולות א' ו-ב' המעבד עובר ל - thread השני ואז התוצאה היא שאיבדנו קידום של ספרה אחת.
אם תשנה את שורה 7 לזה:
Interlocked.Increment(ref count);
או לזה:
lock(...) { count++; }
תראה שהתוצאה היא שני מליון כמצופה, וזה קורה בגלל שנעלנו את הקטע הקריטי שבו אנחנו צריכים שיהיה סינכרון בין שני ה threadים, ולא יבוצע עדכון במקביל, במילים אחרות, במקרה הזה זה מחייב שפעולות א' ב' ו-ג' יתבצעו ביחד ולא יתפרקו באמצע.
אני יודע שזה מסובך אבל ניסיתי להסביר כמה שהתאפשר לי.
-
-
@dovid כתב בשימוש נכון ב-await Task.Run ב-C#:
ובCPU, צריך לעשות ממש דברים רציניים מאוד כדי שתהיה הצדקה לקפיאה.
באפליקציה קלאסית נתקלים בזה כשמריצים סינון על הרבה פריטים באמצעות תיבת חיפוש, ולפעמים ה - UI קופא לזמן קצר, במקרה הזה הפתרון להשתמש ב - Task.Run.
יש שני סיבות למה להשתמש בכלל בTask:
א. לומר לתוכנה שהיא לא צריכה הרגע לחכות לתוצאה, אלא לכשתגיע נדבר.
ב. לנצל את ריבוי המעבדים.הייתי מגדיר את זה טיפה שונה: המטרה העיקרית של Task ו - async/await היא לא לחסום את ה - thread הראשי. המושג הכללי multi-threading נועד גם בין היתר לנצל את ריבוי המעבדים.