ב-Linux, אתה יכול ליצור ולנהל שרשורים ב-C/C++ באמצעות ספריית השרשורים של POSIX (pthread). בניגוד למערכות הפעלה אחרות, אין הבדל קטן בין שרשור לתהליך בלינוקס. זו הסיבה שלינוקס מתייחסת לרוב לחוטים שלה כתהליכים קלים.
באמצעות ספריית pthread, אתה יכול ליצור שרשורים, להמתין עד שהם יסתיימו ולסיים אותם במפורש.
ההיסטוריה של השימוש בחוטים בלינוקס
לפני גרסה 2.6 של לינוקס, יישום השרשור הראשי היה LinuxThreads. ליישום זה היו מגבלות משמעותיות מבחינת ביצועים ופעולות סנכרון. הגבלה על המספר המרבי של שרשורים שיכולים לפעול הגבילה אותם לאלף המאה.
בשנת 2003, צוות בראשות מפתחים מ-IBM ו-RedHat הצליח ליצור את ספריית חוטים מקורית של POSIX פרויקט (NPTL) זמין. זה הוצג לראשונה בגרסה 3 של RedHat Enterprise כדי לפתור בעיות ביצועים עם ה-Java Virtual Machine ב-Linux. כיום, ספריית GNU C מכילה יישומים של שני מנגנוני השרשור.
אף אחד מאלה אינו יישום של פתילים ירוקים, שמכונה וירטואלית תנהל ותפעיל במצב משתמש בלבד. כאשר אתה משתמש בספריית pthread, הקרנל יוצר שרשור בכל פעם שתוכנית מתחילה.
אתה יכול למצוא מידע ספציפי לשרשור עבור כל תהליך פועל בקבצים מתחת /proc/
לוגיקה עבודה של חוטים
שרשורים הם כמו תהליכים הפועלים כעת על מערכת ההפעלה. במערכות מעבד יחיד (למשל מיקרו-בקרים), ליבת מערכת ההפעלה מדמה פתילים. זה מאפשר לעסקאות לפעול בו זמנית באמצעות חיתוך.
מערכת הפעלה בעלת ליבה אחת יכולה באמת להריץ תהליך אחד בכל פעם. עם זאת, ב מערכות מרובות ליבות או מעבדים מרובות, תהליכים אלה יכולים לפעול בו זמנית.
יצירת שרשור ב-C
אתה יכול להשתמש ב pthread_create פונקציה ליצירת שרשור חדש. ה pthread.h קובץ header כולל את הגדרת החתימה שלו יחד עם פונקציות אחרות הקשורות לשרשור. שרשורים משתמשים באותם מרחב כתובות ובתיאורי קבצים כמו התוכנית הראשית.
ספריית pthread כוללת גם את התמיכה הדרושה עבור mutex ופעולות מותנות הנדרשות לפעולות סנכרון.
כאשר אתה משתמש בפונקציות של ספריית pthread, עליך לוודא שהמהדר מקשר את pthread הספרייה לתוך קובץ ההפעלה שלך. במידת הצורך, תוכל להורות למהדר לקשר לספרייה באמצעות ה -ל אוֹפְּצִיָה:
gcc -o מִבְחָן test_thread.c -lpthread
לפונקציה pthread_create יש את החתימה הבאה:
intpthread_create(pthread_t *פְּתִיל, constpthread_attr_t *attr, בָּטֵל *(*שגרת_התחלה)(בָּטֵל *), בָּטֵל *ארג)
זה מחזיר 0 אם ההליך הצליח. אם יש בעיה, הוא מחזיר קוד שגיאה שאינו אפס. בחתימת הפונקציה לעיל:
- ה פְּתִיל הפרמטר הוא מסוג pthread_t. השרשור שנוצר תמיד יהיה נגיש עם הפניה זו.
- ה attr פרמטר מאפשר לך לציין התנהגות מותאמת אישית. אתה יכול להשתמש בסדרה של פונקציות ספציפיות לשרשור שמתחיל ב pthread_attr_ כדי להגדיר ערך זה. התאמות אישיות אפשריות הן מדיניות התזמון, גודל הערימה ומדיניות הניתוק.
- start_routine מציין את הפונקציה שהשרשור יפעיל.
- arg מייצג מבנה נתונים גנרי המועבר לפונקציה על ידי השרשור.
הנה יישום לדוגמה:
#לִכלוֹל
#לִכלוֹל
#לִכלוֹל
#לִכלוֹלבָּטֵל *עוֹבֵד(בָּטֵל *נתונים)
{
לְהַשְׁחִיר *שם = (לְהַשְׁחִיר*)נתונים;
ל (int אני = 0; אני < 120; i++)
{
אתה ישן(50000);
printf("היי מהשרשור שם = %s\n", שם);
}
printf("השרשור %s הסתיים!\n", שם);
לַחֲזוֹרריק;
}
intרָאשִׁי(בָּטֵל)
{
pthread_t th1, th2;
pthread_create(&th1, ריק, עובד, "X");
pthread_create(&th2, ריק, עובד, "Y");
לִישׁוֹן(5);
printf("יציאה מהתוכנית הראשית\n");
לַחֲזוֹר0;
}
סוגי חוטים
כאשר חוט חוזר מה רָאשִׁי() פונקציה ביישום, כל השרשורים מסתיימים והמערכת משחררת את כל המשאבים שבהם השתמשה התוכנית. באופן דומה, כאשר יוצאים מכל שרשור עם פקודה כמו an יְצִיאָה(), התוכנית שלך תסיים את כל השרשורים.
עם ה pthread_join פונקציה, אתה יכול לחכות לסיום שרשור במקום זאת. השרשור המשתמש בפונקציה זו יחסום עד שהשרשור הצפוי יסתיים. המשאבים שבהם הם משתמשים מהמערכת אינם מוחזרים אפילו במקרים כגון סיום שרשורים הניתנים לחיבור, לא מתוזמן על ידי ה-CPU, או אפילו אי-הצטרפות עם ptread_join.
לפעמים יש מצבים שבהם הצטרפות עם pthread_join אינה הגיונית; אם אי אפשר לחזות מתי השרשור יסתיים, למשל. במקרה זה, אתה יכול להבטיח שהמערכת מחזירה את כל המשאבים באופן אוטומטי בנקודה שבה השרשור חוזר.
כדי להשיג זאת, עליך להתחיל את השרשורים הרלוונטיים עם מְנוּתָק סטָטוּס. כאשר פותחים שרשור, לנתק ניתן להגדיר את הסטטוס באמצעות ערכי תכונה שרשור או באמצעות ה- pthread_detach פוּנקצִיָה:
intpthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
intpthread_detach(pthread_t פְּתִיל);
הנה דוגמה לשימוש ב-pthread_join(). החלף את הפונקציה הראשית בתוכנית הראשונה בפעולות הבאות:
intרָאשִׁי(בָּטֵל)
{
pthread_t th1, th2;
pthread_create(&th1, ריק, עובד, "X");
pthread_create(&th2, ריק, עובד, "Y");
לִישׁוֹן(5);
printf("יציאה מהתוכנית הראשית\n");
pthread_join (th1, ריק);
pthread_join (th2, ריק);
לַחֲזוֹר0;
}
כאשר אתה מקמפל ומפעיל את התוכנית, הפלט שלך יהיה:
היי מהשרשור Y
היי מהשרשור X
היי מהשרשור Y
...
היי מהשרשור Y
יציאה מהתוכנית הראשית
היי מהשרשור X
...
היי מהשרשור X
שרשור X הסתיים!
היי מהשרשור Y
שרשור Y הסתיים!
סיום שרשור
אתה יכול לבטל שרשור עם קריאה ל-pthread_cancel, להעביר את המתאים pthread_t תְעוּדַת זֶהוּת:
intpthread_cancel(pthread_t פְּתִיל);
אתה יכול לראות את זה בפעולה בקוד הבא. שוב, רק את רָאשִׁי הפונקציה שונה:
intרָאשִׁי(בָּטֵל)
{
pthread_t th1, th2;
pthread_create(&th1, ריק, עובד, "X");
pthread_create(&th2, ריק, עובד, "Y");
לִישׁוֹן(1);
printf("> מבטל שרשור Y!!\n");
pthread_cancel (th2);
אתה ישן(100000);
printf("> מבטל שרשור X!\n");
pthread_cancel (th1);
printf("יציאה מהתוכנית הראשית\n");
לַחֲזוֹר0;
}
למה נוצרים חוטים?
מערכות הפעלה תמיד מנסות להריץ שרשורים על מעבד אחד או יותר, בין אם מתוך רשימה שנוצרה בעצמך או מרשימת שרשורים שנוצרה על ידי המשתמש. שרשורים מסוימים אינם יכולים לפעול מכיוון שהם ממתינים לאות קלט/פלט מהחומרה. ייתכן שהם גם ממתינים מרצונם, מחכים לתגובה משרשור אחר, או שרשור אחר חוסם אותם.
אתה יכול להתאים את המשאבים שאתה מקצה לשרשורים שאתה יוצר באמצעות pthread. זו יכולה להיות מדיניות תזמון מותאמת אישית, או שתוכל לבחור באלגוריתמי תזמון כגון FIFO או Round-robin אם תרצה בכך.