Liran Chen's Blog

.Net Internals, Development, Multithreading - and More!

June 2009 - Posts

Advanced Debugging Using SOS

WinDbg הוא כלי דיבאגינג בעל יכולות מתקדמות המופץ חינמית על ידי מיקרוסופט. במקור, הוא יועד לעבודות דיבאג של תוכניות Native, אבל, כשאנחנו מצרפים לו את חבילת ההרחבה SOS (או: Son of Strike) אנחנו מקבלים תמיכה גם כן בעבודה מול תוכניות Managed, כל שבפועל, בעזרת SOS אנחנו יכולים למעשה גם לדבג את ה-CLR בעצמו.
איך שאני רואה את זה, השימוש ב-SOS מתחלק ל-2 חלקים: עבודה, ומחקר. הכוונה היא שבדרך כלל, לא נצטרך להשתמש באותן יכולות (שנדבר עליהן עוד רגע) שהוא חושף לנו. בדרך כלל המקרים בהם הוא יוכל לעזור לנו, הם בניתוח Crash Dumps, פתרון Deadlock'ים, ובעיות זכרון. מעבר לנקודות האלה, עבודה עם SOS יכולה גם לעזור לנו לקבל הבנה טובה ומעמיקה יותר על "איך דברים באמת עובדים" בכל הקשור ל-CLR. למשל, גם אם קראנו אינספור מאמרים על MethodTables בדוט-נט, איך הן בנויות, ואיך הן עובדות, עדיין אפשר לקבל איזשהיא תמורה "מעבר" ברגע שאנחנו באמת רואים איך הכל קורה "תכל'ס" בתוכנית שכתבנו בעצמנו.

כדי לדבג בעזרת SOS, אנחנו יכולים לבחור להשתמש ב-WinDbg או ב-Visual Studio. בשניהם, עלינו לטעון את רכיב ה-SOS בנפרד, ולאחר מכן, העבודה בשניהם זהה לחלוטין. בפוסט הזה אני אשתמש ב-WinDbg לצורך ההדגמה, אך שוב, העבודה מול SOS היא זהה גם כשמדובר ב-VS.
עבור משתמשי VS, שימו לב שכדי לדבג בעזרת SOS, תצטרכו לאפשר Unmanaged Debugging בפרוייקט שלכם. וכדי לקרוא לפקודות המתאימות, תצטרכו לעבוד דרך חלון ה-Immediate. גם יכולים להיות מספר הבדלים סמנטים לא משמעותים כשטוענים את SOS, אך שוב... לא מדובר בשום דבר משמעותי.

אז כדי להתחיל, תחילה נצטרך להוריד את WinDbg וההרחבה של SOS מכאן.
בהדגמה, ננתח את התוכנית הבאה:

interface IFoo { }

class Foo : IFoo { }

 

class Program

{

    static void Main()

    {

        Foo a = new Foo();

        IFoo b = a;

 

        while (true) { }

    }

}


הצעד הראשון לאחר שהפעלנו את WinDbg, הוא לבחור את כתובת ה-Symbol'ים של Windows שנשתמש בהם. מכיוון שחבילת ה-Symbol'ים לא נמצאת על המחשב הממוצע, יש לנו 2 אפשרויות לבחור מהן. או שנפנה את WinDbg לכתובת מיוחדת של מיקרוסופט, שדרכה הוא יוכל להוריד אך ורק את ה-Symbol'ים שהוא צריך בזמן העבודה, או שנוריד מבעוד מועד את החבילה המלאה (כמה מאות מגה-בייטים) ונשתמש בה במקום זה. לצורך הדוגמה הזאת, אני אפנה את WinDbg לשרתים של מיקרוסופט. הסינטקס הוא נראה כך:

SRV*_LOCAL_FOLDER_*http://msdl.microsoft.com/download/symbols

אחרי זה, אנחנו כמעט ומוכנים. כעת, נדאג שהתוכנית שלנו רצה ברקע, ונבחר לדבג אותה דרך חלון ה-Attach to Process.
לאחר הצירוף, נקבל את החלון הראשי:



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

.loadby sos mscorwks
 
