שיקולים בתכנון מקביליות: Beyond Threads

מקביליות (concurrency) מתורגמת ע”י לא מעט אנשים ל Thread ו synchronized (בג’אווה) – דבר שהוא נכון, אבל מסתיר כמה אלטרנטיבות חשובות.

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

Thread נתפס כ”אמצעי להאצת התוכנה”, אבל זה לא בדיוק נכון. אם יש לי משימה מקבילית שכוללת הרבה I/O (כגון client להורדת קבצים מהאינטרנט) הגדרת thread לכל קובץ או segment שמורד היא הדרך הקלה לפיתוח, אבל לא הדרך היעילה.

דרך יעילה יותר היא הגדרת thread יחיד שעובד עם ערוצים רבים של IO אסינכרוני (כגון channels בספרית java.nio. מקביל לפקודת select ב C של unix/linux, למי שמכיר). בגישה זו יש thread יחיד שמאזין לרשימת ערוצי ה IO הרלוונטים (sockets מאתר ההורדות + streams לכתיבה לקבצים) וכל פעם שערוץ IO זמין לקבל פקודה (התקבל packet ברשת או נסתיימה כתיבה של באפר לדיסק)ה thread שלנו יתעורר ע”י event. עליו לעשות איטרציה ולבדוק איזה ערוצ(י)  מוכנים, לבצע את הפעולה ולחזור לישון עד פעולת ה I/O הבאה שהסתיימה.

אז מה חסכנו בעבודה עם thread יחיד (בסדר עולה):

  • יצירה של thread היא פעולה יקרה (thread pool עוזר להתמודד)
  • תזמון ה threads השונים הוא overhead.
  • לcontext switch בין threads יש מחיר.

כתיבה ב Thread אחד היא בהחלט יותר יעילה! בכל זאת ברוב הפרוייקטים הייתי מעדיף לעבוד עם מספר threads. דוגמת אפליקציית ההורדות היא דוגמא פשוטה במיוחד שבה כל ה threads הם אחידים, אבל לרוב המצב יותר מורכב. כמות הרווח מ thread יחיד תתרום, נאמר, 5% לביצועי המערכת? לא שווה ברוב המקרים לייצר קוד מסובך בשביל שיפור שכזה.
סיכום: thread יחיד היא אופטימיזציה טובה למקרים כמו המתואר לעיל.

היבט חשוב נוסף הוא מערכת multi-core שאותה ניתן לנצל רק עם מספר threads שיתאים למספר ה cores.
אם הייתי מריץ את המערכת הנ”ל על מערכת עם ארבעה cores (וייתכן היה שיש לי מספיק ערוצי IO להעסיק את ה thread עד תום) – הייתי רוצה 4 threads שינצלו את ה CPU עד הסוף. דוגמא קלאסית היא WinZip שעובד עם thread בודד מול תוכנות דומות (winrar, 7zip) שעובדות עם מספר threads:

אפרופו multi-core: הנוסחא המקובלת לבחור כמה threads לייצר ליעילות מרבית היא:

# threads = # of cores / (1 - blocking coefficient)
כאשר המקדם (Blocking Coefficient) הוא ערך מ 0 עד 1 כמה אחוז מהזמן ה thread ישן בין פעולות IO. אם יש לי מערכת עם 4 cores שישנים 30% מהזמן, אני ארצה לעבוד עם 6 threads.
Runtime.getRuntime().availableProcessors();
ייתן לי בג’אווה את מספר הcores הלוגים (יתחשב ב hyperthreading).

אסטרטגיות מקביליות

אחד הדברים שכן כואבים במספר threads הוא עלות הסינכרון. אם לא נסנכרן – יכולים להיות שיבוש מידע, deadlocks ו livelocks. אם אנחנו מסנכרנים, אז המחיר הוא בביצועים: כל lock שמגיעים אליו n threads גורם לכך ש n-1 יאלצו “לישון” למרות שהם רוצים לעבוד. דמיינו בעבודה את קבצי ה excel שמישהו פותח ואף אחד לא יכול לעבוד עליהם (כי הם נעולים), אבל במקום לעבור למשימה אחרת – כל מי שניסה לפתוח את הקובץ חייב ללכת לישון! (ועד עובדי חברת החשמל – רעיון לעיונכם). יצירת המון threads במקום לרוב לא תשפר את המצב: תשלמו הרבה על context switch בלי לפתור את הבעיה המהותית.

