עד לפני מספר שנים כל מה שהיה דרוש על מנת להאיץ את התוכנה החביבה עלינו היה פשוט לקנות מעבד מהיר יותר, החיים של מפתחי התוכנה היו קלים יחסית, יצרני המעבדים עמדו בתחזית Moores law וכל שנה הם הצליחו למקם מספר עולה וגדל של טרנזיסטורים במעבד יחיד.
מגמה זו המשיכה עד לנקודה שבה היצרנים החליטו לשנות אסטרטגיה ובמקום לייצר שבב אחד מהיר בכל מעבד הם עברו לייצר מספר ליבות איטיות יותר המסוגלות לבצע מטלות יחדיו, במצב חדש זה לא די לקנות מעבד מהיר יותר כדי להאיץ את אותה תוכנה, יש לבצע שינוי בקוד כך שפעולות מסויימות ימוקבלו וירוצו במספר Thread-ים בו זמנית.
אוקיי נשמע פשוט למדי לא? אז זהו שלא כל כך, הפרדיגמה הנפוצה כיום בשפות כמו Java ו C# לכתיבת קוד מקבילי הינה shared state with locks, כלומר ישנו איזור משותף בזיכרון אליו ניגשים מספר Thread-ים, האמצעי לסנכרון גישה זו הינו באמצעות מנעולים שעל כל Thread לרכוש טרם הוא ניגש למקטע זה, גישה זו יוצרת מספר בעיות:
- היא מבוססת על מוסכמה, כלום בשפה לא מכריח אותנו לרכוש מנעול וקל שלא לשים לב שאנו ניגשים לאיזור משותף.
- אי דטרמיניזם, המפתח לא מסוגל לדעת מראש כיצד תבוצע הגישה ובאיזה סדר, לא רק שקשה לחזות התנהגות עתידית קשה מאוד גם לשחזר מצבי קצה (למשל מחשבים עם מספר ליבות שונה יצרו תזמונים ובאגים שונים).
- dead locks, מצבים בהם Thread -ים נועלים זה את זה באמצעות מנעולים אותם רכשו.
ניתן למעשה לצמצם את הבעיה למוקד אחד והוא shred state, למעשה בכל פעם שאנו מבצעים השמה למשתנה אנו שומרים מצב כלשהוא בזיכרון, במידה והשפה מאפשרת השמה מרובה (כמו רוב השפות ה imperative – יות) אזי אנחנו יכולים לדרוס ערך ישן בחדשת יכולת זו בשילוב גישה למידע זה ממספר גורמים (thread – ים) בו זמנית גורמת לבעיות שציינו קודם.
אז מה הן האלטרנטיבות בעצם?
אז בואו נחזור אחורה בזמן ל 1936, שנה בה Alonzo Church פיתח מערכת חישובית בשם Lambda calculus השקולה בכוחה ל Turing machine, מערכת זו היוותה את הבסיס למשפחה עשירה של שפות תוכנה בפרדיגמה פונציונאלית (functional programming languages).
איך זה מתקשר לנושא?
ובכן אם נחשוב לרגע על פונקציה f(x) = y פונקציה זו תחזיר את אותה תוצאה ולא משנה כמה פעמים נפעיל אותה, היא אינה תלויית זמן או מקום, תוצאתה מובטחת לנו תמיד, כך שלא משנה כמה Thread – ים יפעילו אותה תמיד נקבל תוצאה תקינה, אותן שפות פונקציונאליות (כמו Lisp למשל) מאפשרות יצירת תוכניות שלמות המורכבות כולן מפונקציות, תוכניות כאלו הינן "טהורות" במובן הזה שהן נטולות side effects (אין שום פרמטר חיצוני המשפיע על ביצוען), מובטחת לנו אותה תוצאה תמיד עבור אותם ערכים.
יתרון זה הוא גם חיסרון שכן תוכנה ללא side effects לא תעשה דבר מלבד לחמם את המעבד (IO הוא תוצר לוואי), לכן כל השפות הללו אינן "טהורות" הן מאפשרות side effects, באמצעות סגמנטציה של הקוד אנו יכולים לבודד קטעים "טהורים" מקטעים שאינם ולהבטיח נכונות ללא תלות במספר ה Thread-ים.
אלטרנטיבה נוספת מגיעה מעולם ה Data bases, אם נזכר לרגע אנו פועלים מול RDBMs-ים לא פעם באלפי גישות מקבילות לאותן פיסות מידע, הדרך שבה מנהלים מסדי נתונים את עקביות המידע בהם היא באמצעות transactions, כל תהליך המבקש לגשת למידע עושה זאת במסגרת transaction, במידה והתגלה קונפליקט אזי אחד (או יותר) מהתהליכים המעורבים "זוכה" לכך שכל השינויים שהוא ביצע מגולגלים לאחור (הוא יכול להתחיל את עבודתו מבראשית במועד מאוחר יותר).
גישה זו אפשרית גם בניהול גישה למקטעי זיכרון משותפים בתוך process, היא זוכה לשם Software transactional memory או בקיצור STM הרעיון הבסיסי דומה לתהליך שהזכרתי קודם, במקרה בו מספר Thread – ים ניגשים למקטע זיכרון עליהם לבצע זו במסגרת transaction, במידה ומאותרת אי עקביות (למשל תהליך אחד כותב ושני קורא את אותו מקטע) אזי אחד מהם מגולגל ומתחיל מבראשית, קיימים מספר מימושים מעניינים לגישה זו מעל ה JVM כאשר אחד הבולטים הינו בשפה Clojure עליה אני מתעתד לכתוב בעתיד.
הגישה האחרונה שאעסוק בה הפעם Data flow programming נוטלת את ההשראה שלה מפסי יצור, דמיינו לרגע פס כזה המורכב משרשרת ארוכה של נקודות בכל נקודה קיים פועל (או מכונה) המקבל תוצרים מנקודות קודמות ומפיק תוצרים עבור המשך התהליך, מערכת כזו הינה מקבילית לגמרי (אין state משותף בין 2 רכיבים), דמיינו עשרות או מאות Thread-ים כל אחד מהם צורך מידע ומפיק מידע באופן בלתי תלוי בעמיתיו וקיבלתם מערכת שלא סובלת מאותן בעיות ש shread state גורם.
לסיכום, מהפכת ה multi concurrency תשנה את האופן שבו אנו מפתחים וצורכים אפליקציות, הולך להיות מעניין!