// For VS users, you could use the following command:
.load sos
 
זהו זה. אנחנו מוכנים (טוב, בערך).
מכאן, אנחנו יכולים להתחיל להשתמש בפקודות השונות ש-SOS יודע לקבל. כדי לקבל תמונה כללית על האפשרויות העומדות בפנינו, תמיד אפשר לחזור ולהקליד help! כדי לקבל את רשימת הפקודות הבסיסית.
אז עכשיו נחזור לרגע לתוכנית הבדיקה שלנו. בואו נאמר שאנחנו רוצים לנתח את מופע האובייקט הבודד שיצרנו (Foo). כדי לעשות זאת, קודם נצטרך לקבל את כתובת הזכרון שלו, ולשם כך נשתמש בפקודה CLRStack -l! שלמעשה תיתן לנו dump של כל המשתנים המוקצים של ה-Managed Stack הנוכחי שלנו. אבל, מה הבעיה? בדרך כלל כשאנחנו מתחילים לדבג עם WinDbg, אנחנו מופנים ל-Unmanaged Thread שלא קשור לתוכנית שלנו. לכן, קריאה ל-CLRStack! במצב כזה תכשל מאחר ואין שום Managed Stack שאפשר לנתח בכלל. על כן, עלינו לעבור ל-Managed Thread אחר. אז מה שנעשה, זה מה שקודם כל נרצה, זה לקבל את רשימת ה-Thread'ים הקיימים בתוכנית שלנו. לשם כך נשתמש בפקודה Threads!. הפלט עבור התוכנית שלנו נראה כך:

0:003> !Threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
                                      PreEmptive   GC Alloc           Lock
       ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
   0    1  33c 003b90b8   200a020 Enabled  01c35618:01c35fe8 003b4da0     0 MTA
   2    2 15e8 003c8bb8      b220 Enabled  00000000:00000000 003b4da0     0 MTA (Finalizer)


במקרה הזה, הת'רד שמעניין אותנו הוא הת'רד הראשי, שנמצא תחת מספר 0. שימו לב שהעמודה שמעניינת אותנו בהקשר של בחירת ת'רדים ב-WinDbg היא השמאלית ביותר. היא מסמלת עבורנו את ה-ID שדרכו נוכל לזהות ת'רדים כשנבקש להחליף ביניהם.
אז כדי לעבור לת'רד המתאים נשתמש בפקודה THREAD_ID s~. במקרה שלנו, אותו Id יהיה 0 מאחר והוא מזהה את הת'רד הראשי.
אחרי שעברנו ל-Managed Thread המתאים, נוכל להשתמש ב CLRStack -l! בשביל לקבל את רשימת המשתנים הנמצאת על ה-Stack שלנו.
הפלט הפעם נראה כך:

0:000> !CLRStack -l
OS Thread Id: 0x33c (0)
ESP       EIP    
002af310 003800d8 Program.Main()
    LOCALS:
        0x002af318 = 0x01c33758
        0x002af314 = 0x01c33758
        0x002af31c = 0x00000001

002af544 6d911b4c [GCFrame: 002af544]


והנה, אנחנו רואים שקיימים לנו 3 משתנים על ה-Stack. שני המשתנים הראשונים הם הרפרנסים של Foo, אפשר לשים לב שהם מצביעים לאותו המקום, אותו מופע של Foo (שימו לב שמידע כזה, כמו כתובת זכרון של אובייקט הנמצא על ה-GC Heap לא ניתן לקבל דרך כלים קונבנציונלים אחרים, גם כשמדובר למשל ב-Unsafe Code Blocks). המשתנה השלישי הוא לא אחר מאשר המשתנה שיצרנו תחת לולאת ה-while. כן, מדובר באותו Constant True, שבזמן הריצה זקוק "לאיזשהו" ייצוג בזכרון, לכן נוצר לנו משתנה חדש שקיבל את הערך 1.

כדי להתחיל לנתח את האובייקט שלנו, נשתמש בפקודה do!, ובתור פרמטר נעביר לה את הכתובת של האובייקט.

