מודל ה-threading של Binder נועד להקל על קריאות לפונקציות מקומיות, גם אם הקריאות האלה הן לתהליך מרוחק. בפרט, לכל תהליך שמארח צומת חייב להיות מאגר של שרשור אחד או יותר של קשירה כדי לטפל בעסקאות בצמתים שמארחים בתהליך הזה.
עסקאות סינכרוניות ואסינכרוניות
Binder תומך בעסקאות סינכרוניות ואסינכרוניות. בקטעים הבאים מוסבר איך כל סוג של עסקה מתבצע.
עסקאות סינכרוניות
טרנזקציות סינכרוניות נחסמות עד שהן מבוצעות בצומת, ותשובה לגבי הטרנזקציה מתקבלת על ידי הגורם שביצע את הקריאה. באיור הבא מוצג אופן הביצוע של טרנזקציה סינכרונית:
איור 1. עסקה סינכרונית.
כדי לבצע טרנזקציה סינכרונית, binder מבצע את הפעולות הבאות:
- הת'רדים במאגר הת'רדים של היעד (T2 ו-T3) קוראים לדרייבר של ליבת המערכת כדי להמתין לעבודה נכנסת.
- הליבה מקבלת טרנזקציה חדשה ומפעילה Thread (T2) בתהליך היעד כדי לטפל בטרנזקציה.
- השרשור שמתקשר (T1) נחסם וממתין לתשובה.
- תהליך היעד מבצע את הטרנזקציה ומחזיר תשובה.
- ה-thread בתהליך היעד (T2) קורא בחזרה לדרייבר של הליבה כדי להמתין לעבודה חדשה.
עסקאות אסינכרוניות
עסקאות אסינכרוניות לא חוסמות עד שהן מסתיימות. השרשור שקורא להן מפסיק להיחסם ברגע שהעסקה מועברת לליבה. באיור הבא מוצג אופן הביצוע של עסקה אסינכרונית:
איור 2. עסקה אסינכרונית.
- הת'רדים במאגר הת'רדים של היעד (T2 ו-T3) קוראים לדרייבר של ליבת המערכת כדי להמתין לעבודה נכנסת.
- הליבה מקבלת טרנזקציה חדשה ומפעילה Thread (T2) בתהליך היעד כדי לטפל בטרנזקציה.
- ההרצה של השרשור של השיחה (T1) נמשכת.
- תהליך היעד מבצע את הטרנזקציה ומחזיר תשובה.
- ה-thread בתהליך היעד (T2) קורא בחזרה לדרייבר של הליבה כדי להמתין לעבודה חדשה.
זיהוי פונקציה סינכרונית או אסינכרונית
פונקציות שמסומנות ב-oneway בקובץ AIDL הן אסינכרוניות. לדוגמה:
oneway void someCall();
אם פונקציה לא מסומנת כ-oneway, היא פונקציה סינכרונית, גם אם היא מחזירה void.
סריאליזציה של טרנזקציות אסינכרוניות
Binder מבצע סריאליזציה של עסקאות אסינכרוניות מכל צומת יחיד. באיור הבא אפשר לראות איך binder מבצע סריאליזציה של טרנזקציות אסינכרוניות:
איור 3. סריאליזציה של טרנזקציות אסינכרוניות.
- השרשורים במאגר השרשורים של היעד (B1 ו-B2) שולחים קריאה למנהל ההתקן של ליבת המערכת כדי להמתין לעבודה נכנסת.
- שתי טרנזקציות (T1 ו-T2) באותו צומת (N1) נשלחות לליבה.
- הליבה מקבלת עסקאות חדשות, ומכיוון שהן מגיעות מאותו צומת (N1), היא מבצעת סריאליזציה שלהן.
- טרנזקציה אחרת בצומת אחר (N2) נשלחת לליבה.
- הליבה מקבלת את הטרנזקציה השלישית ומעירה Thread (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 מריץ את A.
באיור הבא מוצג ביצוע של שרשור אם הצומת שמטמיע את bar() נמצא בתהליך אחר:
איור 5. עסקאות מקוננות בתהליכים שונים.
- בקשות בשרשור A1 פועלות
foo(). - כחלק מהבקשה הזו, השרשור B1 מופעל
bar()שמופעל בשרשור אחר C1.
באיור הבא מוצג אופן השימוש החוזר בשרשור באותו תהליך בכל מקום בשרשרת העסקאות:
איור 6. עסקאות מקוננות שמשתמשות מחדש בשרשור.
- תהליך א' שולח קריאה לתהליך ב'.
- תהליך ב' שולח קריאה לתהליך ג'.
- תהליך ג' מבצע קריאה חזרה לתהליך א', והליבה משתמשת מחדש ב-Thread א1 בתהליך א', שהוא חלק משרשרת הטרנזקציות.
בעסקאות אסינכרוניות, אין משמעות לקינון. הלקוח לא מחכה לתוצאה של עסקה אסינכרונית, ולכן אין קינון. אם המטפל בעסקה אסינכרונית מבצע קריאה לתהליך שהנפיק את העסקה האסינכרונית הזו, אפשר לטפל בעסקה הזו בכל שרשור פנוי בתהליך הזה.
הימנעות ממבוי סתום
בתמונה הבאה מוצגת דוגמה נפוצה למצב של קיפאון:
איור 7. קיפאון נפוץ.
- תהליך א' לוקח את mutex MA ומבצע קריאת binder (T1) לתהליך ב' שמנסה גם הוא לקחת את mutex MB.
- במקביל, תהליך B מקבל את mutex MB ומבצע קריאת Binder (T2) לתהליך A, שמנסה לקבל את mutex MA.
אם יש חפיפה בין העסקאות האלה, כל עסקה עלולה לתפוס mutex בתהליך שלה בזמן ההמתנה לשחרור mutex בתהליך השני, וכתוצאה מכך להתרחש deadlock.
כדי להימנע מקיפאון בזמן השימוש ב-binder, אל תחזיקו אף נעילה כשמבצעים קריאה ל-binder.
נעילה של כללי הזמנה וקיפאון
בסביבת ביצוע אחת, לרוב נמנע מצב של קיפאון באמצעות כלל של סדר נעילה. עם זאת, כשמבצעים קריאות בין תהליכים ובין בסיסי קוד, במיוחד כשמבצעים עדכונים לקוד, אי אפשר לתחזק ולתאם כלל סדר.
Mutex יחיד וקיפאון
בטרנזקציות מקוננות, תהליך ב' יכול להתקשר ישירות בחזרה לאותו Thread בתהליך א' שמחזיק ב-Mutex. לכן, בגלל רקורסיה לא צפויה, עדיין יכול להיות שתיווצר חסימה הדדית עם mutex יחיד.
שיחות אסינכרוניות וקיפאון
למרות שקריאות אסינכרוניות של binder לא חוסמות עד שהן מסתיימות, כדאי להימנע גם מהחזקת נעילה עבור קריאות אסינכרוניות. אם אתם מחזיקים נעילה, יכול להיות שתיתקלו בבעיות נעילה אם שיחה חד-כיוונית תשתנה בטעות לשיחה סינכרונית.
שרשור Binder יחיד וקיפאונות
מודל העסקאות של Binder מאפשר כניסה חוזרת, כך שגם אם לתהליך יש שרשור Binder יחיד, עדיין צריך נעילה. לדוגמה, נניח שאתם מבצעים איטרציה ברשימה בתהליך A עם שרשור יחיד. לכל פריט ברשימה, מבצעים עסקה של העברת כרטיס. אם ההטמעה של הפונקציה שאתם קוראים לה יוצרת טרנזקציה חדשה של binder לצומת שמארח בתהליך א', הטרנזקציה הזו מטופלת באותו השרשור שביצע איטרציה ברשימה. אם ההטמעה של הטרנזקציה הזו משנה את אותה הרשימה, יכול להיות שתיתקלו בבעיות אם תמשיכו לבצע איטרציות על הרשימה בהמשך.
הגדרת הגודל של מאגר השרשורים
כשבשירות יש כמה לקוחות, הוספה של עוד שרשורים למאגר השרשורים יכולה לצמצם את המחלוקת ולטפל ביותר קריאות במקביל. אחרי שתטפלו בבעיות של פעולות מקבילות בצורה נכונה, תוכלו להוסיף עוד שרשורים. בעיה שיכולה להיגרם כתוצאה מהוספה של עוד שרשורים, כך שחלק מהשרשורים לא ישמשו במהלך עומסי עבודה נמוכים.
השרשורים נוצרים לפי דרישה עד למקסימום שהוגדר. אחרי שנוצר שרשור binder, הוא נשאר פעיל עד שהתהליך שמארח אותו מסתיים.
בספריית libbinder, ברירת המחדל היא 15 שרשורים. כדי לשנות את הערך הזה, משתמשים ב-setThreadPoolMaxThreadCount:
using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);