Smart-World Surf

יחידה 10: מקביליות וקונקרנטיות

כתיבת תוכניות המבצעות מספר משימות במקביל.
תהליכונים (Threads)מנעולים (Locks)מנגנוני סנכרוןמודל אקטורים

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

מבוא למקביליות ותהליכונים

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

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

הבדלים בין תהליכים לתהליכונים:

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

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

אתגרי מקביליות ומנגנוני סנכרון

שיתוף זיכרון בין תהליכונים מביא עמו אתגרים משמעותיים. הבעיות הנפוצות ביותר הן מצבי מרוץ וקיפאון.

מצב מרוץ (Race Condition): מצב בו תוצאת התוכנית תלויה בסדר הלא-דטרמיניסטי של ביצוע פעולות על משאב משותף. לדוגמה, שני תהליכונים מנסים לעדכן משתנה גלובלי בו-זמנית.
קיפאון (Deadlock): מצב בו שני תהליכונים או יותר ממתינים זה לזה לשחרור משאב, ואינם יכולים להתקדם. לדוגמה, תהליכון A מחזיק במשאב X וממתין למשאב Y, בעוד תהליכון B מחזיק במשאב Y וממתין למשאב X.

כדי למנוע בעיות אלו, אנו משתמשים במנגנוני סנכרון.

מנעול (Lock / Mutex): מנגנון סנכרון המאפשר לתהליכון אחד בלבד לגשת לקטע קריטי (critical section) בזמן נתון, ובכך למנוע מצבי מרוץ על משאבים משותפים.

מנגנוני סנכרון נפוצים:

Mutex (מנעול הדדי)

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

Semaphore (סמפור)

מונה המאפשר שליטה על מספר התהליכונים שיכולים לגשת למשאב בו-זמנית. סמפור בינארי מתפקד כמו mutex. שימושי לניהול מאגר משאבים מוגבל.

Condition Variables (משתני תנאי)

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

Monitor (צג)

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

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

מודל האקטורים כגישה חלופית

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

מודל אקטורים (Actor Model): מודל קונקרנטיות המבוסס על יחידות חישוביות עצמאיות (אקטורים) המתקשרות ביניהן באמצעות העברת הודעות אסינכרונית בלבד, ללא מצב משותף.

עקרונות מודל האקטורים:

  • בידוד: לכל אקטור יש מצב פנימי משלו, שאינו נגיש ישירות לאקטורים אחרים.
  • העברת הודעות: אקטורים מתקשרים רק על ידי שליחת הודעות זה לזה. הודעות נשלחות לתיבת דואר (mailbox) של האקטור המקבל.
  • התנהגות: כאשר אקטור מקבל הודעה, הוא יכול לשנות את מצבו הפנימי, לשלוח הודעות לאקטורים אחרים, או ליצור אקטורים חדשים.
  • ללא מצב משותף: היעדר מצב משותף מבטל מטבעו את רוב בעיות הסנכרון כמו מצבי מרוץ וקיפאון.

מודל האקטורים פופולרי במיוחד במערכות מבוזרות ובמערכות הדורשות עמידות גבוהה וסילומיות, כגון Akka ב-Scala/Java או Erlang.

שאלות לדיון

  • השווה והבן בין תהליכון לתהליך מבחינת משאבים משותפים, בידוד, ועלות יצירה/החלפת קונטקסט. באילו תרחישים תבחר בכל אחד מהם?
  • הסבר את המושגים "מצב מרוץ" ו"קיפאון". תאר כיצד מנעולים (Mutexes) יכולים למנוע מצב מרוץ, אך עלולים להוביל לקיפאון. כיצד ניתן למנוע קיפאון?
  • מהם היתרונות והחסרונות של מודל האקטורים בהשוואה לשימוש בתהליכונים ומנעולים? באילו תרחישים תבחר בכל אחד מהמודלים?
  • תאר מצב בו שימוש במשתני תנאי (Condition Variables) הכרחי לסנכרון, והסבר כיצד הם פועלים בשילוב עם מנעולים כדי לפתור את הבעיה.

נקודות לתשובת מודל

  • תהליך מול תהליכון: תהליך הוא יחידה כבדה, עם מרחב זיכרון משלה, מבודדת. תהליכון הוא יחידה קלה בתוך תהליך, חולקת זיכרון, פחות מבודדת. תהליכים למשימות עצמאיות, תהליכונים למשימות משותפות באותו יישום.
  • מצב מרוץ וקיפאון: מצב מרוץ: תוצאה תלויה בסדר ביצוע לא דטרמיניסטי על משאב משותף. מנעולים מונעים גישה בו-זמנית לקטע קריטי. קיפאון: תהליכונים ממתינים זה לזה למשאבים. מניעה: סדר קבוע לרכישת מנעולים, הימנעות מהחזקה והמתנה, שחרור מנעולים.
  • מודל אקטורים מול תהליכונים+מנעולים: אקטורים: ללא מצב משותף, תקשורת בהודעות, פשוט יותר להבנה וסילומיות גבוהה, מתאים למערכות מבוזרות. תהליכונים+מנעולים: ביצועים גבוהים יותר במצבים מסוימים (גישה ישירה לזיכרון), אך מורכב יותר לניהול סנכרון ובאגים.
  • משתני תנאי: משמשים כאשר תהליכון צריך להמתין לתנאי מסוים שיתקיים לפני שימשיך. לדוגמה, תהליכון צרכן הממתין לפריטים בתור ריק. תמיד משולבים עם מנעול: המנעול מגן על המצב המשותף (התור), ומשתנה התנאי מאפשר לתהליכון להמתין (wait) ולתהליכון אחר לאותת (signal) כשהתנאי מתקיים.
מצאתם טעות או שחסר משהו?
→ הקודמת
טיפול בשגיאות וחריגות
הבאה ←
עקרונות תכנון שפות