יתרה מכך, סינכרון חוסם את מידת ה scalability האפשרי בריבוי cores.
בהינתן מערכת ש 5% מזמן הביצוע שלה הוא קטע מסונכרן, אפילו אם יהיה לי את ה banana bridge i9-9990EX של אינטל שיצא ב 2024 עם 6400 cores, לא אוכל להשיג יותר מפי 5 ביצועים מאשר על מעבד ה i5 ארבעה cores הסטנדרטי שלי (בהנחה שלא היה חיזוק כוחו של כל core ושזו משימה יחידה שאני מריץ). נשמע דיי מאכזב למי שמתכוון לחכות לbanana bridge מעכשיו.

עקרון זה ידוע כ Amdahl’s Law וניתן לקרוא עוד עליו כאן. הפיתרון הוא לצמצם את כמות הסינכרון למינימום.

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

  • mutable synchronization
  • isolated mutability
  • pure immutability
  • actors

mutable synchronization
זו הגישה שרובנו בוחרים בד”כ באופן אוטומטי בלי לשים לב. העקרון הוא לשים synchronized על כל מתודה שיש בה שינוי state שיכול להשתנות בין threads. זו הגישה הקלה והפחות יעילה. דרך שיפור מהירה היא לכתוב את ה synchronized בבלוקים הכי קטנים שאפשר ולא בחתימת המתודה (למי שלא מכיר – תנסו – אפשר לכתוב אותם אפילו על שורה בודדת).

isolated mutability
גישה זו היא צעד אחד הלאה, לצמצם את כמות הסינכרון למינימום – שזה המשתנים עצמם. במקום לעבוד עם Long אני אעבוד עם AtomicLong של java.util.concurrent שמספק לי פעולות אטומיות כגון getAndIncrement או incrementAndGet
שימוש בהן יאפשר לי לצמצם את הסינכרון לנתונים עצמם ולא מעבר.

pure immutability
זו גישה הפוכה לגמרי שאומרת – אל תשנה משתנים. כל ערך שתיצור יהיה immutable (כמו String), כל פעם שתצטרך ערך אחר – צור אובייקט חדש. גישה זו דיי קשה ולא כ”כ נתמכת בג’אווה. אם פיספסתי – הקומפיילר לא יתריע וגם יש כמה מצבים בעייתים שאצטרך להתחכם בהם. Closure דוגמא לשפה שתומכת בגישה זו באופן מובנה.

actors
זו גישה שפופולארית היום ב scala (השפה שנוצרה ל multi-core ו scalability). הגישה מדברת על active objects או actors שהם אובייקטים “חיים” – כלומר ה threads. לכל actor יש תור נכנס (“דואר נכנס”) והם מתקשרים אחד עם השני רק בהודעות אסינכרוניות (שמונעות מצב של deadlock). בזכות התקשורת – אין צורך בסנכרון בכלל. גישה זו ניראית מוצלחת וישימה, לא ניסיתי בעצמי – אך שמעתי כמה סיפורי הצלחה ממקור ראשון. נראה שהיא גם עובדת יפה כשהמערכת גדלה והופכת למסובכת.
אמנם בג’אווה אין תמיכה בשפה ל actors אך יש מספר ספריות (כגון akka או GPars) שלמרות שנכתבו בשפות שונות על ה JVM – עובדות יפה מתוך Java. הגישה מתאימה, אגב, גם לסנכרון בתוך אותו JVM וגם ב remoting בין JVMs שונים (כגון nodes שונים ב cluster).

ארבעה דברים טובים שנוספו עם Java 7

