תהליכים ותהליכונים

איך המחשב מריץ כמה דברים במקביל: Process ו-Thread, PID, תזמון, Context Switch, Race Conditions, Deadlocks, IPC, Signals ו-Thread Pools.

פורסם 2026-07-21. תגיות: Operating Systems, Concurrency, Performance.

בכל רגע נתון פועלות במחשב עשרות ולעיתים מאות תוכנות ומשימות.

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

כדי להבין כיצד כל זה מתרחש, צריך להכיר שני מושגים בסיסיים:

למרות שהם קשורים זה לזה, הם אינם אותו דבר.

תוכנית אינה תהליך

כל עוד תוכנה כמו Chrome, ‏VS Code או Spotify אינה פועלת, היא בסך הכול קוד ונתונים המאוחסנים בדיסק.

Program = תוכנית המאוחסנת בדיסק
Process = מופע פעיל של התוכנית

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

מה מערכת ההפעלה מחזיקה עבור process?

Process אינו רק קוד שרץ. מערכת ההפעלה מנהלת עבורו מידע ומשאבים כגון:

לדוגמה:

Process: chrome
PID: 8421
User: evyatar
State: Running
Memory: 350 MB

המידע הזה מאפשר למערכת ההפעלה לנהל כל process בנפרד.

מהו PID?

לכל process מוקצה מספר בשם PID, קיצור של Process ID:

Chrome    PID 4210
VS Code   PID 8351
Spotify   PID 9012

ה-PID מאפשר לבדוק את מצב התהליך, לעקוב אחר צריכת המשאבים שלו, לשלוח לו signal או לסיים אותו.

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

למה אותה תוכנה מופיעה כמה פעמים?

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

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

המחיר הוא צריכת זיכרון גבוהה יותר ותקשורת מורכבת יותר בין processes.

Parent ו-Child Processes

Process יכול ליצור process אחר. היוצר נקרא parent והנוצר נקרא child:

Terminal
└── Python
    └── Worker Process

לכל אחד יש PID משלו. ל-child יש בדרך כלל גם PPID, כלומר Parent Process ID:

Terminal PID: 1200
Python PID:   1350
Python PPID:  1200

המשמעות היא ש-process מספר `1350` נוצר על ידי process מספר `1200`.

מהו Thread?

Process הוא המסגרת שמחזיקה את המשאבים, והעבודה מתבצעת באמצעות threads.

אפשר לחשוב על process כמשרד ועל threads כעובדים בתוכו. העובדים משתמשים באותו משרד ובאותם משאבים, אבל יכולים לבצע משימות שונות:

Thread 1 → מציג את ממשק המשתמש
Thread 2 → מוריד מידע מהרשת
Thread 3 → שומר קובץ
Thread 4 → מבצע חישוב ברקע

Process פעיל מכיל לפחות thread אחד.

מה threads חולקים?

Threads בתוך אותו process חולקים בדרך כלל:

לכל thread יש גם מצב פרטי משלו:

השיתוף מאפשר תקשורת מהירה, אבל הוא גם מאפשר ל-thread אחד לפגוע בנתונים ש-thread אחר משתמש בהם.

Race Condition

נניח ששני threads מנסים להגדיל את אותו משתנה:

counter = 10

כל אחד קורא את הערך, מוסיף אחד וכותב את התוצאה. אם שניהם קוראים `10` לפני שאחד הספיק לכתוב, שניהם יחשבו `11` ושניהם יכתבו `11`.

התוצאה הסופית תהיה `11`, למרות ששתי פעולות התרחשו והציפייה הייתה ל-`12`.

זהו race condition: התוצאה תלויה בתזמון המדויק של threads ולכן הבאג עשוי להופיע רק לפעמים.

כיצד מתאמים גישה משותפת?

מנגנונים נפוצים כוללים:

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

גם לתיאום יש מחיר: המתנה, האטה, מורכבות וסיכון ל-deadlock. לכן יצירת threads היא רק תחילת התכנון; צריך להחליט גם מי מחזיק את המידע וכיצד הוא משתנה.

מהו Context Switch?

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

כאשר היא מפסיקה להריץ thread אחד ומתחילה להריץ אחר, היא צריכה לשמור את המצב הנוכחי, לבחור thread אחר, לטעון את מצבו ולהמשיך ממנו. הפעולה נקראת context switch.

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

לכן יותר threads אינם מבטיחים יותר ביצועים.

מצבי ריצה

Process או thread יכולים להימצא בכמה מצבים עיקריים:

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

Concurrency לעומת Parallelism

Concurrency פירושה שכמה משימות מתקדמות באותו פרק זמן. בליבה אחת זה יכול להיראות כך:

Thread A רץ מעט
Thread B רץ מעט
Thread C רץ מעט
Thread A ממשיך