0:000> !do 0x01c33758
Name: Foo
MethodTable: 001130a4
EEClass: 00111368
Size: 12(0xc) bytes
 (F:\Users\Liran\Documents\Visual Studio 2005\Projects\ConsoleApplication2\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe)
Fields:
None

אפשר לשים לב כאן לעוד פרט מעניין. למרות שהגדרנו את הטיפוס שלנו ללא שום שדות, אנחנו יכולים לראות שהגודל שהוא תופס ב-Heap הוא 12 בתים. הסיבה לכך היא שבחישוב גודל האובייקט, נלקחים גם המצביע ל-MethodTable שנמצא בו, וגם ה-ObjHeader הנמצא לפניו בזכרון (פירוט נוסף בפוסט הזה).

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

0:000> !DumpMT -md 001130a4
EEClass: 00111368
Module: 00112c5c
Name: Foo
mdToken: 02000003  (F:\Users\Liran\Documents\Visual Studio 2005\Projects\ConsoleApplication2\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 1
Slots in VTable: 5
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
6c4d6ab0   6c354944   PreJIT System.Object.ToString()
6c4d6ad0   6c35494c   PreJIT System.Object.Equals(System.Object)
6c4d6b40   6c35497c   PreJIT System.Object.GetHashCode()
6c547540   6c3549a0   PreJIT System.Object.Finalize()
003800f8   0011309c      JIT Foo..ctor()

מכאן, השמיים הם הגבול. אנחנו יכולים להתחיל לנתח כל חלקיק הכי קטן בזכרון. אם זה ה-MethodTable/EEClass/GCDesc/SyncBlocks ... ועוד ועוד. הפוסט הזה באמת קטן מלהכיל את כל האפשרויות העומדות לפנינו בשלב הזה. מה שכן, שעות של הנאה מובטחת לכל המשפחה.

Visual Studio: Using The Incremental Search
הייתי רוצה לנצל את הפוסט הזה בשביל להכיר לכם את ה-Incremental Search המובנה בויזואל סטודיו. מדובר בקונבנצית חיפוש טקסט מעט שונה מהדרך "הסטנדרטית" אליה בדרך כלל רגילים, אבל עם שימוש נכון (ו-5 דקות של התרגלות), תגלו שצורת העבודה הזאת יכולה לשפר פלאים את תהליך הקידוד/חיפוש שלכם.
הדרך הנפוצה לחיפוש טקסט היום היא על ידי הקלקה על Ctrl+F ושימוש בחלון החיפוש שנפתח בשביל לחפש טקסטים. הסיבה שהשיה הזאת כל כך פופולרית היא שבדרך כלל זוהי הדרך היחידה בה היה ניתן לבצע חיפוש בתוכנות אחרות (למרות שבשנים האחרונות המצב דווקא החל להשתנות לטובה). הבעיה בשיטה הזאת היא שהיא מצריכה לא מעט עבודה עם העכבר שסתם מפריעה ולא עוזרת לשום דבר. אז במקום זה, אפשר להשתמש ב- Incremental Search.
בקונבנציה הזאת, הרעיון הוא שלא פותחים שום חלון חדש, במקום זה, אנחנו רק מתחילים להקליד את הטקסט והסמן שלנו (caret) כבר קופץ למקום הבא בו נמצאו רצף התווים שהקלדנו.
כדי להפעיל את החיפוש, נשתמש בקיצור Ctrl+I. לאחר מכן, נוכל להתחיל להקליד את הטקסט אותו אנו מעוניינים לחפש. מיד נוכל להבחין בשורת הסטטוס את הטקסט שהקלדנו עד כה. כל עוד יצאנו ממצב החיפוש (Esc), אנחנו יכולים גם לערוך את טקסט החיפוש ולמחוק גם תווים. ברגע שסיימנו להכניס את כל מילת החיפוש (או חלק ממנה), אנחנו יכולים ללחוץ על F3 כדי לגשת למקומות נוספים בו מילת החיפוש נמצאה (או, F3+Shift בשביל לחפש בכיוון ההפוך).
מהניסיון שלי, שימוש ב-Incremental Search מהיר ונוח בהרבה מאשר חיפוש בעזרת ה-Search Dialog (כאמור, עבור חיפוש בתוך קבצים בודדים).

שיטת החיפוש הזאת רחוקה מלהיות ייחודית לויזואל סטודיו. המקור הוא כאמור בעורכים הטקסטואלים כגון vi בהם אין ממש יותר מדי אופציות אחרות (אם לא מכניסים את grep לתמונה..). אבל מעבר לזה, אנחנו נתקלים בצורת החיפוש הזאת גם ביום יום, אם זה בעזרת יכולת ה-Search as I Type בפיירפוקס, או חיפוש קבצים/תיקיות בתוך ה-Windows Explorer. וגם כאן, בשני המקרים האלה, השימוש בחיפוש הזה עושה את החיים שלנו למעט יותר קלים. אז למה לא לנסות?


Understanding SyncBlocks
בדוט-נט קיים מנגנון סנכרון אובייקטים מובנה. ובין אם אנחנו מודעים לזה או לא, אנחנו משתמשים בו בכל פעם שאנחנו באים לסנכרן קטעי קוד באפליקציה שלנו. המשמעות היא שאנחנו לא צריכים לגשת לאובייקטי סנכרון מובנים של מערכת ההפעלה, בכל פעם שאנחנו רוצים לעשות lock קטן בקוד. מה שמקנה לנו יתרון בביצועים בהשוואה לשימוש באובייקטי סנכרון כגון Mutex או Semaphore (במקרה שלנו, אובייקט הקרנל היחיד בו משתמשים הוא Event).
 
עבור כל אובייקט שקיים אצלנו באפליקציה, קיים גם SyncBlock שמשוייך אליו. אותו אובייקט SyncBlock אחראי על סנכרון הגישות לאותו אובייקט. הוא מנהל למעשה את רשימת ה-Thread'ים שמעוניינים לגשת לאובייקט שלנו. בכל פעם שה-Thread הנוכחי סיים את העבודה שלו ושחרר את הנעילה שלו מהאובייקט, ה-SyncBlock ניגש לרשימת FIFO של Thread'ים שממתינים לנעול את האובייקט, ובוחר את הבא בתור.
מבחינת ניהול זכרון, אין מה לחשוש שעצם השימוש ב-SyncBlocks יגדיל משמעותית את ניצול הזכרון של האפליקציה שלנו. זאת משום שאין באמת יחס של אחד לאחד בין אובייקטים שאנחנו יוצרים, לבין SyncBlock'ים. כלומר, עצם זה שיצרנו אובייקט חדש בקוד, לא אומר שהוקצה עכשיו גם SyncBlock חדש. הצורה בה מנהלים את השימוש ב-SyncBlocks הוא מעט יותר יעיל וחכם מזה. מה שקורה זה שבאזור הזכרון הפרטי ל-Execution Engine מנוהלת טבלה המיוצגת על ידי האובייקט SyncTableEntries. האובייקט הזה למעשה מחזיק Cache שלם של אובייקטים מסוג SyncBlock, שמוכנים לשימוש על ידי האפליקציה שלנו. בפוסט קודם הסברתי שלפני כל אובייקט שלנו קיים מופע של ObjHeader שנמצא ב-Offset שלילי בזכרון. אותו ObjHeader מכיל בין היתר את ה-SyncBlock Index ששייך לאובייקט שלנו. האינדקס הזה למעשה מייצג איזה SyncBlock מתוך טבלת ה-SyncBlock'ים הקיימים משוייך לאובייקט שלנו. וכברירת מחדל, הוא מאותחל ל-0 (כלומר, שום SyncBlock לא משוייך לאובייקט).
השימוש ב-SyncBlocks נחשף למשתמש (לנו), באמצעות המחלקה Monitor. דרך הפונקציות Enter, TryEnter ו-Exit אנחנו יכולים לבקש להחזיק בנעילה, או לשחרר אותה. ברגע שמגיעה בקשה חדשה לנעילה, האובייקט פונה ל-ObjHeader שלו ובודק האם קיים SyncBlock שכבר משוייך אליו (כלומר, האינדקס לא מאופס). במידה וקיים, אז הוא ניגש לאותו SyncBlock וקורא ל-Enter עליו. בתוך הקונטקסט של ה-SyncBlock (שבתוכו עושה שימוש ב-AwareLock), הת'רד החדש נכנס לרשימת ההמתנה, וברגע שהאובייקט ישוחרר מהנעילה הנוכחית, הנעילה תעבור לת'רד הבא שבתור.
במידה ולא קיים SyncBlock שמשוייך אלינו, אז האובייקט יבדוק האם קיים SyncBlock פנוי ב-Cache. במידה ולא קיים אחד כזה, אז יוקצה Cache חדש גדול פי 2 מהקודם, שאליו יועתקו כל הנתונים מהטבלה הקודם. ה-Cache הישן ימחק כבר במעבר הבא של ה-GC.

מעבר להיותו עוגן סנכרון, ה-SyncBlock משמש גם כסוג של "כולבוייניק" (או אם אתם מתעקשים: Kitchen Sink). כאשר ה-ObjHeader בו האובייקט שלנו עושה שימוש כבר לא יכול להחזיק את כל המידע הדרוש לגבי האובייקט שלנו, אז הנתונים האלו יעברו לתוך SyncBlock שמקושר לאובייקט. מה שלמעשה הופך את הגישה אליו למעט יותר איטית מבדרך כלל.


Silence is Golden
אולי אני מעט מוזר בקטע הזה, אבל לא פעם אני מוצא את עצמי מעדיף לעבוד עם פאנל ה-Output במקום ה-Error List כשאני בא לפתור שגיאות קומפילציה. אני חושב שהכל השתרשר מ-VS2003 שלמיטב זכרוני נטה לזייף במיון סדר השגיאות בפאנל, מה שדי הפך את העבודה איתה ללא נוחה בעליל.
הבעיה בעבודה עם פאנל ה-Output היא שמאוד קל לזבל אותו. לאט לאט הפרוייקט יכול לאסוף שלל Post Build Events ושאר פעולות שיגרמו להדפסות לא נגמרות תחת פאנל ה-Output. למעשה, אפילו שלא לצורך פתרון שגיאות קומפילציה, כל ההדפסות הלא נגמרות האלה הופכות את השימוש ב-Output ללא פרקטי בכלל. במקום לקבל מידע שמעניין ורלוונטי אלינו, אנחנו מותקפים בהרים וגבעות של הדפסות לא ברורות שלא אומרות לנו שום דבר מעניין.

למזלנו, אפשר לפתור את הבעיה המטרידה הזאת. כל מה שעלינו לעשות הוא להגדיר מחדש את רמת ה-Verbosity שאנו מעוניינים לקבל מ-MSBuild. כדי לעשות זאת, עלינו לפתוח את חלון ה-Options ומשם לגשת בתפריט: Projects and Solutions-->Build and Run. משם, עלינו רק לבחור בקומבו בוקס המתאים את רמת הפירוט שאנו מעוניינים בה. ברירת המחדל היא Minimal, שלפי דעתי, היא רחוקה מלהיות באמת .. מינימלית. במקום זה, ההעדפה שלי היא להשתמש ב-Quite. כך נקבל הדפסות אך ורק "כשדברים רעים" קורים. למשל שגיאות קומפילציה, הזהרות, שגיאות של Post Build Events... או בקיצור: מה שבאמת מעניין אותנו.




לעיתים קרובות אנחנו נתקלים בבעיה זהה כשמקמפלים פרוייקטים דרך ה-Command Prompt במקום דרך Visual Studio. רק שבמקרה הזה, בגלל שעומס ההודעות למעשה מונע מאיתנו להבין באמת האם הקימפול נכשל או עבר בהצלחה, בדרך כלל עושים את המאמץ הנוסף, ומגלים את אותו סוויץ' קסמים. רק שמשום מה, לא תמיד מקשרים בינו לבין הכאוס שמתחולל אצלנו ב-Output.. מוזר.
Don't Get Lazy
כשמתכנתים מתחיל להתעצל, דברים רעים מתחילים לקרות. בהתחלה כותבים איזה Anoynmous Method, אחר כך מזדרזים ומשרבטים Extension Method ... ובעיקר, הופכים את הקוד לפחות קריא ונוח לתחזוקה.
אחד ה-"Horror'ים" שכדאי להמנע ממנו, הוא השימוש בפונקציות "הנוחיות" השונות שמספקת לנו מחלקה Array. אני מתכוון לכל הפונקציות שמקבלות מצביע לפונקצית עזר כלשהו, שמופעלת על כל איברי המערך. למשל Find, ForEach, TrueForAll... וכדומה.
במבט מהיר, הפונקציות האלה יכולות להראות נוחות ושימושיות. אחרי הכל, חסכנו כתיבה של לולאה! או בהערכה חופשית: חסכנו לעצמנו גג עוד 4 שורות קוד. ומה קיבלנו בתמורה? קוד שבזמן דיבאג יהיה הרבה פחות נוח לדבג. בואו נאמר שיש איזשהיא בעיה ב-Predicate שהעברנו, או שאנחנו סתם מדבגים דרך איזשהו קטע קוד, ואנחנו רוצים לראות איך Find מחפש בתוך המערך שלנו. אבל מה, בגלל שחסכנו 4 שורות קוד, אין לנו בלוק קוד נחמד ונוח של לולאה בה אנחנו יכולים בקלות ובבהירות לעבור איבר-איבר במערך ולראות באמת מה קורה עם כל אחד מהם. במקום זה, אנחנו צריכים להציב breakpoint חדש בתוך פונקציות העזר שהגדרנו, כדי שנוכל לתפוס כל הפעלה נפרדת שלה על כל איבר במערך (ושמן הסתם לא נדע בדיוק באיזו איטרציה מדובר, אבל זה כבר.. מינורי).
בשורה התחתונה, כן. חסכנו 4 שורות קוד ... אבל באיזה מחיר? במחיר של הוספת קושי חסר טעם על עבודת הדיבאג. כך שבסופו של דבר, אנחנו רק מפסידים. במקום זה, עדיף להפסיק להתעצל לרגע, וכן, להוסיף את הכמה שורות האלה עבור הלולאה שלנו. הקוד יהיה אותו קוד בסופו של דבר, אז למה שלא נקל מעט על עצמנו על הדרך? :)
Objects Structure: Under The Hood
אובייקטים בדוט-נט אינם בנויים בתור יחידות בודדת ושלמות שנמצאות על ה-GC Heap, שמכילות את ה-Data Member'ים שהגדרנו להן.
בפועל, מעבר למידע "הנראה לעין" שאנחנו מגדירים כשאנחנו כותבים מחלקה חדשה, מתווספים גם מספר שדות "בלתי נראים" לאובייקט שלנו שנמצאים באזורים שונים בזכרון.