כבר נשמעה הרבה ביקורת, על כמה מעט נוסף במשך חמש וטיפה שנות הפיתוח (או יותר נכון: דיונים) – של ג\’אווה 7.
אם גלעד שליט ידע ג\’אווה לפני שנחטף – אז הוא חזר לסביבה מוכרת וחסרת הפתעות.

סיכמתי כמה דברים חיוביים לאלו שיעברו ל Java 7, במקרה או בכוונה, ששווה להכיר:
פחות צורך במשמעת עצמית ב Exception Handling
Java נבנתה בצורה שתקל על המפתח להמנע משגיאות, הפחזבל (Garbage Collector) חסך מיליוני שנות פרך של debug וחיפוש אחר זליגות זכרון. בכל זאת, חברה שלא אנקוב בשמה, שילמה 17 מיליון דולר השתתפות בנזקים של הלקוח בעקבות הבאג הבא (חפשו אותו):

  Connection conn = null;
  Statement stmt = null;
  try {
    conn = connectionPool.getConnection();
    stmt = conn.createStatement();
    // Do the lookup logic
    // return a list of results
    } finally {
      if (stmt != null) {
        stmt.close();
      }
      if (conn != null) {
        conn.close();
    }
  }

אני מניח שהבאג לא צועק, הקוד נראה במבט ראשון מסודר ונכון. אבל מה – אם ()stmt.close יזרוק exception (בגלל מנגנון ה keep alive  של Oracle?), ה connection לא יתנקה וכשפעולה זו תחזור שוב ושוב, Oracle יסרב לקבל עוד קריאות. במערכת Mission Critical המשמעות היא לבצע restart לבסיס הנתונים כל מספר דקות, חוסר יכולת לעבוד, יום בטלה לעובדים והפסד אדיר ללקוח.

הפתרון הוא לעטוף ב try-catch נוספים (ה catch יישאר כנראה ריק) את קריאות ה ()close של stmt ו conn. זה היה ה best practice שדרשנו ממפתחים לעשות, שוב ושוב, וברגשות מעורבים.

איך מוודאים שבקשה זו תיושם בפועל? ביצוע תחוף של Code Reviews, הגדרת המודעות בקרב המפתחים ובארגון שעובד נכון – בניית Connection Pool שיודע לזרוק Connections בשעת לחץ.
בג\’אווה 7 ניתן לכתוב את הקוד הבא:

  try (Connection conn = connectionPool.getConnection();
       Statement stmt = conn.createStatement()) {
    // Do the lookup logic
    // return a list of results
    } catch {
      // respond to exception

    }

  }


שדואג ש conn ו stmt ייסגרו, אם לא שווים ל null ולמרות exceptions שיכולים להזרק. הוא מקביל לקוד הארוך והמכוער שתארתי למעלה. נחמד!

פחות חזרה חזרה חזרה ב catch statement
תופסים שלושה סוגי Exception ורוצים לעשות אותו הדבר?
מעכשיו כתבו:

} catch (IOException | SQLException | SQLStatementException ex) {
  log.warn(ex);
  throw ex;
}
המשתנה ex יתייחס לסוג ה Exception (מתוך השלושה) שנזרק. יופי.

Performance Profiling: שחרור VistualVM
אם תחפשו בתוך ספריית Bin ב JDK שהורדתם, תגלו שיושב לו שם Profiler בשם VisualVM. מלבד כך שהוא חינמי, מתחבר לכל ה IDEs החשובים (עם plugin) ויש לו ממשק משתמש נחמד, הוא מספק נקודת איזון מאוד מוצלחת של פשוטות מול עוצמה. הוא ידידותי – אבל גם אינו צעצוע. הוא יכול להספיק לרוב משימות ה profiling הנפוצות, גם בתוך tomcat, ואינו דורש הגדרות מיוחדות לJVM או לשרת שאתם עובדים איתו.

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

נ.ב. – אופס, תפסתם אותי. VisualVM מסופק החל ממרץ כבר ב JDK 1.6 ואינו חידוש של Java 7. בכלל הוא יכול לעבוד (תחת מגבלות) עם אפילקציות שקומפלו ב JDK1.4. הוספתי אותו לכאן מכיוון שהוא תוספת מכובדת ל JDK שקל לפספס בגלל שיצא ב update ולא ב major release.

