יום שבת, 15 בנובמבר 2014

ההארה של ה-MainThread חלק 1

 “Understanding more about the architecture and what’s actually going on in the platform will help you write better applications” (Chet Haase and Romain Guy - Tech lead of the Android UI toolkit)

על מנת לענות על החידה מהכתבה הראשונה, נחתור לביאור ה-Main Thread ולהבנת הפעולות אשר יביאו אותנו ליצירת ה-Main Thread.
נניח והחלטנו לבנות אפליקציית לוח-שנה, ומכיוון שאין אנו רוצים להטמע בין שאר אפליקציות לוח-השנה הקיימות החלטנו להוסיף פיצ׳ר חדש וייחודי לאפליקציה שלנו - התאריך אצלנו יוצג בצורה אופטימלית לכל שפה. כך למשל נבקש להראות למשתמש מארה״ב תאריך בצורה M/DD/YY ולמשתמש מצרפת DD/MM/YY, עוד על אפשרויות לוקליזציה של תאריכים ניתן למצוא כאן.
לשם השמשת הפיצ׳ר, כתבנו את הקוד הבא -


/**
 * Gets a date instance and return its date formatted to the user's locale.
 * @param date - The date instance to format
 * @return the date in a short and localized format
 */ 
public String formatDate(Date date) {
    DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT);
    return formatter.format(date);  
}
פונקציה אשר מקבלת אובייקט של תאריך ומחזירה את התאריך הנוכחי מותאם לשפה של המשתמש.
לאחר שסיימנו לכתוב את האפליקציה והתפננו לשיפורי מהירות, גילינו שהפונקציה הנ״ל רצה המון פעמים, יוצרת הרבה אובייקטים וגורמת לקריאות רבות ל-GC.

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

בהצצה חטופה בקוד המקור של אנדרואיד ניתן למצוא שאכן בכל קריאה נקבל אובייקט חדש -
public static final DateFormat getDateInstance(int style) { 
    checkDateStyle(style);
    return getDateInstance(style, Locale.getDefault());
}

public static final DateFormat getDateInstance(int style, Locale locale) {
    checkDateStyle(style);
    if (locale == null) 
        throw new NullPointerException("locale == null");
    }
    return new SimpleDateFormat(LocaleData.get(locale).getDateFormat(style), locale);
}

מכאן הדרך לפתרון מהירה, נאתחל את המשתנה פעם אחת בלבד -

private DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT); 
 
public String formatDate(Date date) {  
    return formatter.format(date);  
}

סגרנו את הפתרון, בדקנו, קימפלנו כמעט העלינו ואז גילינו שאנחנו מקבלים ערכים מוזרים בקריאות ממספר Threads לפונקציה שלנו.
  
גם כאן הפתרון מהיר ופשוט, נדאג לסינכרון -
private DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT); 
 
public synchronized String formatDate(Date date) {  
    return formatter.format(date);  
}
אך לאחר הפתרון הנ״ל גילינו שהפונקציה שלנו מהווה צוואר בקבוק ואנו לא מעוניינים לחסום אותה. מכאן נבדוק בדוקמנטציה של אנדרואיד ונגלה את המשפט הבא: 
״SimpleDateFormat is not thread-safe. Users should create a separate instance for each thread.״

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

איך זה עובד?
המחלקה עצמה עברה מספר דורות והיא עמוסה באופטימיזציות, להבנה מעמיקה ניתן לקרוא את הבלוג של בוב לי, האיש אשר חתום על הקוד של ThreadLocal באנדרואיד. 
בהפשטה רבה של הקוד: 
נניח שבכל Thread יהיה לנו את ה-HashMap הבא
public HashMap<Object, Object> values;
ו-ThreadLocal בנוי כך

public class ThreadLocal<T extends Object> extends Object {
 
    public T get() {
        return Thread.currentThread().values.get(this);
    }

    public void set(T value) {
        Thread.currentThread().values.put(this, value);
    }
}
אזי לפתירת הבעיה שלנו אנחנו יכולים לגשת כך