השדה הראשון והבסיסי הוא המצביע ל-MethodTable השייך לטיפוס שלנו. אני מדלג על ההסבר על ה-MethodTable והמידע שהיא מכילה (שימו לב שלא מדובר ב-VTable עצמו, אלא בטיפוס שבין היתר מכיל מצביע אליה), ובמקום זה אתמקד בנקודה אחרת: מיקום המצביע.
העניין הוא כזה, כשאנחנו יוצרים מופע של מחלקה, יש לנו גישה אך ורק ל-Member'ים שהגדרנו. כלומר, שדות "נסתרים" שמתווספים אוטומטית לאובייקט שלנו נמצאים מעבר להישג ידנו. ועם זאת, עדיין חלקם מוסתרים "פחות" ומוסתרים "יותר". אני אסביר למה הכוונה..
מאחר ואנחנו נוגעים כאן בנושאים שנמצאים "בברזלים" של הפלטפורמה, מבט בקוד ה-IL של המחלקה Object למשל לא יעזור לנו מאחר ורוב המידע "המעניין" נסתר מאיתנו שם. במקום זה, נציץ בקוד של SSCLI על מנת לקבל הבנה קצת יותר טובה על מה שקורה מאחורי הקלעים.

קובץ ה-Header של Object נמצא תחת הכתובת clr/src/vm/object.h. כשנפתח אותו, נראה את הקוד הבא:

class Object

{

  protected:

    MethodTable*    m_pMethTab;

    // ... lots of code

}


ממה שאנחנו רואים כאן, אפשר להתרשם שלאובייקט שלנו מתווסף "שדה נסתר" אחד בלבד, שמצביע ל-VTable שמשוייך לטיפוס. אבל בפועל, המצב מעט שונה.
בנוסף ל-VPointer שהתווסף לנו לתוך תחום האובייקט שלנו, מתווסף לנו אוטומטית גם ה-ObjHeader. הסיבה שאנחנו לא רואים כל אזכור לו בהגדרה של מחלקת Object היא שהוא לא נמצא בתוכה, אלא ב-Offset שלילי (דהיינו, ממוקם לפניה בזכרון).
ה-ObjHeader בנוי מ-DWORD שמחזיק בתוכו מספר ערכים שונים, שכוללים בין היתר את אינדקס ה-SyncBlock שמשוייך אליו (יפורט בפוסט עתידי), מספר ה-AppDomain בו האובייקט קיים, ערך ה-Hashcode של האובייקט, וביט נוסף לשימושי SpinLock שנעשים על האובייקט.
מאחר ולכל ביט וביט יש משמעות שונה כאן, העבודה מול ה-ObjHeader נעשית על הביטים עצמם, דרך מניפולציות שונות של Bit Mask'ים כאלו ואחרים. אפשר לקחת בתור דוגמה את הפונקציה GetHeaderSyncBlockIndex שמיצאת את אינדקס ה-SyncBlock מתוך ה-ObjHeader:

// Access to the Sync Block Index, by masking the Value.

DWORD GetHeaderSyncBlockIndex()

{

    // pull the value out before checking it to avoid race condition

    DWORD value = m_SyncBlockValue;

    if ((value & (BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE))

        != BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)

    {

        return 0;

    }

 

    return value & MASK_SYNCBLOCKINDEX;

}


רק לשם ההבהרה, שימו לב שהקוד הזה אינו נמצא תחת מחלקת Object, אלא תחת המחלקה ObjHeader.
ההתיחסות היחידה ל-ObjHeader מתוך המחלקה Object נעשית כאשר רוצים לקבל את ה-ObjHeader, שמיוחס לאותו אובייקט. מדובר בפונקציית Getter פשוטה שפונה ל-Offset המתאים ומחזירה את אותו ObjHeader.

ObjHeader   *GetHeader()

{

    return PTR_ObjHeader(PTR_HOST_TO_TADDR(this) - sizeof(ObjHeader));

}


לסיכום,ניתן לייצג את מבנה האובייקט בזכרון בצורה הבאה:
 

Events Thread Safety Hazards
דבר אחד שימושי בדוט-נט הוא התמיכה הטבעית הקיימת ל-Event'ים. כדי ליצור אירוע למחלקה שלנו, כל מה שעלינו לעשות הוא להגדיר Event חדש בעזרת ה-Delegate המתאים, וזהו זה. שימו לב שאפילו לא היינו צריכים לכתוב Accessors כלשהם, מאחר וכברירת מחדל, אלא אם כן לא הגדרנו אחרת, הקומפיילר יחולל כבר Accessor'ים משלו שידאגו לנהל עבורנו את הרישומים עבור האירוע שהגדרנו. זוהי נקודה שהרבה פעמים לוקחים כמובן מאליו, ולא מפנים אליה תשומת לב מיוחדת, ולמרות זאת, מסתתרת מאחוריה משמעות קריטית כש-Multithreading נכנס לתמונה.
הרעיון הוא כזה, מה קורה במידה וננסה להוסיף/להסיר הרשמויות לאותו האירוע מתוך מספר ת'רדים שונים? קיים כאן עניין של סנכרון שאנו מוכרחים לטפל בו.
אז כדי לבדוק באמת מה קורה, נכתוב דוגמה קצרה ונראה איזה קוד IL נקבל.