גרסה חדשה – מספר חדש
סימן חיים, ג\’אווה עדיין קיימת! זה לא דבר גדול אבל, (סרקזם קל) זה בהחלט אחד השינויים הבולטים בגרסה שקיבלנו.

בהצלחה!

האם Java גוססת?

כבר בסביבות שנת 2008 התחיל לצבור תאוצה הקרב על הירושה: איזו שפה תחליף את שפת Java? אם היה קרב לרשת את סטלין בעודו בחיים, מחנות החינוך מחדש בסיביר היו מקבלים מספר חברים חדשים לשורותיהם, אבל שפת Java עם הקהילה הפתוחה שלה, היא רחוקה מאוד מדיקטטורה*. אחת התלונות הקשות על Java היא הקצב האיטי בו גוף התקינה של Java מקבל החלטות. JDK 1.7 יצא לאחר חמש שנים ולא כלל שיפורים יוצאי דופן. C#, שפה מאוד דומה, מציגה שיפורים משמעותיים (לדוגמה LINQ, closures) באופן תדיר.

האם צריך להחליף את Java? מה רע בה?
if it’s not broken – don’t touch it!” יאמרו רבים.
if it’s ain’t broke – break it” יענו המהפכנים. “חובה עלינו לחפש שיפורים כל הזמן”, בתרגום חופשי. “ואם נוח לנו במצב הקיים, אדרבא”
התלונות העיקריות לגבי Java הן הבאות:

שפה  verbose (מרבה במילים) ומסורבלת.
יש אינספור דוגמאות של: 10 שורות בג’אווה, 4 שורות ב X (שפה אחרת) . דברים מרגיזים כוללים:

  • Checked exceptions
  • Java Beans (הסינטקסט המסורבל של getXXX/setXXX שהופך משפטים לקשים לקריאה)
  •  הצורך לכתובanonymous class  בכדי להעביר פונקציה פשוטה כפרמטר (שפות אחרות פשוט נותנת להקליד את הקוד הרצוי במקום הנכון)

גוף התקינה הוא שמרן ואיטי.

“מדוע היה צריך לחכות לג’אווה 7 בכדי שמשפט switch יוכל לקבל String?” היא תלונה נפוצה וקלה להבנה (אך קצת קטנונית). הבעיה האמיתית היא ריבוי ה cores שהפך למציאות תיכנותית, וחוסר היכולת של Java להתאים את עצמה למציאות החדשה. Fork-Join שהגיע באיחור ולא יושב יפה בתוך השפה ו Actors (בדקו בתחתית הפוסט בבלוג זה להסבר על Actors) שמתכנתי Java אוספים משדות זרים.

