משימות threads
משימות היא שיטה נוספת למימוש אפליקציה בצורה של תוכניות רצות במקביל (concurrent
programming) שאפליקציות רב תהליכיות multi-process programming ותכנות רב משימתי -multi-threaded programming הם
מקרים פרטיים שלו. תכנית רב משימתית היא
למעשה תהליך אחד שהזמן הקצוב לו מתחלק בין מספר משימות. בכדי לבצע את "החלוקה" הזו נחוץ סיוע
של מערכת ההפעלה.
אפשר לסכם את ההבדלים העיקריים בין אפליקציה רב משימתית לאפליקציה רב תהליכית בכמה מאפיינים:
-
לאפליקציה רב משימתית מרחב הזיכרון משותף לכל המשימות. מזה משתמע שהעברת
מידע בין משימות הוא עניין פשוט של שימוש בזיכרון, בדרך כלל זיכרון גלובלי. מכך שגם המשימות אינן ממודרות: שגיאות בתכנות
משימה יכולה לגרום לו להשחית מה שאמור להיות זיכרון פנימי של משימה אחרת.
-
התקורה של יצירת משימה חדשה או מעבר ממשימה אחת לשנייה היא קטנה באופן
משמעותי מאשר התהליכים המקבילים שלהם במשימות.
-
קוד של משימה היא פרוצדורה או פונקציה ולא קובץ קוד על הדיסק.
-
אפליקציה רב תהליכית יכולה להיות מבוזרת,
כלומר לרוץ על יותר ממערכת מחשוב אחת. אפליקציה רב משימתי היא (כמובן) מוגבלת
למערכת מחשוב אחת.
-
באפליקציה רב תהליכית, אם אחת התהליכים קורה
תקלה נוסך חלוקה באפס רק התהליך הספציפי הזה עף, ויתר התהליכים ממשיכים. ניתן
להביא בחשבון אפשרות כזו שהאפליקציה תתאושש תעופה לא צפויה של אחד התהליכים. באפליקציה רב משימתית ההשלכה לתסריט כזה היא
תעופת כל האפליקציה.
-
באפליקציה רב משימתית, אם התוכנית הראשית מסיימת (או עפה) המשימות חדלות
להתקיים יחד איתה, גם אם הם לא סיימו.
באפליקציה רב תהליכית תהליכי בן בלתי תלויים בכל
תהליכים שיצרו אותם בכלל זה התהליך המקורי של האפליקציה, וגורלו של אף תהליך אינו
גורם לסיומו באופן אוטומטי של שום תהליך אחר.
אם תהליך מתפעל למשימות, הגורמים
הבאים משותפים לכולם:
-
קובץ הקוד
-
רוב שטחי המידע של התוכנית
-
קבצים פתוחים
-
פונקציות הטיפול בסיגנלים במידה ויש.
-
ספריה נוכחית
-
מספר מזהה של התהליך וקבוצת התהליכים.
המשימות נבדלות ביניהם במאפיינים
הבאים:
-
מספר זיהוי של המשימה
-
קבוצת ערכי אוגרים
-
קטע מחסנית פרטית ונקודה פעילה בתוך המחסנית
-
משתנים מקומיים וכתובות חזרה
-
הגדרות התגובה לסיגנלים
-
עדיפות יחסית למשימות אחרות
במידה
ומשימה מסיימת באופן תקין היא מחזירה 0 למשימה היוצרת אותה.
תכנות
משימות ב-unix: Posix Threads
מי
שרוצה להשתמש במנגנון משימות מן הסוג הזה חייב להכליל בתוכנית שלו את הפקודה
#include
<pthreads.h>
קריאות
המערכת הנפוצות ביותר
משימה
נוצרת ע"י קריאה לרוטינה pthread_create שההגדרה שלה היא
int pthread_create(pthread_t *thread,
const pthread_attr_t
*attr,
void *(*start_routine)(void *),
void *arg);
כאשר הפרמטרים הם:
tread
- פוינטר שדרכו מוחזר המספר המזהה של המשימה
attr – פוינטר לרשומה
למטרה של העברת מאפיינים רצויים
הפרמטר attr יהיה NULL אם מתכנת מעוניין במאפיינים של
ברירת מחדל וזה ערכו בדרך כלל.
start_routine – פוינטר לפונקציה
שמשמש התוכנית הראשית של המשימה
arg – פוינטר לפרמטר
(יחיד) המיועד למשימה
הפוינטר arg יכול להיות פוינטר
לכל דבר. במידה והמתכנת מעוניין להעביר מספר נתונים למשימה, הוא יכול שהפוינטר הזה
יצביע לרשומה.
משימה יכולה לסיים את הריצה שלה ע"י קריאה לקריאת מערכת pthread_exit שההגדרה שלה היא
void pthread_exit(void
*retval);
כאשר הפרמטר retval הוא פוינטר
המשמש להעברת ערך כ"תוצאה"
"קוד סיום" למשימה שיצרה אותו.
סנכרון משימות
למשימות posix יש שלוש אמצעי סנכרון:
mutexs – מנגנון/משתני
נעילות
join -
גורם למשימה להמתין לסיום של משימה/ות אחר/ות
condition variables – pthread_cond_t
משתנים מסוג
מנגנון/משתני נעילות mutexs
המטרה של מנגנוני/משתני נעילות מן הסוג הזה
הוא להבטיח שלמות integrity של מידע משותף. אם נניח
שאנחנו רוצים להעביר מידע ממשימה אחת לשנייה,
האמצעי הוא פשוט משום הזיכרון הגלובלי משותף לכל המשימות, אך קיימת בעיה של
חוסר סנכרון. תמיד קיימת האפשרות שמשימה תתחיל לעדכן את השטח ותוחלף לפני שתסיים
המלאכה והמשימה השנייה תיגש לקרוא את השטח
כאשר המידע אינו שלם או מלא. מנגנון נעילה
מאפשר למשימה להכריז על משאב שהיא שלה ואם משימה אחרת תבחן את מנגנון הנעילות היא
תיעצר עד שמשימה אחרת "תשחרר" אותה.
שיטת העבודה
מאחר ומנגנון הנעילות נכתב
עבור שפת C שאינה מונחת עצמים, השימוש במנגנון הנעילות היא לפי נוהל.
מי שרוצה לממש מנגנון
נעילות חייב:
-
להגדיר משתנה מסוג pthread_mutex_ t ולאתחל אותו
בערך PHTREAD_MUTEX_INITIALIZER
-
משימה המעוינת לנעול או להיות מסונכרת באמצעות מנגנון הנעילות קוראת לפונקציה pthread_mutex_lock עם פרמטר שהוא כתובת המשתנה מסוג pthread_mutex_ t.
-
משימה שסיימה עם המשאב ומעוניינת
לשחרר אותו לגישה של משימות אחרות חייבת לקרוא לפונקציה pthread_mutex_unlock עם פרמטר שהוא כתובת המשתנה מסוג pthread_mutex_ t.
חשוב להבין שכל משתנה מסוג
pthread_mutex_ t והשימוש
בו מגדיר מנגנון נעילות נפרד ובלתי תלוי.
לדוגמא, קוד ה-C הבא מגדיר ומשתמש במשאב שלם בשם counter ללא מנגנון נעילות:
int counter=0;
/* Function C */
void functionC()
{
counter++
}
לעומת זאת קוד ה-C הבא מגדיר ומשתמש במשאב שלם בשם counter ללא מנגנון נעילות:
/* Note scope of variable and mutex are the same */
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int counter=0;
/* Function C */
void functionC()
{
pthread_mutex_lock( &mutex1 );
counter++
pthread_mutex_unlock( &mutex1 );
}
המתנה לסיום משימה join
לעיתים משימה מעוניינת להמתין עד שמשימת בת שלה מסיימת. האפשרות הזו נתמכת ע"י קריאת המערכת pthread_join שההכרזה עליה היא
int pthread_join(pthread_t th, void **thread_return);
כאשר הפרמטר th הוא המספר
המזהה של המשימה שהמשימה הקוראת ל- pthread_join רוצה להמתין לו. הפרמטר thread_return משמש להחזרת סתוצאת הסיום של המשימה "שמצטרפים" אליה, בתנאי
שהערך של thread_return אינו NULL שמשמעותו ויתור
על שמירת תוצאת הסיום.
משתני תנאי condition variables
משתני תנאי מאפשרים למשימה
להשעות את עצמה עד שתנאי מסוים מתקיים. הדבר נעשה ע"י הגדרת משתנה מסוג pthread_cond_t והרצת קריאות מערכת
מתאימות עליו. בדרך כלל יש לתכנת את הקוד
בזהירות תוך שימוש במנגנוני נעילות אחרת משימה עשויה להמתין לתנאי שכבר התרחש ולא
יחזור יותר דבר שיגרום לתהליך הממתין להמתין לעולם (deadlock).
רשימת קריאות המערכת המשמשות לממשק לשימוש מסוג הזה של סנכרון היא
להלן:
יצירת משתני תנאי:
pthread_cond_init
pthread_cond_t cond =
PTHREAD_COND_INITIALIZER;
pthread_cond_destroy
יצירת המתנה:
pthread_cond_wait
pthread_cond_timedwait
הערת משימות ממתינות לתנאי:
pthread_cond_signal
pthread_cond_broadcast
נוהלי שיבוץ משימות
ספרית התמיכה במשימות pthread מספקת
אופציות של ברירות מחדל לתמיכה במשימות ואפשרויות מדיניות השיבוץ שהן מספיק טובות
למרבית האפליקציות. יחד עם זאת המתכנת
יכול לשנות את מדיניות השיבוץ אם הוא מעוניין בכך לאפשרויות נתמכות אחרות. את זה
אפשר לעשות בשיטות הבאות:
-
בזמן יצירת המשימה, לספק מאפיינים שאינם ברירות המחדל.
-
שינוי המאפיינים ע"י קריאות מערכת למשימה קיימת.
-
בזמן יצירת מנגנון נעילות, לספק מאפיינים שאינם ברירת המחדל
לשיבוץ של מנגנון הנעילות.
-
ע"י שינוי מאפיינים של אמצעי הסנכרון של משתני תנאי.
מלכודות ואמצעי
זהירות בתכנות משימות
תנאי הספק race conditions
כמו בכל תכנות של גורמי תוכנית אסנכרוניים, צריכה תכנת המשימות האמורות לשתף פעולה בצורה כזו
שהתנהלות האפליקציה תהיה צפויה ובלתי תלויה בגורמים שאינם בשליטת התוכנית, כמו סדר
השיבוץ של המשימות או ההספק של כל אחד מהם. יש להשתמש במנגנון סנכרון בכל מקרה שבו
סדר הפעולות של משימות שונות הוא חשוב.
שימוש בקוד "בטוח משימות" thread safe
קוד
של משימה חייבת לא לכתוב ולא להסתמך על משתנים גלובליים אלא אם כן מדובר בפירוש
במשתנים שנועדו במפורש להעברת מידע בין משימות. אם קוד מסוים עשוי לשמש יותר
ממשימה אחת, אסור לו להשתמש במשתנים המוגדרים static למעט מידע
לקריאה בלבד או משתנים שהומצאו במיוחד להעברת מידע בין משימות בדיוק מאותה סיבה.
לנקודת המבט הזה יש פן מוסתר או לא צפוי: יש
קריאות מערכת שמשתמשות במידע גלובלי או סטטי, ולפיכך אינן thread safe. בדרך כלל לקריאות
המערכת הללו יש גרסאות שהן כן בטוחות עם תוספת לשם "_r". לדוגמא, קריאת המערכת strtok אינה בטוחה
אבל קריאת המערכת strtok_r כן. לפיכך, אם קוד של משימה צריכה לקרוא לקריאת
מערכת, על המתכנת לבדוק בתיעוד אם היא בטוחה או אם קיימת לה גרסה בטוחה למשימות.
הרעבה ע"י
מנגנון הנעילות mutex deadlock
בכתיבת קוד רב משימתי הכולל נעילות תמיד יש סכנה שבו שני משימות (או
יותר) האמורות לשחרר זו את זו נעולים כל אחת לחוד וכל אחת ממתינה למשימה השנייה שתשחרר
אותה. יש מאד להיזהר לא להשאיר שום
התנהלות של הקוד ליד המקרה או יותר נכון לגורמים שאינם בשליטת התוכנית.
להלן רשימה של man pages
על קריאות מערכת התומכות ב-pthreads
pthread_atfork - register handlers to be called at
fork(2) time
pthread_attr_destroy [pthread_attr_init]
- thread creation attributes
pthread_attr_getdetachstate [pthread_attr_init]
- thread creation
attributes
pthread_attr_getinheritsched [pthread_attr_init]
- thread creation
attributes
pthread_attr_getschedparam [pthread_attr_init]
- thread creation
attributes
pthread_attr_getschedpolicy [pthread_attr_init]
- thread creation
attributes
pthread_attr_getscope [pthread_attr_init]
- thread creation attributes
pthread_attr_init - thread creation attributes
pthread_attr_setdetachstate [pthread_attr_init]
- thread creation
attributes
pthread_attr_setinheritsched [pthread_attr_init]
- thread creation
attributes
pthread_attr_setschedparam [pthread_attr_init]
- thread creation
attributes
pthread_attr_setschedpolicy [pthread_attr_init]
- thread creation
attributes
pthread_attr_setscope [pthread_attr_init]
- thread creation attributes
pthread_cancel - thread cancellation
pthread_cleanup_pop [pthread_cleanup_push]
- install and remove cleanup
handlers
pthread_cleanup_pop_restore_np [pthread_cleanup_push]
- install and remove
cleanup handlers
pthread_cleanup_push - install and remove cleanup handlers
pthread_cleanup_push_defer_np [pthread_cleanup_push]
- install and remove
cleanup handlers
pthread_condattr_destroy [pthread_condattr_init]
- condition creation
attributes
pthread_condattr_init - condition creation attributes
pthread_cond_broadcast [pthread_cond_init]
- operations on conditions
pthread_cond_destroy [pthread_cond_init]
- operations on conditions
pthread_cond_init - operations on conditions
pthread_cond_signal [pthread_cond_init]
- operations on conditions
pthread_cond_timedwait [pthread_cond_init]
- operations on conditions
pthread_cond_wait [pthread_cond_init]
- operations on conditions
pthread_create - create a new thread
pthread_detach - put a running thread in the detached
state
pthread_equal - compare two thread identifiers
pthread_exit - terminate the calling thread
pthread_getschedparam [pthread_setschedparam]
- control thread scheduling
parameters
pthread_getspecific [pthread_key_create]
- management of thread-specific
data
pthread_join - wait for termination of another thread
pthread_key_create - management of thread-specific data
pthread_key_delete [pthread_key_create]
- management of thread-specific
data
pthread_kill_other_threads_np - terminate all threads in program except
calling thread
pthread_kill [pthread_sigmask]
- handling of signals in threads
pthread_mutexattr_destroy [pthread_mutexattr_init]
- mutex creation
attributes
pthread_mutexattr_getkind_np [pthread_mutexattr_init]
- mutex creation
attributes
pthread_mutexattr_init - mutex
creation attributes
pthread_mutexattr_setkind_np [pthread_mutexattr_init]
- mutex creation
attributes
pthread_mutex_destroy [pthread_mutex_init]
- operations on mutexes
pthread_mutex_init - operations on mutexes
pthread_mutex_lock [pthread_mutex_init]
- operations on mutexes
pthread_mutex_trylock [pthread_mutex_init]
- operations on mutexes
pthread_mutex_unlock [pthread_mutex_init]
- operations on mutexes
pthread_once - once-only initialization
pthread_self - return identifier of current thread
pthread_setcancelstate [pthread_cancel]
- thread cancellation
pthread_setcanceltype [pthread_cancel]
- thread cancellation
pthread_setschedparam - control thread scheduling parameters
pthread_setspecific [pthread_key_create]
- management of thread-specific
data
pthread_sigmask - handling of signals in threads
pthread_testcancel [pthread_cancel]
- thread cancellation