אז קוד הדוגמה שלנו יהיה:

public class Foo

{

    public event EventHandler Bar;

}


הדבר הראשון שנוכל לראות ב-IL הוא שחוללו לנו ה-Accessors הנחוצים עבור האירוע. ניקח את Add לצורך ההדגמה:

.method public hidebysig specialname instance void add_Bar(
class
[mscorlib]System.EventHandler 'value') cil managed synchronized
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldarg.0
L_0002: ldfld class [mscorlib]System.EventHandler Demo.Foo::Bar
L_0007: ldarg.1
L_0008: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(
class
[mscorlib]System.Delegate, class [mscorlib]System.Delegate)
L_000d: castclass [mscorlib]System.EventHandler
L_0012: stfld class [mscorlib]System.EventHandler Demo.Foo::Bar
L_0017: ret
}

והנה, בחתימת הפונקציה אנחנו יכולים להבחין במילת הקסם synchronized שנעשה בה שימוש על מנת להעניק לנו את אותו Thread Safety נחשק.
אבל בעצם, מה מילת הקסם הזאת אומרת? ומה כל כך רע בה?
אז ככה, synchornized היא הדרך של הקומפיילר להדריך את ה-JIT שאת הקטע קוד הבא יש לבצע רק על ידי ת'רד בודד בכל רגע נתון. האלמנט הזה מתווסף אוטומטית כשאנחנו כותבים Event'ים ללא מימוש מפורש של Accessors, ומעבר לכך, אנחנו יכולים לאלץ הוספה שלו בקוד על ידי שימוש ב Attribute של MethodImpl (עם הדגל Synchronized).
הסיבה ששימוש ב-synchronized הוא כל כך רע הוא צורת הנעילה שלו. כשה-JIT נתקל בפונקציה שמוגדרת עם synchronized הוא עוטף את כל קטע הקוד שלה עם Monitor.Enter כשאובייקט הנעילה הוא המופע שמכיל את ה-Event, או הטיפוס שלו (כשמדובר ב-Event'ים סטאטים). כלומר, הקוד הנ"ל שווה ערך לקוד הבא כשמדובר ב-Instance Event:

public class Foo

{

    private EventHandler m_bar;

 

    public event EventHandler Bar

    {

        add

        {

            Monitor.Enter(this);

            m_bar = m_bar + value;

            Monitor.Exit(this);

        }

        remove

        {

            Monitor.Enter(this);

            m_bar = m_bar - value;

            Monitor.Exit(this);

        }

    }

}


ולקוד הבא כשמדובר ב-Static Event:

public static class Foo

{

    private static EventHandler s_bar;

 

    public static event EventHandler Bar

    {

        add

        {

            Monitor.Enter(typeof(Foo));

            s_bar = s_bar + value;

            Monitor.Exit(typeof(Foo));

        }

        remove

        {

            Monitor.Enter(typeof(Foo));

            s_bar = s_bar - value;

            Monitor.Exit(typeof(Foo));

        }

    }

}


רק כדי לעשות את זה מעט ברור יותר, אין לנעול לעולם על this או typeof. וליתר דיוק, על כל אובייקט שהוא Publicly Accessed. הכוונה היא שבעוד שאנחנו נועלים אותו, יכולים להגיע גם אנשים אחרים, מחוץ לקוד שלנו (למשל מודול כלשהו שמשתמש בטיפוס שלנו), ולנעול אותו גם כן. כל הנעילות האלה שמתבצעות ללא הכרה אחת של השניה יכולות לגרום בקלות ל-Deadlock'ים "נסתרים" בקוד. המצב עוד יותר גרוע כשמנסים לנעול את typeof מאחר וה-Type נגיש לכל ה-AppDomain שאנו נמצאים בו.

מכאן, ששימוש ב-Events ללא מימוש מפורש של Add/Remove, הם פתח לאי אלו התנהגויות "מעניינות" בתוכנית שלכם ברגע שמערבים Multithreading. ויש לדאוג להמנע מכך, או לפחות לנהוג בזהירות יתרה.
מה שאירוני כאן זה שבכל מקום שלא תחפשו, תתקלו באינספור הזהרות והוראות ממיקרוסופט עצמה לא לנעול בצורה הזאת. והנה, בלי ששמנו לב אותה נעילה מסוכנת נעשית ממש מתחת לאף שלנו, בלי שבכלל שמנו לב.