האם שפה שהיא verbose (מרבה במילים) משפיעה באופן משמעותי על פריון המתכנתים? השאוות הראו שלספריות העומדות לרשות המפתח, ה IDE, ניסיון והכישרון של המפתח יש השפעה גדולה מהשפה עצמה. רק אסמבלי (ואולי קצת קובול) נשארה משמעותית מאחור (מקור: ספר Code Complete) אני מכיר היטב את הטענה שכתיבה ברובי או פייטון היא הרבה יותר מהירה אך אין לזה חיזוק אמפירי. הסיכוי לטעויות בשפות אלה בגלל שאין בדיקת קומפיילר מפצה לרעה על ה syntax הקליל. אני מניח שזו עניין של הרגשה ואולי מתכנתים מעולים שלא צריכים קומפיילר (שמעתי TDD?) באמת כותבים מהר יותר בשפות סקריפט. 
בואו נתבונן בשני האלמנטים האובייקטיבים (לא תלוי במתכנת) שכן משפרים את הפריון, וכיצד השפות החדשות יכולות להתמודד מול Java:
1.כלים – auto-complete, קומפיילר טוב, Refactoring וכו’. כל שפה חדשה שמוצגת לשוק מהר מאוד מייצרת כלים בסיסיים (לפחות syntax highlighting ו auto-complete), קשה לשכנע שהשפה שלך תקצר זמני פיתוח אם אתה אמור לעבוד ב Notepad. כל מערכות ה IDE המרכזית (Eclipse, VS, IntelliJ ו NetBeans) בנויות להרחבה ותמיכה בשפות נוספות.
2.  ספריות – האם עלייך לכתוב ספריית Scheduling בעצמך, או שאתה מוריד את Quartzומתחיל לעבוד? ב C++ לא היו הרבה ספריות משותפות, אולי בגלל שהיו קומפיילרים שונים למערכות הפעלה שונות. ברגע שJava הציגה את “Write Once, Run Everywhere“** – כמות הספריות הזמינות להורדה (ובחינם) נערמה והפכה לכוחה הגדול של Java – ה Community. אז איך מציגים שפה עם 0 ספריות מול Java עם אלפי ספרות בדוקות ובוגרות? רובי הצליחה להתרומם בעזרת Rails – ספרייה חדשנית ומוצלחת במיוחד לפיתוח אפליקציות Web (וגם בעזרת שפה מוצלחת בפני עצמה, כמובן) – אבל זה לא הספיק. ואז – נתגלתה השיטה: ב JDK 1.6, ה JVM – Java Virtual Machine הורחב לתמוך ב JavaScript שיתקמפל ל Byte Code – וירוץ בצד השרת. לצורך העניין נעשו שינויים שאפשרו ל JVM לתמוך בתכונות של שפה דינמית (כלומר פונקציה היא evaluated רק ברגע שצריך להריץ אותה, שמאפשר לתוכנה לבצע שינויים עד אותו הרגע). הסכר נפרץ ושפות רבות קפצו על ה JVM: בוגר, יעיל, מוצלח,highly optimized, עם התקנה לעשרות מערכות הפעלה ואולי הכי חשוב – היכולת  להשתמש באופן טבעי בכל מאגר ספריות הJava הקיימות. אפילו שפות שהיה להן Virtual Machine – פיתחו וריאציה עבור ה JVM.
 
היכן המהפכה?
אוקיי, אז עברו שלוש שנים, וJavaעדיין כאן. השפות שעשו זינוק בשנה האחרונה הן דווקא LUA ו F# – שאינן קשורות ל JVM. השפות המדוברות תופסות הרבה נפח בכנסים ובבלוגים, אך עדיין הנוכחות שלהן סה”כ קטנה.
ברוכים הבאים לעולם העסקי (ניתן לומר: האמיתי). מקום שבו טכנולוגיה טובה לא מבטיחה הצלחה, מקום שבו שיתופי פעולה והרגלים הם בעלי כוח מכריע.
הדעיכה של Java כנראה התחילה, אבל Java לא עתידה להעלם סתם כך. הייתי מעריך שיעברו עוד שנים רבות עד שJava תחשב כשפה לא רלוונטית. כמות הקוד הקיים, הספרות והכי חשוב – האנשים שמושקעים באופו אישי בJava הוא עצום והוא לא יעלם סתם כך. (בעולם הJVM) תמיכת ה IDE בJava היא עדיין הטובה ללא עוררין, וכך גם כמות החומר הלימודי. יתרונות השפות המדוברות – הן עדיין הדרגתיות וצריך סיבה טובה כדי לעשות מעבר.למשל: אם יש לי בעיות Concurrency קשות באמת, אני יכול לכתוב מודול ב Scala, לייצר .jar ולצרף אותו לפרוייקט הJava הקיים.

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

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

