בדיון שעלה לאחרונה בפורום דוט-נט בתפוז, עלתה השאלה "באיזה פרימיטיב סנכרון ה-CLR משתמש כאשר אנחנו קוראים ל-Monitor.Enter?". האם נוצר Mutex לטובת העניין? אולי Event? ואולי בכלל מדובר בפרימטיב שרץ גם ב-User Mode כגון CriticalSection? מתברר שקיימת איזשהיא אי-בהירות בנושא הזה, כך שהפוסט הזה ידגים איך ניתן לברור את התשובה לשאלה בכלים העומדים לרשותנו.
לפני קרוב לשנה פרסמתי פוסט קצר שתיאר בקווים כללים את האופן בו מנגנון הנעילות של ה-CLR עובד. בקצרה, הרעיון הוא שמתוך כל בלוק Header של אובייקט דוט-נטי אפשר להגיע לאובייקט SyncBlock המקושר אליו, שלמעשה מייצג את יכולות הסנכרון של האובייקט, שה-CLR דואג לחשוף לנו. צריך לזכור שיכולות הסנכרון האלה הן פיצ'ר של ה-CLR, במובן הזה שהן ממומשות בתוך ה-CLR, ולאו דווקא בתוך (או באמצעות) פרימיטיבים השייכים למערכת ההפעלה. כמו ש"על הנייר" ת'רד דוט-נטי לא חייב להתבסס בהכרח על Kernel Thread של מערכת ההפעלה, כך גם אין חובה מצד ה-CLR להשתמש בפרימיטיב סנכרון כלשהו של מערכת ההפעלה. היום זה אינו המצב, ובינתיים גם לא נשמעות שום תוכניות שהמצב הזה הולך להשתנות בעתיד הקרוב או הרחוק.
בסקירה ראשונה בתיעוד ב-MSDN אפשר להתרשם שלא קיים תיעוד אמיתי לגבי הפרימטיב בו נעשה שימוש. אבל מאחר ומדובר באובייקט סנכרון, אפשר להזכר בממשק IHostSyncManager שנחשף לנו דרך יכולות ה-Hosting של ה-CLR. אחת מהפונקציונליות שהממשק הזה חושף לנו היא היכולת להחליף את המימוש של פרימיטיב הסנכרון בו נעשה שימוש בתוך Monitor.Enter. הפונקציונליות הזאת מתאפשרת לנו דרך הפונקציה CreateMonitorEvent.
כבר בשלב הזה אפשר לשים לב למה שנאמר תחת פסקת ה-Remarks:
עם זאת, מילת המפתח כאן היא באמת "Mirrors" כך שאין כאן הבטחה אמיתית לגבי מה שמתרחש במימוש ב-CLR עצמו. כך שכדי לאמת את הרמז העבה שקיבלנו כאן, נצטרך לשחק מעט עם WinDbg.
אז לצורך הבדיקה, נכתוב תוכנית קצרה הגורמת ל-Contention אינספוי בין שני ת'רדים:
אחרי שהתוכנית כבר רצה ברקע, נוכל להריץ את WinDbg ולעשות Attache לפרוסס המתאים. כמו תמיד, הדבר הראשון שאנחנו רוצים לעשות הוא לטעון את SOS שיתן לנו כמה עזרים לדבג את התוכנית הדוט-נטית שלנו. עד גרסה 4 של הפריימוורק היינו רגילים לרשום את הפקודה הבאה:
>.loadby sos mscorwks
שלמעשה אומרת שאנחנו רוצים לטעון את sos.dll מהכתובת ממנה נטען mscorwks.dll (למעשה, אנחנו חוסכים חיפוש והקלדה של הנתיב המלא של הקובץ). אבל, כחלק מהשינויים שהפריימוורק עברה בגרסה 4, השם של הקובץ mscorwks עודכן ל-clr. כך שאם ננסה להשתמש באותה פקודה אלמותית, נקבל את השגיאה "Unable to find module mscorwks" מהסיבה הפשוטה שה-dll המדובר כלל לא נטען לפרוסס שלנו. לכן נצטרך לעדכן את הפקודה כך שנשתמש ב-clr.dll. לאחר ההרצה שלה, נוכל לוודא שטענו את ה-dll המתאים על ידי קריאה ל-chain.
>.loadby sos clr
>.chain
Extension DLL search Path:
[...]
Extension DLL chain:
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\sos: image 4.0.30319.1, API 1.0.0, built Thu Mar 18 10:09:41 2010
[path: C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\sos.dll]
dbghelp: image 6.11.0001.404, API 6.1.6, built Thu Feb 26 03:55:30 2009
[path: C:\Program Files\Debugging Tools for Windows (x86)\dbghelp.dll]
ext: image 6.11.0001.404, API 1.0.0, built Thu Feb 26 03:55:30 2009
[path: C:\Program Files\Debugging Tools for Windows (x86)\winext\ext.dll]
exts: image 6.11.0001.404, API 1.0.0, built Thu Feb 26 03:55:24 2009
[path: C:\Program Files\Debugging Tools for Windows (x86)\WINXP\exts.dll]
uext: image 6.11.0001.404, API 1.0.0, built Thu Feb 26 03:55:26 2009
[path: C:\Program Files\Debugging Tools for Windows (x86)\winext\uext.dll]
ntsdexts: image 6.1.7015.0, API 1.0.0, built Thu Feb 26 03:54:43 2009
[path: C:\Program Files\Debugging Tools for Windows (x86)\WINXP\ntsdexts.dll]
השלב הבא יהיה להגיע לת'רד שהפסיד במרוץ לנעילה, וכעת נאלץ להמתין עד שהיא תשתחרר. לשם כך נדפיס את ה-Managed Stack של כל הת'רדים שלנו, וכשנראה Stack "מתאים", נעבור לקונטקסט שלו:
>~*e!clrstack // execute !clrstack on all of the threads
OS Thread Id: 0xf80 (0)
Child SP IP Call Site
0012f3ec 030300fe ConsoleApplication1.Program.Main(System.String[]) [...]
0012f648 791421db [GCFrame: 0012f648]
OS Thread Id: 0xf4c (1)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
OS Thread Id: 0x840 (2)
Child SP IP Call Site
02ccfe68 7c90e514 [DebuggerU2MCatchHandlerFrame: 02ccfe68]
OS Thread Id: 0xbe0 (3)
Child SP IP Call Site
0313f67c 7c90e514 [GCFrame: 0313f67c]
0313f794 7c90e514 [GCFrame: 0313f794]
0313f7b0 7c90e514 [HelperMethodFrame_1OBJ: 0313f7b0] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
0313f808 79b2e0c4 System.Threading.Monitor. Enter(System.Object, Boolean ByRef)
0313f818 03030163 ConsoleApplication1.Program.< Main>b__3() [...]
0313f848 79b2ae5b System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
0313f858 79ab7ff4 System.Threading. ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
0313f87c 79ab7f34 System.Threading. ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0313f898 79b2ade8 System.Threading.ThreadHelper. ThreadStart()
0313fabc 791421db [GCFrame: 0313fabc]
0313fd80 791421db [DebuggerU2MCatchHandlerFrame: 0313fd80]
OS Thread Id: 0xe60 (4)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
>~3s // change the current thread to 3
אחרי שמצאנו את הת'רד המתאים, נצטרך לראות את ה-Native Stack שלו, כך שנשתמש כבר בפקודה kb שתציג לנו גם את 3 הפרמטרים הראשונים המועברים לכל פונקציה:
>kb
ChildEBP RetAddr Args to Child
0313f3c8 7c90df4a 7c809590 00000001 0313f3f4 ntdll!KiFastSystemCallRet
0313f3cc 7c809590 00000001 0313f3f4 00000001 ntdll!ZwWaitForMultipleObjects+0xc
0313f468 791f516a 00000001 001820bc 00000000 KERNEL32!WaitForMultipleObjectsEx+0x12c
0313f4cc 791f4f98 00000001 001820bc 00000000 clr!WaitForMultipleObjectsEx_SO_TOLERANT+0x56
0313f4ec 791f4dd8 00000001 001820bc 00000000 clr!Thread::DoAppropriateAptStateWait+0x4d
0313f580 791f4e99 00000001 001820bc 00000000 clr!Thread::DoAppropriateWaitWorker+0x17d
0313f5ec 791f4f17 00000001 001820bc 00000000 clr!Thread::DoAppropriateWait+0x60
0313f640 7919d409 ffffffff 00000001 00000000 clr!CLREvent::WaitEx+0x106
0313f654 792e0160 ffffffff 00000001 00000000 clr!CLREvent::Wait+0x19
0313f6e4 792e0256 001818a0 ffffffff 8079c412 clr!AwareLock::EnterEpilogHelper+0xa8
0313f724 792e029b 001818a0 001818a0 79142c0d clr!AwareLock::EnterEpilog+0x42
0313f744 792c7729 8079cb36 0313f830 00b3c368 clr!AwareLock::Enter+0x5f
0313f800 79b2e0c4 79161f8e 00941f02 0313f840 clr!JIT_MonReliableEnter_Portable+0x104
0313f840 79b2ae5b 00b3c3ec 01b3101c 0313f86c mscorlib_ni+0x2ae0c4
0313f850 79ab7ff4 00b3e010 00000000 00b3c3b8 mscorlib_ni+0x2aae5b
0313f86c 79ab7f34 00000000 00b3c3b8 00000000 mscorlib_ni+0x237ff4
0313f88c 79b2ade8 00b3c3b8 00000000 001818a0 mscorlib_ni+0x237f34
0313f8a4 791421db 000001a7 0313fae0 0313f930 mscorlib_ni+0x2aade8
0313f8b4 79164a2a 0313f980 00000000 0313f950 clr!CallDescrWorker+0x33
0313f930 79164bcc 0313f980 00000000 0313f950 clr!CallDescrWorkerWithHandler+0x8e
בשלב הזה אנחנו כבר יכולים לראות שהדבר האחרון שהת'רד המדובר הספיק לעשות לפני שהפרענו לו הוא לקרוא ל-WaitForMultipleObjectsEx כאשר הפרמטר הראשון 1 והשני הוא 0x001820BC. כך שאפשר להבין שאנחנו למעשה מחכים על Handle בודד, מאחר והפרמטר הראשון מציין את מספר ה-Handle'ים במערך שהועבר בתור הפרמטר השני. אז כל מה שנותר לנו לעשות עכשיו הוא רק להבין מה מסתתר מאחורי אותו Handle שהועבר לפונקציה, מה שיפתור למעשה את תעלומת פרימיטיב הסנכרון שלנו.
>dp 0x001820BC 0x001820BC
001820bc 000006c8 // our handle's value
>!handle 000006c8 F // pass "F" as bitmask to display all of the relevant data
Handle 6c8
Type Event
Attributes 0
GrantedAccess 0x1f0003:
Delete,ReadControl,WriteDac,WriteOwner,Synch
QueryState,ModifyState
HandleCount 2
PointerCount 4
Name <none>
Object Specific Information
Event Type Auto Reset
Event is Waiting
אם כן, זוהי פיסת האימות שחיפשנו. אישור שהפרימיטיב בו אנחנו עושים שימוש הוא למעשה Event מסוג AutoReset.
מי שמעוניין לראות את השימוש והיצירה של ה-Event בקוד, יכול לקחת בתור רפרנס את הקוד ב-sync.cpp מתוך ה-SSCLI, ולראות כיצד קריאה ל-CLREvent::CreateMonitorEvent גוררת קריאה ל-UnsafeCreateEvent (שלמעשה מהווה typedef ל-CreateEvent המוכר).
עם זאת, צריך לזכור שמדובר בתשובה חלקית בלבד. כפי שהזכרתי בתחילת הפוסט, אין כלל הבטחה שכאשר נקרא ל-Monitor.Enter בכלל נגיע למצב בו נשתמש באובייקט קרנל כלשהו. למעשה, בפוסט הזה דאפי דואג לציין שבמימוש ה-CLR'י, כשת'רדים נתקלים ב-Contention הם ינסו קודם לרוץ קצת ב-Busy Loop בתקווה שהנעילה תשתחרר לפני שהם יתיאשו וילכו להמתין על ה-Event שנמצא ב-SyncBlock (למעשה, התנהגות דומה לזו של אובייקטי CriticalSection למעשה יתכן מצב בו עבור SyncBlock'ים שלא מגיעים אף פעם ל-Contention'ים לחסוך לגמרי את העבודה עם אובייקט הקרנל, ולמעשה לבצע את הנעילה כולה ב-User Mode. כך שגם אם ה-CLR לא נותן מימוש מלא ומאפס של פרימטיב סנכרון משלו, עדיין אין סיבה שהוא לא יוסיף יכולות ואופטימיזציות משלו על גבי היכולות שמספקת מערכת ההפעלה.