283. Using Lazy to achieve warm initialization

[נכתב ע”י שני אלחרר]

אהלן.

לאחרונה שאל אותי מישהו על בעיה שעלתה לו בתוכנה שכתב : יש לו יותר מThread אחד בתוכנה שמשתמש במשתנים שלוקח הרבה זמן ליצור אותם (מדובר ביותר ממשתנה אחד), כאשר הוא מעוניין בזמן עליה נמוך ובביצועים גבוהים.

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

המשתנים (הפלט) של האתחול הם כאלו:

  1. קונפיגורציה של האפליקציה
  2. חיבור לDB (התצורה של החיבור נקבעת לפי הפלט הראשון)
  3. פלט של שאילתא שמבוצעת מול הDB

היו לו שלוש פונקציות שעושות את מה שהוא רצה כך (לא אכנס לפרטי מימוש כיוון שזה כ”כ לא חשוב) :

1
2
3
IConfiguration GetConfiguration();
IDbConnection CreateDbConnection(IConfiguration configuration);
IEnumerable<User> GetUsers(IDbConnection dbConnection);

המימוש ההתחלתי של מנגנון הסנכרון היה ManualResetEvent שנקבע כאשר כל האתחול הסתיים בThread, ושאר הThreadים חיכו לEvent.

בטיפ’ מספר 129 זלינגר מסביר על המחלקה Lazy שקיימת ב.net 4 (ואף בDLLים שאפשר למצוא ברשת, המימוש פשוט), מה שזלינגר לא סיפר לכם זה שLazy תומך בThread Safety מהקופסה, כלומר – שהאתחול יתבצע פעם אחת בלבד, ואם Thread אחר ינסה לגשת למשתנה בזמן האתחול, הוא יחכה עד שהאתחול יסתיים.

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

ניצור שני Fieldים מהסוג Lazy, כאשר לכל אחד הפרמטר הגנרי יהיה לפי הפלט של האתחול :

1
2
3
private Lazy<IConfiguration> mConfiguration;
private Lazy<IDbConnection> mDbConnection;
private Lazy<IEnumerable<User>> mUsers;

בC’tor של מחלקת האתחול, נאתחל אותם :

1
2
3
mConfiguration = new Lazy<IConfiguration>(GetConfiguration);
mDbConnection = new Lazy<IDbConnection>(() => CreateDbConnection(mConfiguration.Value));
mUsers = new Lazy<IEnumerable<User>>(() => GetUsers(mDbConnection.Value));

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

כעת, נוכל לפתוח Thread שמביא את הLazy כדי ליצור את החיבור ברקע (ע"מ שזמן העליה יתקצר במקרה ונרצה להשתמש במשתנים הללו) :

1
ThreadPool.QueueUserWorkItem(x => { var nothing = mUsers.Value; });

השתלשלות העניינים היא כזו :

  1. הLazy של המשתמשים יריץ את הפונקציה של האתחול שלו
  2. הפונקציה של האתחול תגש לLazy של החיבור לDB
  3. הLazy של החיבור לDB יריץ את הפונקציה של האתחול שלו
  4. הפונקציה של האתחול תגש לLazy של הקונפיגורציה
  5. הLazy של הקונפיגורציה תריץ את הפונקציה של האתחול שלו
  6. הקונפיגורציה תובא
  7. החיבור לDB יושג
  8. המשתמשים ייובאו מהDB

בגלל שהמימוש של Lazy הוא כמו של Singleton – (Double null check), אבל פחות סטאטי, הוא מבטיח לנו שהפונקציה שאנחנו נותנים לו בC’tor תורץ רק פעם אחת. בפועל הוא משתמש בנעילה כדי שThreadים אחרים לא יוכלו לגשת לערך בזמן שהוא מאתחל אותו. ולכן הוא מאפשר לנו את ההתנהגות הרצויה במקרה הזה.

אם בשלב כלשהו אנחנו מנסים לגשת לאחד המשתנים והוא לא אותחל עדיין, אז אנחנו נחכה עד שהוא יאותחל (בין הThread שמבצע את העבודה הוא הThread שלנו או Thread אחר).

המשך יום עצל טוב

שתף