Scala  – כנראה המתמודדת הבולטת ביותר. משלבת אלמנטים פונקציונליים ו Object Oriented. מאפשרת גם תכנות דינמי (כמוJavaScript  או רובי) אבל גם Static Typing – כלומר הקומפיילר יתפוס לכם את רוב הבעיות. הייחוד שלה היא ההצלחה לשלב פרדיגמות שונות בצורה מוצלחת והתכנות מהיסוד לשפה לתכנות מקבילי. היא תהיה יחסית נוחה למתכנת Java, C++ או C# אבל תציע הרבה כוח ואפשרויות שלא היו זמינות בשפות אלה.
Groovy– שפה מגניבה, שכוללת אלמנטים של python Syntax ותאימות רבה לJava. קוד Java ללא שינוי צפוי להתקמפל בגרובי ב 95% מהמקרים. נעשתה עבודה לא כ”כ מסודרת בפרטים – וכבר יצאה גרובי++ שמטפלת בכמה מהבעיות. הביאה לעולם את Grails שזה העתק של Rails של רובי, ואת Griffon – שזו גרסת ה Desktop לאותה ארכיטקטורה. נראתה מועמדת מובילה לפני שנתיים-שלוש (בעיקר בעקבות backward compatibility), אך לאחר שהמפתח שלה הצהיר “אם הייתי מכיר את סקאלה – לא הייתי טורח לפתח את גרובי” גורלה כנראה נחרץ.
JRuby– רובי היא רביזיה Pure-Object-Oriented של שפת Python מתחילת שנות ה-90 שהוזכרה קודם. יש לה את ספריית Rails שזכתה להצלחה רבה. JRuby היא פשוט מאוד גרסת ה של רובי JVM, שילוב שגרם לרובי לרוץ מהר הרבה יותר. יש לה מומנטום חזק כ”כ בעולם ה web – כך שנראה שהיא כאן בכדי להשאר. השאלה בעיקר היא האם תתפשט מחוץ ל Web UI לכיוונים אחרים.
Closure– מין LISP מודרני, דינאמי ופונקצינאלי. גמישה מאוד ביכולת הביטוי. משפט אחד בלתי-קריא יכול להיות שקול ל 10 שורות בכל שפה אחרת. האם זה ייתרון? אם אתם באקדמיה, אוהבים קדרים וקודרים (המבין יבין) – לבריאות. אני לא נהנה לקרוא אותה ואיני מבין על מה המהומה. 
Jython – פייטון (ע”ש להקת מונטי-פייטון, כדרך אגב) היא הותיקה בשפות, אפילו ותיקה מ Java. מאז הצלחת Java ירדה מהכותרות אך חזרה אליהן לאחרונה, אולי גם בגלל Jython ה porting ל JVM. היא פחות סקסית וחדישה, אך יש בעולם הרבה מאוד קוד פייטון ומתכנתי פייטון (יותר מרובי, למשל) בעוד שרובי תפסה את הנישה הוובית, פייטון נשארה general purpose. באופן אישי, אני מכיר בעיקר סקריפטים וכלים שכתובים בה ומעט מוצרים ולכן יש לי רושם שנתוני השימוש היבשים קצת מזייפים. כמו לשאול כמה מתכנתי Bash יש בעולם…המצב הקיים:

< הלינק מת… היה כאן תרשים של אחוזי השימוש בשפות JVM שונות + לאורך זמן >

 
לסיכום: קרב הירושה בעיצומו ועוד עשוי להמשך זמן רב. לא חייב להיות בהכרח מנצח אחד. הJVM  יאפשר למספר שפות לחיות זו לצד זו, כאשר כל אחת תוכל להתמקד בנישה שלה. 
Java עשתה את שלה, עכשיו ה JVM הוא העיקר.
* מאז שאורקל, חברה דיי כוחנית, לקחה שליטה על Java היא הצליחה בעיקר להבריח מפתחים מקהילת המפתחים של Java ולייצר תביעות שונות, בחשד להפרת פטנטים של Java. היא ניסתה לדלל את כוחה של הקהילה הפתוחה, אך עם הצלחה מוגבלת.
** Java לא הייתה המערכת הראשונה שרצה על מערכות שונות: פייטון הגיע לשם כמה שנים מוקדם ממנה עם Virtual Machine משלה. מה שהכריע את הכף הוא כנראה ש Java קסמה למפתחי ה C++ (שהיו אז הרוב) בכך שאימצה תחביר דומה וגם תמיכה גורפת מגופי ענק כמו IBM, אורקל, SUN ועוד.