Parallelism פירושו שכמה משימות רצות ממש באותו רגע:

Core 1 → Thread A
Core 2 → Thread B
Core 3 → Thread C

Concurrency אינה מחייבת parallelism. מערכת יכולה לנהל עבודה מקבילית בזמן גם כאשר רק משימה אחת רצה בכל רגע.

מתי לבחור Process?

נעדיף process נפרד כאשר חשוב לנו בידוד:

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

מתי לבחור Thread?

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

Threads מציעים שיתוף מהיר ועלות יצירה נמוכה יחסית, אבל הם מביאים race conditions, deadlocks וקושי באיתור באגים. שגיאת native memory חמורה ב-thread אחד יכולה להפיל את ה-process כולו; לעומת זאת, exception רגיל בשפות מסוימות עשוי לסיים רק את ה-thread, בהתאם ל-runtime ולמדיניות האפליקציה.

כיצד processes מתקשרים?

לכל process יש בדרך כלל מרחב כתובות נפרד, ולכן הוא אינו יכול פשוט לקרוא משתנה של process אחר. התקשורת מתבצעת באמצעות IPC, קיצור של Inter-Process Communication:

כל מנגנון מציע איזון אחר בין מהירות, בידוד, אמינות, מורכבות ואבטחה.

צפייה בתהליכים וסיום process

ב-Linux וב-macOS אפשר להשתמש ב:

ps -ef
top

במערכות שבהן הוא מותקן אפשר להשתמש גם ב-`htop`. ב-Windows קיימים Task Manager והפקודה `tasklist`.

כדי לבקש מתהליך להסתיים בצורה מסודרת:

kill <PID>

ברירת המחדל היא בדרך כלל `SIGTERM`. התהליך יכול ללכוד את ה-signal, לשמור מידע, לסגור קבצים וחיבורים ולבצע cleanup.

אם הוא אינו מגיב, אפשר להסלים ל:

kill -9 <PID>

הפקודה שולחת `SIGKILL`. התהליך אינו יכול ללכוד או להתעלם ממנו ואינו מקבל הזדמנות לבצע cleanup. לכן משתמשים בו כמוצא אחרון.

Zombie ו-Orphan

כאשר child process מסתיים, מערכת ההפעלה שומרת רשומה קטנה עם סטטוס הסיום שלו. ה-parent אמור לאסוף אותה באמצעות פעולה כמו `wait`.

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

Orphan הוא המצב ההפוך: ה-parent הסתיים אבל ה-child עדיין רץ. במערכות Unix תהליך מערכת או subreaper מאמץ בדרך כלל את ה-orphan ואוסף את סטטוס הסיום שלו בהמשך. Orphan אינו בהכרח תקלה; לעיתים תהליך נועד להמשיך לפעול אחרי מי שהפעיל אותו.

Zombie = ה-child הסתיים, ה-parent עדיין לא אסף אותו
Orphan = ה-parent הסתיים, ה-child עדיין רץ

מהו Thread Pool?

יצירה והרס חוזרים של threads עלולים להיות יקרים. Thread pool מחזיק מספר workers ומעביר אליהם משימות מתור:

  1. יוצרים מספר threads.
  2. מכניסים משימות לתור.
  3. כל worker לוקח משימה פנויה.
  4. לאחר הסיום הוא חוזר לקחת משימה אחרת.

כך אפשר להגביל את כמות ה-threads, לחסוך עלויות יצירה ולשלוט בעומס.

Pool קטן מדי יוצר תור ארוך. Pool גדול מדי יוצר context switches ותחרות. גם pool בגודל נכון אינו מספיק אם התור בלתי מוגבל: במקרה של עומס, המשימות עלולות להצטבר בזיכרון. לכן לעיתים צריך תור מוגבל, backpressure או מדיניות דחייה.

CPU-Bound לעומת I/O-Bound

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

משימת I/O-bound מבלה זמן רב בהמתנה לרשת, דיסק, מסד נתונים או קלט. כאן אפשר לעיתים לנהל יותר משימות במקביל משום שחלק גדול מהזמן הן אינן משתמשות במעבד.

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

Deadlock בקצרה

Deadlock נוצר כאשר threads מחכים זה לזה ואיש מהם אינו יכול להתקדם:

Thread A מחזיק Lock 1 ומחכה ל-Lock 2
Thread B מחזיק Lock 2 ומחכה ל-Lock 1

המערכת אינה בהכרח קורסת, אבל העבודה נעצרת.

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

סיכום

תוכנית היא קוד המאוחסן בדיסק. Process הוא מופע פעיל שלה שמערכת ההפעלה מנהלת. Thread הוא יחידת ביצוע בתוך ה-process.

הבחירה הנכונה אינה "כמה שיותר processes או threads". היא איזון בין ביצועים, בידוד, אבטחה, צריכת זיכרון ומורכבות.