# שבוע 10 : פרק Threads חלק א ` קורס מערכות הפעלה א

‫שבוע ‪#10‬‬
‫פרק‪Threads :‬‬
‫חלק א‬
‫קורס מערכות הפעלה א'‬
‫מכללת הדסה ‪ /‬מכללה חרדית‬
‫צבי מלמד‬
‫‪Tzvi.Melamed@gmail.com‬‬
‫הרצאות הקורס מבוססות במידה רבה ביותר על ההרצאות של ד"ר יורם ביברמן‬
‫© כל הזכויות שמורות לד"ר יורם ביברמן ולצבי מלמד‬
‫©צבי מלמד‬
‫‪1‬‬
‫למה ‪) ? Threads‬מוטיבציה(‬
‫• נניח שעלינו לכתוב תכנית המבצעת מספר פעילויות במקביל‪:‬‬
‫– קוראת קלט מהמקלדת‪ ,‬מהעכבר‪ ,‬ממספר קבצים‪ ,‬מהרשת‬
‫)ממספר שרתים שונים במקביל(; מציגה תמונה על המסך‪ ,‬וכן‬
‫הלאה‪.‬‬
‫• הרצתה של התכנית כתהליך יחיד תפגע ביכולתה של התכנית לבצע‬
‫את המשימות הללו במקביל‪:‬‬
‫– המתנה לקלט )או לסיום פעולת פלט( מהתקן כלשהו ‪ -‬תכנית‬
‫חסומה )‪(blocked‬‬
‫– ‪ ‬אינה יכולה בינתיים לבצע פעולה אחרת‬
‫©צבי מלמד‬
‫‪2‬‬
‫פתרון אפשרי‪ :‬ריבוי תהליכים‬
‫• הצורך אם כן‪:‬‬
‫– מקביליות רבה של התוכנית‬
‫– שחסימה‪/‬המתנה לא תעצור את המשך הפעילות‬
‫• פתרון אפשרי‪:‬‬
‫– האפליקציה )תכנית( שלנו תיצור מספר תהליכים‬
‫– כל‪-‬אחד מהם יהיה אחראי על תת‪-‬משימה אחרת של התכנית‪.‬‬
‫– כאשר אחד התהליכים ממתין\חסום ‪ ‬האחרים יכולים‬
‫להמשיך בפעולתם‪.‬‬
‫©צבי מלמד‬
‫‪3‬‬
‫מגבלות הפתרון‬
‫‪ .1‬חוסר יכולת לחלוק מידע "בקלות ובטבעיות"‪:‬‬
‫–‬
‫בהנחה שהתהליכים השונים מבצעים משימה שלמה אחת יהיה עליהם‬
‫לחלוק מידע‪.‬‬
‫–‬
‫כלים שראינו לשיתוף מידע בין תהליכים – לכל אחד המגבלה שלו‬
‫–‬
‫במקום שצריכים קריאות מערכת – האטה בתוכנית‬
‫‪ .2‬ייצור תהליך – פעולה ארוכה ויקרה‬
‫‪ .3‬בזבוז זיכרון – לתהליכים שמיוצרים אותו קטע קוד )שמבצע פעולות דומות‬
‫עבור תהליכים שונים(‬
‫‪ .4‬הצפת המערכת בתהליכים‬
‫–‬
‫לדוגמא‪ ,‬אם התכנית שלנו היא שרת שמייצר מספר רב‪-‬מאוד של תהליכים‬
‫‪ -‬אזי הבזבוז בסעיפים )‪ (2‬ו‪ (3) -‬יגדל‬
‫©צבי מלמד‬
‫‪4‬‬
‫• מסקנה‪ :‬נשמח לפתרון פחות יקר‪.‬‬
‫• התשובה לצורך הזה‪:‬‬
‫– פתיל או חוט – ‪Thread‬‬
‫– נקרא גם תהליכון או )‪.lightweight process (LWP‬‬
‫– קיים בכל מערכות ההפעלה המודרניות ) ‪Linux, Unix,‬‬
‫)… ‪(Windows, Mac OS X, iOS,‬‬
‫©צבי מלמד‬
‫‪5‬‬
‫‪ – Threads‬הרעיון הכללי‬
‫• תהליך יחיד‪:‬‬
‫– מרחב כתובות יחיד‪,‬‬
‫– מוקצה לו ‪ PCB‬יחיד‪,‬‬
‫– )עד כאן כרגיל ‪ -‬כפי שהיכרנו(‬
‫– יוכל להריץ פונקציות שונות )של אותה תוכנית( במקביל זו לזו‪.‬‬
‫– "הדברים האלו" שרצים במקביל – )בדומה לתהליכים שרצים‬
‫במקביל( – נקראים ‪ Threads‬או "חוטים"‬
‫• כלומר‪ :‬התהליך )או ליתר דיוק‪ :‬החוט הראשי בתהליך( יכול לזמן‬
‫פונקציה )(‪ – f‬אבל‪ ,‬לא להמתין לסיומה )עד לחזרה ממנה( אלא להמשיך‬
‫בביצוע הפקודה הבאה לאחר הזימון‪ ,‬במקביל לביצוע של הפונקציה )(‪f‬‬
‫©צבי מלמד‬
‫‪6‬‬
‫‪ – Threads‬הרעיון הכללי‬
‫• כמובן‪ ,‬שאם המחשב שלנו כולל מעבד יחיד אזי המקביליות היא‬
‫רק במובנים‪:‬‬
‫– החוטים השונים רצים במקביל‪ ,‬כשם שתהליכים רצים במקביל‬
‫– זאת אומרת‪ ,‬בפועל רצים סדרתית‪ ,‬אבל הזמן מתחלק‬
‫ביניהם‬
‫– חסימת הפתיל שמבצע את )(‪ f‬אינה צריכה לגרום לחסימתו‬
‫של הפתיל הראשי‪.‬‬
‫– אם המחשב שלנו כולל מספר מעבדים המקביליות תוכל להיות‬
‫'אמיתית' )בדרגה של מספר המעבדים(‪.‬‬
‫• כלל החוטים שמרכיבים אפליקציה= ‪.Thread Group‬‬
‫©צבי מלמד‬
‫‪7‬‬
‫‪ – Threads‬הרעיון הכללי‬
‫• התוצאה‪ :‬על אותם נתונים – אותו מרחב כתובות‬
‫– מקטע קוד‬
‫– מקטע נתונים=משתנים גלובליים‬
‫– קבצים פתוחים‪ ,‬טפלי סיגנלים וכו'‬
‫• מתבצעות במקביל סדרות פעולות שונות )כלומר מורצות פונקציות‬
‫שונות(‪.‬‬
‫• כל סדרה מהווה ‪.thread‬‬
‫©צבי מלמד‬
‫‪8‬‬
‫‪) process‬תזכורת(‬
‫©צבי מלמד‬
‫‪9‬‬
‫‪process and threads‬‬
‫כדי שכל פתיל יוכל לבצע את הפונקציה 'עליה הוא מופקד' מוקצה לכל פתיל ‪ PC‬משלו‪ ,‬אזור‬
‫משלו על המחסנית התהליך )קטע בו יישמרו פרמטרי הפונקציה‪ ,‬המשתנים הלוקליים‪ ,‬וכל‬
‫המידע שנשמר בעת זימון פונקציה‪ .‬המידע אודות ה‪ Threads-‬נשמר במבנה‪Thread :‬‬
‫‪Control Block‬‬
‫©צבי מלמד‬
‫‪10‬‬
‫שימוש בפתילים – יתרונות וחסרונות‬
‫יתרונות‬
‫•‬
‫מהירות יצירת ‪Threads‬‬
‫•‬
‫מהירות החלפת הקשר‬
‫•‬
‫תקשורת מהירה יותר בין ‪threads‬‬
‫•‬
‫תקשורת קלה יותר )הזכרון משותף(‬
‫– גישה למשתנים גלובליים‬
‫– גישה לקבצים – כולם‬
‫משתמשים באותה טבלת‬
‫מתארי הקבצים‬
‫חסרונות‬
‫•‬
‫ללא ההגנות שמערכת ההפעלה‬
‫מספקת בין תהליכים )הפרדה‬
‫מוחלטת של מרחב הזיכרון( –‬
‫פגיעות רבה‪ :‬ניתן "בקלות" לקלקל‬
‫את תוכן הזיכרון‬
‫•‬
‫‪ ‬לכן יש להגן על הפתילים – אחד‬
‫מפני משנהו‬
‫•‬
‫פתילים של אותו תהליך חייבים‬
‫לרוץ על אותה מכונה‬
‫©צבי מלמד‬
‫‪11‬‬
process ‫ מול‬Thread ‫השוואת זמנים – יצירת‬
‫לא להתעמק‬
‫המספרים האלו הם‬
,‫רק לסבר את העין‬
‫לגבי ההבדלים‬
‫בזמנים‬
Note: don't expect the system and user times to add up to real time,
because these are SMP systems with multiple CPUs working on the
problem at the same time .
(SMP: Symmetric Multi-Processor systems – ‫) מחשב מרובה מעבדים‬
12
‫©צבי מלמד‬
‫שימוש בפתילים – אתגרים נוספים‬
‫כמו בכל תיכנות מקבילי‪:‬‬
‫• יש חשש ממצב תחרות )‪ :(race condition‬תוצאת ריצתה של‬
‫התכנית תקבע )גם( על‪-‬פי האופן המקרי בו מערכת ההפעלה מזמנת‬
‫את הפתילים השונים‪ ,‬ועל‪-‬כן עלולה להשתנות מהרצה להרצה‪ ,‬או‬
‫לא להיות בהתאם למצופה\נדרש )דוגמאות בהמשך(‪.‬‬
‫©צבי מלמד‬
‫‪13‬‬
‫‪User Threads, Kernel Threads‬‬
‫• קיימת אבחנה בין‪:‬‬
‫– ‪ – user threads‬אינם מוכרים למערכת ההפעלה‬
‫– ‪ - kernel threads‬מוכרים לגרעין )נוצרים על ידי קריאות‬
‫מערכת(‪.‬‬
‫• ‪user threads‬‬
‫– כשתכניתנו מייצרת ‪ User-Thread‬היא מזמנת פונקצית ספריה‬
‫במרחב המשתמש‪ ,‬שמייצרת ‪ thread‬שאינו מוכר למערכת‬
‫ההפעלה‪.‬‬
‫– מבחינת מערכת ההפעלה‪ :‬התוכנית )ליתר דיוק‪ :‬תהליך( הוא‬
‫אחד‪ ..‬רצף אחד של קוד הרצה‪.‬‬
‫• אם כך – כיצד קוראת המקביליות?‬
‫©צבי מלמד‬
‫‪14‬‬
‫‪User Threads, Kernel Threads‬‬
‫•‬
‫אם כך – כיצד קוראת המקביליות?‬
‫– אנו משתמשים בספרית פתילים – ‪thread library‬‬
‫– ספרית הפתילים לוקחת על עצמה תפקיד שדומה במקצת )רק במקצת(‬
‫למערכת ההפעלה‪ .‬היא מחלקת את הזמן )שמוקצה לכל התהליך( לחריצי‬
‫זמן ‪ time-slots‬שמוקצים לפתילים השונים‬
‫– בדומה ל'החלפת הקשר' )‪ (context switch‬של תהליכים – מתקיימת‬
‫החלפת הקשר של פתילים‪ ...‬אבל‬
‫– החלפת ההקשר בין הפתילים שקופה לגרעין‪ ,‬הינה באחריות הספרייה‪ ,‬אשר‬
‫מממשת בעצמה את הפעולה‪:‬‬
‫• שמירת בזכרון של מצבו של פתיל א' שמופסק לרוץ )מצבו‪ :‬תוכן‬
‫האוגרים(‬
‫• וטעינת המצב בו הופסק פתיל ב' למעבד לשם הרצתו‬
‫©צבי מלמד‬
‫‪15‬‬
‫משמעויות של ‪user threads‬‬
‫‪ .1‬כאשר אחד הפתילים בתכניתנו מזמן קריאת מערכת חוסמת‬
‫)לדוגמה‪ :‬כדי לקרוא נתון(‪ ,‬מערכת ההפעלה‪ ,‬שאינה מודעת לכך‬
‫שתכניתנו מורכבת ממספר פתילים‪ ,‬חוסמת את )כלל( התהליך‪,‬‬
‫וגם פתילים אחרים בתכנית לא יוכלו להמשיך בפעולתם‪.‬‬
‫‪ .2‬במערכת רבת מעבדים‪ ,‬פתילים שונים הנכללים באותה תכנית לא‬
‫יוכלו לרוץ במקביל‪.‬‬
‫‪ .3‬ומנגד‪ :‬כפי שציינו‪ ,‬פעולת ייצור פתיל‪ ,‬או החלפת הקשר בין‬
‫פתילים‪ ,‬אינן מצריכות שרות של מערכת ההפעלה על כן הינן‬
‫מהירות מאוד באופן יחסי‪.‬‬
‫©צבי מלמד‬
‫‪16‬‬
‫משמעויות של ‪user threads‬‬
‫‪ .4‬לפחות לכאורה‪ ,‬ספריית הפתילים עשויה לחלק את חריץ הזמן בין‬
‫הפתילים השונים הנכללים בתהליך בצורה מותאמת יותר מאשר‬
‫מערכת ההפעלה‬
‫‪ .5‬ספריית פתילי משתמש אינה בהכרח תלוית מערכת ההפעלה ולכן‬
‫קל יותר לעשות ‪) porting‬המרה למכונה או למערכת הפעלה‬
‫אחרת( של קוד שנכתב באמצעות ספריית הפתילים‬
‫©צבי מלמד‬
‫‪17‬‬
‫פתילי גרעין לעומת פתילי משתמש‬
‫• פתילי גרעין הם החלופה האחרת‪ :‬הם נתמכים ע"י גרעין מערכת‬
‫ההפעלה אשר מייצר‪ ,‬מתזמן‪ ,‬ומנהל אותם‪.‬‬
‫• כפי שכבר אמרנו הם איטיים יותר ביצירה ובניהול )שכן מחייבים‬
‫קבלת שירות מהגרעין(‪ ,‬אבל‪ :‬חסימת פתיל א' של האפליקציה‬
‫אינה מונעת מפתיל ב' להמשיך לרוץ‪ ,‬ואם המחשב כולל מספר‬
‫מעבדים ניתן להריץ פתילים שונים במקביל‪.‬‬
‫• המונח ‪ lightweight process‬מתייחס בדרך כלל לפתילי גרעין‬
‫דווקא‪.‬‬
‫©צבי מלמד‬
‫‪18‬‬
‫מודלים של ריבוי פתילים‬
‫• שימוש ב‪ user threads-‬נקרא‪ -‬מודל של רבים ליחיד )‪Many-to-‬‬
‫‪:(One Model‬‬
‫– פתילי משתמש רבים בה ממופים לפתיל גרעין יחיד‬
‫• כאשר כל פתיל משתמש ממופה לפתיל גרעין יחיד‪ :‬מוד יחיד‪-‬ליחיד‬
‫)‪,(One-to-One Model‬‬
‫• מודל של רבים לרבים )‪ .(many-to-Many Model‬על‪-‬פי מודל זה ‪n‬‬
‫פתילי משתמש ממופים ל‪ (n ≥) m :‬פתילי גרעין‪ m) .‬עשוי להיות‬
‫תלוי מכונה‪ ,‬או תלוי אפליקציה(‪.‬‬
‫©צבי מלמד‬
‫‪19‬‬
‫מודלים של ריבוי פתילים‬
‫• היתרון והרעיון במודל ‪many-to-many‬‬
‫• מצד אחד‪ :‬חסימת פתיל כלשהו באפליקציה לא תחסום אותה‬
‫לחלוטין‪ m-1 ,‬פתילים אחרים בה יוכלו לרוץ;‬
‫• מצד שני‪ :‬גם אם האפליקציה תייצר שפע של פתילים הדבר לא‬
‫'יציף' את הגרעין שמגביל את מספר הפתילים מהאפליקציה‬
‫אליהם הוא מוכן להתייחס‬
‫• )בפרק ‪ #10‬נלמד מדוע הגרעין רוצה להגן על עצמו מפני 'הצפה'‬
‫שכזאת‪ ,‬במילים אחרות‪ :‬מה רע בכך שהמערכת תריץ מספר רב של‬
‫פתילים?(‬
‫©צבי מלמד‬
‫‪20‬‬
‫‪ - Threads‬המשך‪ ...‬חלק ב'‬
‫יום א'‪15.1.2012 ,‬‬
‫©צבי מלמד‬
‫‪21‬‬
‫חזרה על פתילים – עד כה‬
‫• מוטיבציה –‬
‫– הרבה פעילויות במקביל‬
‫– מניעת חסימה )פעולה חוסמת נקודתית‪ ,‬ולא את "השאר"(‬
‫– חסכון בתקורה שנובעת מריבוי תהליכים‬
‫– שיתוף נתונים‬
‫• פתרון‪:‬‬
‫– "תהליך קל" ‪LWP = Light Weight Process‬‬
‫– אם יהיה לנו עותק של רגיסטרים ומחסנית )רגיסטרים כוללים גם‬
‫‪ – (IP‬אזי ניתן לקיים רצף‪-‬ביצועים‪ ,‬חוט‪-‬ביצועים נוסף‬
‫– שאר המשאבים של התהליך – משותפים )זיכרון‪ ,‬טבלת קבצים‪,‬‬
‫‪ ,PCB‬וכו'(‬
‫©צבי מלמד‬
‫‪22‬‬
‫חזרה על פתילים – עד כה‬
‫©צבי מלמד‬
‫‪23‬‬
‫חזרה על פתילים – עד כה‬
‫• כאשר ביצענו )(‪ fork‬מה שקרה‬
‫– התהליך שוכפל במלואו‪ ,‬כולל כל משאביו‬
‫– גם האב וגם הבן המשיכו בביצוע "כרגיל" – כלומר שניהם בצעו‬
‫את הפקודה הבאה אחרי ה‪fork()-‬‬
‫– כיצד נקבע שזאת הפקודה שתתבצע? ‪ ‬כי שכפלנו גם את ה‪-‬‬
‫‪(insruction pointer) IP‬‬
‫• יצירת ‪ :thread‬קריאה לפונקציה )(‪ pthread_create‬כאשר –‬
‫מעבירים לה )בין היתר(‪:‬‬
‫א‪ -‬מצביע לפונקציה לביצוע )שצריכה להיות בפרוטוטייפ מסוים(‬
‫ב‪ -‬פרמטר לאותה פונקציה )מטיפוס * ‪(void‬‬
‫ג‪ -‬ה‪ thread-‬החדש יתחיל את חייו בפונקציה הזאת‬
‫©צבי מלמד‬
‫‪24‬‬
‫חזרה על פתילים – עד כה‬
‫• סיום הביצוע של תהליך‪:‬‬
‫– הפונקציה )(‪exit‬‬
‫– ‪ return‬מה‪main -‬‬
‫– הורגים את התהליך מבחוץ‬
‫• סיום הביצוע של ‪:thread‬‬
‫א‪ -‬הפונקציה )(‪pthread_exit‬‬
‫ב‪ – thread-cancellation -‬פתיל אחד מצווה על השני להסתיים‬
‫ג‪ return -‬מהפונקציה שבה הפתיל התחיל את חייו‬
‫•‬
‫)(‪ pthread_exit‬בתהליך עם פתיל יחיד – דינו כדין )(‪exit‬‬
‫©צבי מלמד‬
‫‪25‬‬
‫חזרה על פתילים‬
user versus kernel threads – many to one
26
‫©צבי מלמד‬
‫חזרה על פתילים‬
user versus kernel threads – one to one
27
‫©צבי מלמד‬
‫היבטים נוספים של ‪Threads‬‬
‫• בהמשך נדון בהיבטים נוספים של ‪ Threads‬כגון‪:‬‬
‫– ‪ – threads & fork‬מה קורה כשאחד הפתילים בתוכנית שלנו‬
‫ביצע )(‪?fork‬‬
‫– ‪ – threads & exec‬מה קורה כשאחד הפתילים בתוכנית שלנו‬
‫ביצע )(‪?exec‬‬
‫– ‪ – thread cancellation‬ביטול פתילים‪ ,‬שהיא פעולה אנלוגית‬
‫להריגת תהליך‬
‫– טיפול בסיגנלים‬
‫– ‪ – Thread Specific Data‬האם באמת כל המידע משותף‪ ,‬או‬
‫שאפשר )ורצוי( שחלק מהמידע יהיה פרטי לפתיל?‬
‫– ועוד כהנה‪...‬‬
‫• אבל לפני כן‪ ..‬נראה כיצד דוגמא ראשונה של פתילים‬
‫©צבי מלמד‬
‫‪28‬‬
‫קפיצה‪ ...‬נראה תוכנית ראשונה‬
‫)ואולי גם יותר(‬
‫©צבי מלמד‬
‫‪29‬‬
‫‪POSIX Threads - Pthreads‬‬
‫• ‪ pthread‬הוא תקן של ‪ POSIX‬שמגדיר ‪ API‬ליצירת וסינכרון‬
‫פתילים‬
‫– אינו קובע כיצד ימומשו הפתילים‬
‫– אינו קובע באיזה מידה הם יוכרו ע"י הגרעין‬
‫– בלינוקס ‪ pthread -‬משמש ליצירת פתילי משתמש‪.‬‬
‫• ה‪ include -‬הדרוש‪:‬‬
‫>‪#include <pthread.h‬‬
‫• ופקודת הקומפילציה‪:‬‬
‫‪gcc –Wall –o my_prog –l pthread my_prog.c‬‬
‫©צבי מלמד‬
‫‪30‬‬
‫כיצד )היכן( ‪ Thread‬מתחיל את חייו?‬
‫• תזכורת‪ :‬בפקודת )(‪ - fork‬התהליך החדש שנוצר מתחיל את‬
‫ריצתו בפקודה העוקבת לפקודת ה‪fork() -‬‬
‫• ולהבדיל‪ Thread :‬נוצר על ידי )(‪pthread_create‬‬
‫– )(‪ pthread_create‬מקבלת כארגומנט מצביע לפונקציה‬
‫– ה‪ thread-‬החדש מתחיל את ריצתו בפונקציה זאת‬
‫– הארגומנטים לפונקציה הזאת? ‪ -‬גם כן מועברים ל‪-‬‬
‫)(‪pthread_create‬‬
‫©צבי מלמד‬
‫‪31‬‬
‫צעדים ראשונים‪ :‬ייצור וסיום פתיל‬
‫טיפוס )מספר שלם או מבנה(‬
‫שמכיל את מזהה הפתיל יחסית‬
‫לתהליך )ולא כללית במערכת(‬
‫; ‪pthread_t thread_data‬‬
‫‪int a = 1,‬‬
‫; ‪b = 2‬‬
‫; ‪int status‬‬
‫‪status = pthread_create‬‬
‫‪(&thread_data,‬‬
‫דגלים המציינים כיצד יש לייצר‬
‫‪NULL,‬‬
‫את הפתיל‪ .‬נהוג להעביר ‪NULL‬‬
‫בארגומנט זה‪ ,‬כלומר לבחור‬
‫‪my_func,‬‬
‫במחדלים )=פתיל משתמש‪ ,‬שניתן ;)‪(void *) &a‬‬
‫להמתין לסיומו(‬
‫{ )‪if (status != 0‬‬
‫; )"‪perror("pthread_create failed in main‬‬
‫; )‪exit(EXIT_FAILURE‬‬
‫}‬
‫©צבי מלמד‬
‫‪32‬‬
‫צעדים ראשונים‪ :‬ייצור וסיום פתיל‬
‫מצביע לפונקציה שמהווה את‬
‫קוד הפתיל‪ .‬מכאן תתחיל ריצת‬
‫הפתיל‬
‫מחזירה אפס בהצלחה‪ ,‬ערך‬
‫שונה מאפס במקרה שקרתה‬
‫שגיאה‬
‫; ‪pthread_t thread_data‬‬
‫‪int a = 1,‬‬
‫; ‪b = 2‬‬
‫; ‪int status‬‬
‫‪status = pthread_create‬‬
‫‪(&thread_data,‬‬
‫‪NULL,‬‬
‫מצביע לארגומנט שמועבר‬
‫‪my_func,‬‬
‫לפונקציה ‪my_func‬‬
‫;)‪(void *) &a‬‬
‫)שמהווה את קוד הפתיל(‬
‫{ )‪if (status != 0‬‬
‫; )"‪perror("pthread_create failed in main‬‬
‫; )‪exit(EXIT_FAILURE‬‬
‫}‬
‫©צבי מלמד‬
‫‪33‬‬
‫צעדים ראשונים‪ :‬ייצור וסיום פתיל‬
‫• הפרמטר האחרון הוא מצביע מטיפוס * ‪ void‬ל ַארגומנטים‬
‫ל ַפונקציה שמהווה את קוד הפתיל‪.‬‬
‫• ‪ ‬כלומר זאת חייבת להיות פונקציה שמקבלת ארגומנט יחיד ‪-‬‬
‫מצביע מטיפוס * ‪void‬‬
‫• הפונקציה מן הסתם‪ ,‬תעשה לארוגמנט הזה ‪ cast‬לטיפוס הרצוי לה‬
‫• אם דרוש יותר מארגומנט אחד‪ :‬יוצרים ‪ struct‬אן מערך שמכיל את‬
‫הארגומנטים‪ ,‬ומעבירים לפונקציה מצביע אליו‬
‫• בנוסף‪ ,‬היא חייבת להיות פונקציה שמחזירה ערך מטיפוס * ‪void‬‬
‫• כלומר‪ ,‬הפרוטוטייפ של הפונקציה הוא‪:‬‬
‫)‪void *f (void *params‬‬
‫©צבי מלמד‬
‫‪34‬‬
‫מיד אחרי )(‪pthread_create‬‬
‫• אחרי שבוצעה בהצלחה הקריאה ל‪,pthread_create() -‬‬
‫בתכנית שלנו רצים במקביל שני פתילים‪:‬‬
‫– פתיל ראשי‪ ,‬הממשיך בביצוע ה‪) main -‬או הקוד שביצע באותו‬
‫רגע( הפונ' שהורצה קודם לכן(‪ ,‬ופתיל משני המריץ את‬
‫‪.my_func‬‬
‫– שני הפתילים נכללים באותו תהליך ולכן יש להם אותו ‪,pid‬‬
‫– לכל אחד מהם יש מספר פתיל שונה )שאינו מוכר למערכת‬
‫ההפעלה‪ ,‬אלא רק לספריית הפתילים(‪.‬‬
‫©צבי מלמד‬
‫‪35‬‬
‫יציאה מה‪Thread-‬‬
‫• הפקודה‪:‬‬
‫; )‪pthread_exit(NULL‬‬
‫– מסיימת את ביצוע הפתיל )שמריץ את הפקודה הזאת(‬
‫– נכון גם עבור הפתיל הראשי‬
‫– אם כל הפתילים בתכנית מבצעים פקודה זאת אזי התהליך‬
‫מסתיים‪ ,‬ערך ההחזרה שלו הוא אפס‬
‫• הפונקציה הראשית של כל פתיל מחזירה בהכרח *‪,void‬‬
‫ובפקודת ה‪ pthread_exit -‬אנו מחזירים את המצביע הדרוש‬
‫)בפרט ‪ NULL‬אם אין לנו מה להחזיר(‪.‬‬
‫©צבי מלמד‬
‫‪36‬‬
pthread1.c ‫התוכנית‬
// file: pthread1.c
// a first thread example.
// compile: gcc -Wall -lpthread pthread1.c
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<pthread.h>
<unistd.h>
<time.h>
void* my_func(void *) ;
//-----------------------------------------
37
‫©צבי מלמד‬
pthread1.c ‫התוכנית‬
int main() {
pthread_t thread_data ;
int a = 1,
b = 2 ;
int status ;
srand((unsigned)time(NULL)) ;
‫ ולא להשתמש‬fputs()
status = pthread_create(&thread_data,
? perror() -‫ב‬
NULL, my_func,
void *) &a) ;
if (status != 0) {
fputs("pthread_create failed in main", stderr) ;
exit(EXIT_FAILURE) ;
‫למה‬
{
my_func( (void *) &b) ;
puts("we do not reach this place") ;
return(EXIT_SUCCESS) ;
}
38
‫©צבי מלמד‬
‫למה לא מגיעים‬
? ‫לכאן‬
pthread1.c ‫התוכנית‬
void* my_func(void * args) {
int i, sl=0;
int * val = (int *) args ;
-‫ ב‬2 val ‫ערכו של‬
1 -‫ הראשי ו‬thread
‫ שנוצר‬thread-‫ב‬
for (i= 0; i< 5; i++) {
printf("process = %d thread= %d (%u) i= %d (slept: %d)\n",
getpid(),* val,
(unsigned int) pthread_self(), i, sl) ;
sleep(sl=rand()%8) ;
}
-‫שקול ל‬
getpid()
pthread_exit(NULL) ;
}
‫שני הפתילים‬
‫יסתיימו כאן‬
39
‫©צבי מלמד‬
pthread1.c ‫הרצת התוכנית‬
3:51:20pm-root: ~/workspace/os/thread >
3:51:31pm-root: ~/workspace/os/thread > gcc –Wall \
pthread1.c -lpthread
3:52:20pm-root: ~/workspace/os/thread > a.out
process = 29190 thread= 2 (3086808768) i= 0 (slept:
process = 29190 thread= 1 (3086805920) i= 0 (slept:
process = 29190 thread= 1 (3086805920) i= 1 (slept:
process = 29190 thread= 2 (3086808768) i= 1 (slept:
process = 29190 thread= 2 (3086808768) i= 2 (slept:
process = 29190 thread= 2 (3086808768) i= 3 (slept:
process = 29190 thread= 1 (3086805920) i= 2 (slept:
process = 29190 thread= 2 (3086808768) i= 4 (slept:
process = 29190 thread= 1 (3086805920) i= 3 (slept:
process = 29190 thread= 0 (3086805920) i= 4 (slept:
40
‫©צבי מלמד‬
0)
0)
3)
7)
0)
2)
6)
5)
6)
7)
‫הרצת התוכנית ‪pthread1.c‬‬
‫האם הכל נראה תקין? ‪) -‬רמז ‪ -‬התשובה חיובית‪ ,‬לעת עתה(‬
‫©צבי מלמד‬
‫‪41‬‬
‫צביעת הטקסט‬
‫©צבי מלמד‬
‫‪42‬‬
‫הפונקציה )(‪ + my_func‬צביעת הטקסט‬
‫©צבי מלמד‬
‫‪43‬‬
‫הערות לתוכנית‬
‫‪ .1‬הטיפוס ‪) pthread_t‬שומר את מזהה הפתיל(‪:‬‬
‫–‬
‫–‬
‫עשוי להיות מבנה או פרימיטיבי )‪.(int‬‬
‫למשל כדי לדעת האם הפּ ְ נ ִיה הנוכחית‬
‫אם צריכים להשוות מספרי פתילים )‬
‫היא "אלי"(‪:‬‬
‫; )‪int pthread_equal(pthread_t tid1, pthread_t tid1‬‬
‫–‬
‫משווה מספרי פתילים‪ ,‬מחזירה אפס אם הם שווים‪ ,‬ואחרת מחזירה ≠ ‪0‬‬
‫‪ .2‬בדוגמה שראינו לשני הפתילים היה אותו מספר תהליך‪ .‬בלינוקס זה לא חייב‬
‫להיות כך‪ .‬מימוש ‪ pthread_create‬עשוי להיות באמצעות קריאת מערכת‬
‫)(‪ clone‬הייחודית ללינוקס‪ ,‬עליה נדבר בהמשך‪ ,‬ואשר עשויה ליצור תהליך‬
‫חדש החולק משאבים עם אביו כך שהינו למעשה פתיל‪ .‬בלינוקס זה נקרא ‪task‬‬
‫©צבי מלמד‬
‫‪44‬‬
‫הערות לתוכנית‬
‫‪ exit() .3‬מסיימת תהליך שלם‬
‫–‬
‫‪ ‬אם אחד הפתילים יבצע אותה כלל התהליך יסתיים‪ ,‬כולל‬
‫כל הפתילים הנכללים בו‪.‬‬
‫– שימו לב שבדוגמה שלנו גם הפתיל הראשי מבצע‬
‫)(‪ pthread_exit‬כאשר הוא מריץ את ‪ ,my_func‬בפרט הוא‬
‫לא מגיע לבצע את פקודת הפלט שבסופו‪ ,‬ואת ה‪-‬‬
‫;)‪return(EXIT_SUCCESS‬‬
‫©צבי מלמד‬
‫‪45‬‬
‫הרצת התוכנית ‪pthread1.c‬‬
‫האם הכל נראה תקין? ‪) -‬כבר לא כל כך(‬
‫נשים לב לאפס הזה‪ ..‬מה בעצם קרה כאן?‬
‫©צבי מלמד‬
‫‪46‬‬
‫הרצות נוספות‪ ...‬מה קורה כאן?‬
‫ומה קורה כאן??‬
‫©צבי מלמד‬
‫‪47‬‬
‫נשים לב לאפס הזה‪ ..‬מה בעצם קרה כאן?‬
‫הפתיל מספר ‪ 1‬הוא הפתיל החדש שנוצר‪ ,‬והפתיל‬
‫מספר ‪ 2‬הוא הפתיל הראשי )שימו לב לזה בקוד‬
‫של התוכנית(‪.‬‬
‫כלומר – מקורו של המספר ‪ 1‬הוא במשתנה ‪a‬‬
‫ששלחנו כפרמטר לפונקציה‪ .‬למעשה‪ ,‬לא שלחנו‬
‫את ‪ by-value - a‬אלא שלחנו מצביע אליו –‬
‫כלומר ‪ .by-reference‬שימו לב שמשתנה זה הוא‬
‫משתנה לוקלי של ‪main‬‬
‫רצה הגורל‪ ...‬ובגלל ההגרלות‪ ,‬הפתיל הראשי‪,‬‬
‫מספר ‪ ,2‬הסתיים ראשון‪ .‬ברגע שהוא הסתיים‪,‬‬
‫התקפלה המחסנית שלו‪ ,‬והתקפלה גם הפונקציה‬
‫‪ .main‬ולכן‪ ,‬המשתנה ‪ a‬הפסיק להיות ולידי‪.‬‬
‫בכל אופן‪ ,‬פתיל מספר ‪ 1‬נשאר עם המצביע‬
‫למשתנה הזה – וקרה‪ ,‬שאותו שטח נכתב עליו‬
‫הערך אפס‪ ...‬אז זה מה שהודפס‪.‬‬
‫©צבי מלמד‬
‫‪48‬‬
‫עוד הערות לתוכנית‬
‫‪.4‬‬
‫•‬
‫ראינו את )‪ pthread_exit(NULL‬המסיימת פתיל )תוך החזרת ערך מטיפוס‬
‫* ‪.(void‬‬
‫–‬
‫כאלטרנטיבה הפתיל רשאי לבצע פקודת ‪ return‬מהפונקציה שלתוכה הוא נוצר‬
‫)ולהחזיר ערך מטיפוס * ‪. (void‬‬
‫–‬
‫לדוגמה‪return( (void *) &num) :‬‬
‫יש רק לתת את הדעת היכן הוקצה ‪ num‬ומה קורה לו אחרי סיום הפתיל‪ .‬אך זה‬
‫נכון גם לגבי )(‪ .pthread_exit‬לכן אולי סביר שנרצה להגדיר‪:‬‬
‫))‪int *ret_val = (int *) malloc(sizeof(int‬‬
‫הכנס את ערך ההחזרה של הפתיל ‪*ret_val = 0 ; //‬‬
‫להחזיר תוך ביצוע ההמרה הדרושה ‪pthread_exit((void *) ret_val) ; //‬‬
‫הסיבה להקצאת באמצעות ‪ – malloc‬שלא יהיה לנו ערך מקומי‬
‫על המחסנית‪ ...‬שלא תקרה התופעה שהסברנו קודם‪ :‬בשקף‬
‫עם הכותרת "נשים לב לאפס הזה‪ ...‬מה בעצם קרה כאן(‬
‫©צבי מלמד‬
‫‪49‬‬
‫עוד על ספרית הפתילים ומקביליות‬
‫•‬
‫•‬
‫•‬
‫•‬
‫‪ POSIX‬קובע סטנדרט ל ‪ . API‬המימוש של הסטנדרט הזה –‬
‫כלומר‪ ,‬מימוש ספרית הפתילים – שונה בין ספריות שונות‬
‫המימוש קובע למשל – כיצד תמומש המקביליות‪ ,‬כיצד מתנהלות‬
‫עדיפויות‪ ,‬ומה קורה בסיטואציות שונות‬
‫מכיוון שהמקביליות מתבצעת על ידי ספרית הפתילים‪ ,‬ולא על ידי‬
‫הגרעין –בד"כ מתבצע ‪ non-preemptive scheduling‬אלא‬
‫‪cooperative time-sharing‬‬
‫– לא מחויב המציאות – משתנה בין מחשבים ובין גרסאות‬
‫ספרית הפתילים משתמש בטיימרים‪ ,‬סיגנלים‪ ,‬וכו' בכדי להשיג את‬
‫יעדיה‬
‫– בעצם היא משתמשת )בצורה אינטנסיבית( בכלים שמספקת‬
‫מערכת ההפעלה‬
‫©צבי מלמד‬
‫‪50‬‬
‫הערות לשאלות שסטודנטים העלו בעבר‬
‫• האם ‪ user threads‬יכולים לרוץ על מעבדים שונים?‬
‫– ‪ ‬לא‪ ,‬כי זה תפקיד של מערכת ההפעלה לשייך מעבד לתהליך‬
‫• מה המשמעות שמעבד תומך ב ‪?MT‬‬
‫– לאינטל אגב יש טכנולוגיה שהם קוראים לה ‪Hyper- = HT‬‬
‫‪) Threading‬שם שיווקי‪ ...‬לא להתרגש(‬
‫– למעבד יש העתקים )כפילויות( של הרגיסטרים וה‪ PC -‬וה‪SP-‬‬
‫)‪ – (Stack Pointer‬זהו ה‪ Context-‬של ה‪thread-‬‬
‫– ובכך הוא מייצר מעבד לוגי נוסף‪ ...‬אבל באופן פיזי=ממשי אין‬
‫לו מעבד נוסף‪.‬‬
‫– הוא יכול בפקודת מכונה אחת )פחות או יותר( להחליף את‬
‫ההקשר – אין צורך לשמור ולטעון את ההקשר – הם נשמרים‬
‫על ההעתק הפיזי‬
‫©צבי מלמד‬
‫‪51‬‬
‫)(‪Threads and fork‬‬
‫• נניח שתכניתנו מורכבת ממספר פתילים ואחד מהם ביצע )(‪fork‬‬
‫מה ייוולד?‬
‫• תשובות אפשריות‪:‬‬
‫– תהליך המריץ את אותם פתילים כפי שהיו באב‪ .‬כלומר כלל‬
‫פתילי האב ישוכפלו‪.‬‬
‫– תהליך המריץ רק את הפתיל שביצע את ה‪fork() -‬‬
‫• בלינוקס‪ :‬ה‪ fork() -‬משכפל רק את הפתיל שביצע אותו‬
‫– אם ה‪ fork() -‬בוצע ע"י פתיל משני המריץ פונקציה )(‪ ,f‬נקבל‪:‬‬
‫• תהליך א' הכולל את הפתיל הראשי )המריץ‪ ,‬למשל את‬
‫‪ (main‬ופתיל נוסף )המריץ את )(‪(f‬‬
‫• תהליך ב' שכולל פתיל יחיד )המריץ את )(‪.(f‬‬
‫©צבי מלמד‬
‫‪52‬‬
‫)(‪Threads and exec‬‬
‫•‬
‫נניח שאחד הפתילים מבצע ‪ exec‬ומחליף את מרחב הכתובות )שלו?(‪ .‬מה יהיה‬
‫האפקט על התכנית?‬
‫•‬
‫תשובות אפשריות‪:‬‬
‫‪(a‬‬
‫רק הפתיל הנוכחי מחליף את מרחב הכתובות שלו‪ ,‬ועובר להריץ תכנית‬
‫חדשה; כל יתר הפתילים ממשיכים לרוץ כרגיל‪.‬‬
‫‪ (b‬כל הפתילים הנכללים בתהליך מופסקים‪ ,‬והתהליך בכללותו 'משנה עורו'‪,‬‬
‫ועובר להריץ את התכנית החדשה‪.‬‬
‫•‬
‫בלינוקס חל מקרה )‪(b‬‬
‫–‬
‫אם פתיל ראשי הוליד פתיל משני‪ ,‬והפתיל המשני ביצע ‪ exec‬אזי גם‬
‫ביצועו של הפתיל הראשי ייקטע‬
‫–‬
‫"השטיח נשמט מתחת לכפות רגליו" ‪') -‬השטיח' = מרחב הכתובות( – כל‬
‫מרחב הכתובות מוחלף‬
‫©צבי מלמד‬
‫‪53‬‬