ניהול שרשורים

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

עסקאות סינכרוניות ואסינכרוניות

‫Binder תומך בעסקאות סינכרוניות ואסינכרוניות. בקטעים הבאים מוסבר איך כל סוג של עסקה מתבצע.

עסקאות סינכרוניות

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

עסקה סינכרונית.

איור 1. עסקה סינכרונית.

כדי לבצע טרנזקציה סינכרונית, binder מבצע את הפעולות הבאות:

  1. השרשורים במאגר השרשורים של היעד (T2 ו-T3) קוראים לדרייבר של ליבת מערכת ההפעלה כדי להמתין לעבודה נכנסת.
  2. הליבה מקבלת טרנזקציה חדשה ומפעילה Thread ‏ (T2) בתהליך היעד כדי לטפל בטרנזקציה.
  3. השרשור שמתבצעת בו השיחה (T1) נחסם וממתין לתשובה.
  4. תהליך היעד מבצע את הטרנזקציה ומחזיר תשובה.
  5. ה-thread בתהליך היעד (T2) שולח קריאה חוזרת לדרייבר של הליבה כדי להמתין לעבודה חדשה.

עסקאות אסינכרוניות

עסקאות אסינכרוניות לא חוסמות את ההשלמה שלהן. השרשור שקורא להן מפסיק להיות חסום ברגע שהעסקה מועברת לליבת המערכת. באיור הבא מוצג אופן הביצוע של עסקה אסינכרונית:

עסקה אסינכרונית.

איור 2. עסקה אסינכרונית.

  1. השרשורים במאגר השרשורים של היעד (T2 ו-T3) קוראים לדרייבר של ליבת מערכת ההפעלה כדי להמתין לעבודה נכנסת.
  2. הליבה מקבלת טרנזקציה חדשה ומפעילה Thread ‏ (T2) בתהליך היעד כדי לטפל בטרנזקציה.
  3. השרשור של השיחה (T1) ממשיך לפעול.
  4. תהליך היעד מבצע את הטרנזקציה ומחזיר תשובה.
  5. ה-thread בתהליך היעד (T2) שולח קריאה חוזרת לדרייבר של הליבה כדי להמתין לעבודה חדשה.

זיהוי פונקציה סינכרונית או אסינכרונית

פונקציות שמסומנות ב-oneway בקובץ AIDL הן אסינכרוניות. לדוגמה:

oneway void someCall();

אם פונקציה לא מסומנת כ-oneway, היא פונקציה סינכרונית, גם אם היא מחזירה void.

סידור פרקים של עסקאות אסינכרוניות

ה-Binder מבצע סריאליזציה של עסקאות אסינכרוניות מכל צומת יחיד. באיור הבא מוצג אופן הסדר של טרנזקציות אסינכרוניות ב-Binder:

סריאליזציה של טרנזקציות אסינכרוניות.

איור 3. סריאליזציה של טרנזקציות אסינכרוניות.

  1. השרשורים במאגר השרשורים של היעד (B1 ו-B2) שולחים קריאה למנהל ההתקן של ליבת המערכת כדי להמתין לעבודה נכנסת.
  2. שתי טרנזקציות (T1 ו-T2) באותו צומת (N1) נשלחות לליבה.
  3. הליבה מקבלת עסקאות חדשות, ומכיוון שהן מגיעות מאותו צומת (N1), היא מבצעת סריאליזציה שלהן.
  4. טרנזקציה אחרת בצומת אחר (N2) נשלחת לליבה.
  5. הליבה מקבלת את הטרנזקציה השלישית ומעירה Thread ‏ (B2) בתהליך היעד כדי לטפל בטרנזקציה.
  6. תהליכי היעד מבצעים כל טרנזקציה ומחזירים תשובה.

עסקאות מקוננות

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

def outer_function(x):
    def inner_function(y):
        def inner_inner_function(z):

אם אלה קריאות מקומיות, הן מבוצעות באותו השרשור. באופן ספציפי, אם המתקשר של inner_function הוא גם התהליך שמארח את הצומת שמיישם את inner_inner_function, הקריאה ל-inner_inner_function מבוצעת באותו השרשור.

באיור הבא מוצג אופן הטיפול של binder בעסקאות מקוננות:

עסקאות מקוננות.

איור 4. עסקאות מקוננות.

  1. בקשות בשרשור A1 פועלות foo().
  2. כחלק מהבקשה הזו, השרשור B1 מריץ את bar(), והשרשור A מריץ את אותו השרשור A1.

האיור הבא מציג את ביצוע השרשור אם הצומת שמטמיע את bar() נמצא בתהליך אחר:

עסקאות מקוננות בתהליכים שונים.

איור 5. עסקאות מקוננות בתהליכים שונים.

  1. בקשות בשרשור A1 פועלות foo().
  2. כחלק מהבקשה הזו, השרשור B1 מפעיל את bar() שפועל בשרשור אחר C1.

באיור הבא מוצג אופן השימוש החוזר בשרשור באותו תהליך בכל מקום בשרשרת העסקאות:

עסקאות מקוננות שמשתמשות מחדש ב-thread.

איור 6. עסקאות מקוננות שמשתמשות מחדש ב-thread.

  1. תהליך א' שולח קריאה לתהליך ב'.
  2. תהליך ב' שולח קריאה לתהליך ג'.
  3. תהליך ג' שולח קריאה חזרה לתהליך א', והליבה משתמשת מחדש ב-Thread א1 בתהליך א', שהוא חלק משרשרת הטרנזקציות.

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

הימנעות ממבוי סתום

בתמונה הבאה מוצגת דוגמה נפוצה למצב של קיפאון:

קיפאון נפוץ.

איור 7. קיפאון נפוץ.

  1. תהליך א' מקבל את mutex MA ושולח קריאת binder‏ (T1) לתהליך ב', שמנסה גם לקבל את mutex MB.
  2. במקביל, תהליך B לוקח את mutex MB ומבצע קריאת Binder ‏ (T2) לתהליך A, שמנסה לקחת את mutex MA.

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

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

נעילה של כללי הזמנה ומבוי סתום

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

‫Mutex יחיד וקיפאון

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

שיחות סינכרוניות וקיפאון

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

Single binder thread and deadlocks

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

הגדרת גודל מאגר השרשורים

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

השרשורים נוצרים לפי דרישה עד שמגיעים למקסימום שהוגדר. אחרי שנוצר שרשור binder, הוא נשאר פעיל עד שהתהליך שמארח אותו מסתיים.

בספריית libbinder, ברירת המחדל היא 15 שרשורים. כדי לשנות את הערך הזה, משתמשים ב-setThreadPoolMaxThreadCount:

using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);