public static ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>();
 
public String formatDate(Date date) {  
    if (df.get() == null)
        df.set(DateFormat.getDateInstance(DateFormat.SHORT));
     return df.get().format(date);  
}

בסופו של דבר, קיבלנו קוד נקי מאוד אשר יצור אובייקט בודד של DateFormat לכל Thread המבקש לקרוא לפונקציה שלנו. מלבד פתירת בעיות Thread-safe ניתן להשתמש בו לפתרון בעיות נוספות, למשל ב-Worker Threads אשר כל worker עובד על פעולה בעלת id ייחודי, נוכל לשמור את ה-id ב-ThreadLocal וכך להנגיש אותו לכל מקום בקוד שלנו.

הערה חשובה:
ג׳אווה מעודדת מאוד מחזור Threads ושימוש ב-Thread pools ועל כן יש להזהר בשימוש ב-ThreadLocal, במצב הזה למשל -

final static ThreadLocal<Integer> counter = new ThreadLocal<Integer>();
 
public void fireThreads() { 
    ExecutorService ex = Executors.newFixedThreadPool(2);
    final AtomicBoolean inFirstThread = new AtomicBoolean(true);
    for (int i = 0; i < 4; i++) {
        ex.execute(new Runnable() {
 
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {}
                if (inFirstThread.getAndSet(false))
                    counter.set(5);
                Log.d(TAG, Thread.currentThread().getName() + ", " + counter.get());
            }
        });
    }
    ex.shutdown();
}
ניסינו לתת ל-Thread הראשון בלבד את הערך 5 ב-counter, אך מפני שאנחנו ממחזרים את ה-Threads 
נקבל בלוג שלנו -
pool-1-thread-1, 5
pool-1-thread-2, null
pool-1-thread-1, 5
pool-1-thread-2, null 
אמנם המשתנים ב-ThreadLocal ישמרו כ-WeakReference, אך עם זאת אין צורך להוסיף על הבעיות אשר עשויות לצוץ כאשר ערכים לא רצויים נדחפים לקוד שלנו.
במקרה הנוכחי, על מנת לפתור את הבעיה היינו משנים את הקוד שיראה כך

if (inFirstThread.getAndSet(false))
    counter.set(5);
else
    counter.remove();
ובכך נאפס את הערך ב-counter שלנו. 







4 תגובות:

  1. תודה על הפוסט המצוין.
    אני אמור לכתוב כעת אפליקציית לוח שנה.
    באילו מצבים שכיחים ייתכנו מספר threads שקוראות לפונקציה formatDate של לוח השנה?
    בהנחה שרק משתמש אחד יכול לגשת ללוח השנה שלו.

    השבמחק
    תשובות
    1. היי זלמן,
      את הלוח שנה נתתי בתור דוגמא לאיך משתמשים ב-LocalThread, אבל אם אני צריך לחשוב על מצבים שבו נצטריך formatDate, אז אני מניח שבנוטיפיקציה או ווידג׳ט, אתה תרצה לבכין את הדאטא שתציג לחלוטין ברקע - כולל להוריד מידע שצריך מהשרת, כמו תמונות, וגם את התאריך כבר תכין שם, שהכל יהיה מוכן תוציא נוטיפיקציה או תראה את הווידג׳ט. יכול להיות עוד שימושים לזה אבל זה מאוד תלוי באפליקציה הספציפית שלך.

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

    השבמחק
    תשובות
    1. מכייון שנעלנו את הפונקציה לThread אחד בלבד - synchronized, יכול להווצר צוואר בקבוק שם. נניח ש5 threads ינסו לגשת לפונקציה, הראשון יכנס וארבעת האחרים יחכו שהראשון יסיים. שהראשון יסיים השני יכנס ושלושת האחרים ימשיכו להמתין וכך הלאה עד שכל ה-threads יכנסו לפונקציה. במידה והפונקציה עושה פעולה ארוכה שלוקחת נניח שניה, ה-thread החמישי ימתין 5 שניות לפני שיכנס לבצע את הפונקציה.

      מחק