משימות 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