מודל השרשור של Binder נועד להקל על קריאות לפונקציות מקומיות, גם אם הקריאות האלה הן לתהליך מרוחק. בפרט, לכל תהליך שמארח צומת חייב להיות מאגר של שרשור אחד או יותר של קבצים מצורפים כדי לטפל בעסקאות בצמתים שמארחים בתהליך הזה.
עסקאות סינכרוניות ואסינכרוניות
Binder תומך בעסקאות סינכרוניות ואסינכרוניות. בקטעים הבאים מוסבר איך כל סוג של עסקה מתבצע.
עסקאות סינכרוניות
עסקאות סינכרוניות נחסמות עד שהן מבוצעות בצומת, ותשובה על העסקה מתקבלת על ידי המתקשר. באיור הבא מוצג אופן הביצוע של טרנזקציה סינכרונית:
איור 1. עסקה סינכרונית.
כדי לבצע טרנזקציה סינכרונית, binder מבצע את הפעולות הבאות:
- השרשורים במאגר השרשורים של היעד (T2 ו-T3) קוראים לדרייבר של ליבת מערכת ההפעלה כדי להמתין לעבודה נכנסת.
- הליבה מקבלת טרנזקציה חדשה ומעירה את השרשור (T2) בתהליך היעד כדי לטפל בטרנזקציה.
- השרשור שמתבצעת בו השיחה (T1) נחסם וממתין לתשובה.
- תהליך היעד מבצע את העסקה ומחזיר תשובה.
- השרשור בתהליך היעד (T2) מבצע קריאה חוזרת למנהל ההתקן של ליבת המערכת כדי להמתין לעבודה חדשה.
עסקאות אסינכרוניות
עסקאות אסינכרוניות לא חוסמות את ההשלמה שלהן. השרשור שקורא להן מפסיק להיות חסום ברגע שהעסקה מועברת לליבת המערכת. באיור הבא מוצג אופן הביצוע של עסקה אסינכרונית:
איור 2. עסקה אסינכרונית.
- השרשורים במאגר השרשורים של היעד (T2 ו-T3) קוראים לדרייבר של ליבת מערכת ההפעלה כדי להמתין לעבודה נכנסת.
- הליבה מקבלת טרנזקציה חדשה ומעירה את השרשור (T2) בתהליך היעד כדי לטפל בטרנזקציה.
- השרשור של השיחה (T1) ממשיך לפעול.
- תהליך היעד מבצע את העסקה ומחזיר תשובה.
- השרשור בתהליך היעד (T2) מבצע קריאה חוזרת למנהל ההתקן של ליבת המערכת כדי להמתין לעבודה חדשה.
זיהוי פונקציה סינכרונית או אסינכרונית
פונקציות שמסומנות ב-oneway
בקובץ AIDL הן אסינכרוניות. לדוגמה:
oneway void someCall();
אם פונקציה לא מסומנת כ-oneway
, היא פונקציה סינכרונית, גם אם הפונקציה מחזירה void
.
סידור פרקים של עסקאות אסינכרוניות
Binder מבצע סריאליזציה של עסקאות אסינכרוניות מכל צומת יחיד. באיור הבא מוצג אופן הסדרת הפעולות של טרנזקציות אסינכרוניות ב-Binder:
איור 3. סריאליזציה של טרנזקציות אסינכרוניות.
- השרשורים במאגר השרשורים של היעד (B1 ו-B2) קוראים לדרייבר של ליבת המערכת כדי להמתין לעבודה נכנסת.
- שתי עסקאות (T1 ו-T2) באותו צומת (N1) נשלחות לליבה.
- הליבה מקבלת עסקאות חדשות, ומכיוון שהן מגיעות מאותו צומת (N1), היא מבצעת סדרת פעולות עליהן.
- עסקה נוספת בצומת אחר (N2) נשלחת לליבה.
- הליבה מקבלת את העסקה השלישית ומפעילה שרשור (B2) בתהליך היעד כדי לטפל בעסקה.
- תהליכי היעד מבצעים כל עסקה ומחזירים תשובה.
עסקאות מקוננות
אפשר להטמיע עסקאות סינכרוניות זו בתוך זו. תהליך שמטפל בעסקה יכול להנפיק עסקה חדשה. העסקה המקוננת יכולה להיות תהליך אחר, או אותו תהליך שממנו קיבלתם את העסקה הנוכחית. ההתנהגות הזו דומה לקריאות לפונקציות מקומיות. לדוגמה, נניח שיש לכם פונקציה עם פונקציות מוטמעות:
def outer_function(x):
def inner_function(y):
def inner_inner_function(z):
אם אלה קריאות מקומיות, הן מבוצעות באותו השרשור.
באופן ספציפי, אם המתקשר של inner_function
הוא גם התהליך שמארח את הצומת שמיישם את inner_inner_function
, הקריאה ל-inner_inner_function
מבוצעת באותו השרשור.
באיור הבא מוצג אופן הטיפול של binder בעסקאות מקוננות:
איור 4. עסקאות מקוננות.
- בקשות בשרשור A1 פועלות
foo()
. - כחלק מהבקשה הזו, השרשור B1 מפעיל את
bar()
, שמופעל על אותו השרשור A1.
האיור הבא מציג את ביצוע השרשור אם הצומת שמיישם את bar()
נמצא בתהליך אחר:
איור 5. עסקאות מקוננות בתהליכים שונים.
- בקשות בשרשור A1 פועלות
foo()
. - כחלק מהבקשה הזו, השרשור B1 מפעיל את
bar()
שפועל בשרשור אחר C1.
באיור הבא מוצג אופן השימוש החוזר בשרשור באותו תהליך בכל מקום בשרשרת העסקאות:
איור 6. עסקאות מקוננות שמשתמשות מחדש בשרשור.
- עיבוד שיחות בתהליך א' בתהליך ב'.
- עיבוד שיחות מסוג B בתהליך C.
- תהליך ג' מבצע קריאה חזרה לתהליך א', והליבה משתמשת מחדש בשרשור א1 בתהליך א' שהוא חלק משרשרת העסקאות.
בעסקאות אסינכרוניות, אין משמעות לקינון. הלקוח לא מחכה לתוצאה של עסקה אסינכרונית, ולכן אין קינון. אם המטפל בעסקה אסינכרונית מבצע קריאה לתהליך שהנפיק את העסקה האסינכרונית הזו, אפשר לטפל בעסקה הזו בכל שרשור פנוי בתהליך הזה.
הימנעות ממבוי סתום
בתמונה הבאה מוצגת דוגמה נפוצה למצב של קיפאון:
איור 7. קיפאון נפוץ.
- תהליך א' מקבל את mutex MA ושולח קריאת binder (T1) לתהליך ב' שמנסה גם לקבל את mutex MB.
- במקביל, תהליך B מקבל את mutex MB ומבצע קריאת Binder (T2) לתהליך A, שמנסה לקבל את mutex MA.
אם יש חפיפה בין העסקאות האלה, כל עסקה עלולה לתפוס mutex בתהליך שלה בזמן ההמתנה לשחרור mutex בתהליך אחר, וכתוצאה מכך להתרחש deadlock.
כדי להימנע מקיפאון בזמן השימוש ב-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);