TechEd2008 .NET 應用程式除錯秘技系列(4) 應用程式沒有回應(Idle Hang) 下
終於進入到正題囉~馬上來看一下這個Idle Hang的問題如何重現?
重現步驟:
1. 執行應用程式並開啟WinDbg,在WinDbg中直接附加到HangDemo.exe這個process。忘記的請參考前一篇文章。
2. 在轉帳旁邊的TextBox輸入一個金額(Ex, 2000),按一下"轉帳"鈕,然後很快的切到"支票轉存款帳戶",再按一次"轉帳"
3. 此時您再去按顯示餘額(無論支票或存款)都會變成灰色且無回應。
4. 這時候我們已經確定應用程式Hang住了~可以回到WinDbg裏,在Debug=>Break或按一下
圖示將程式中斷下來。
5. 載入SOS.dll (輸入 ".loadby sos mscorwks")
6. 輸入"!syncblk",如果不知道指令的意思及用法,可以用!help <command>取得說明。例如 !help syncblk 會得到如下結果:
0:015> !help syncblk
-------------------------------------------------------------------------------
!SyncBlk [-all | <syncblk number>]
A SyncBlock is a holder for extra information that doesn't need to be created
for every object. It can hold COM Interop data, HashCodes, and locking
information for thread-safe operations.
When called without arguments, !SyncBlk will print the list of SyncBlocks
corresponding to objects that are owned by a thread. For example, a
lock(MyObject)
{
.... (略)
輸入!syncblk後會得到以下資訊:
0:015> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
34 003fdad4 7 1 0044a0b8 19b0 9 029c3774 HangDemo.Account
36 003fdb34 5 1 0044da00 9fc 8 029729c0 HangDemo.Account
-----------------------------
Total 37
CCW 2
RCW 0
ComClassFactory 0
Free 0
以上有幾個資訊要注意,一是Owning Thread,另一個是Own住的物件位址及型別。 Syncblk指令主要會列出目前lock的資訊。
7. 使用 "!threads"可以列出所有threads。使用"~<n>s" 指令可以切換到不同thread,以此例來看,我們應該看第8及第9條thread在做什麼,以致於應用程式沒有回應。
8. 輸入 "~8s" 切換到第8條thread。然後輸入 "!clrstack",這個指令可以顯示當前thread的call stack,若加上 -a的參數,則可以顯示相關的參數及區域變數。執行後得到的結果如下:
0:009> ~8s
eax=00000000 ebx=00000001 ecx=00000000 edx=00000000 esi=00000000 edi=00000000
eip=77d60bc5 esp=05faef64 ebp=05faeffc iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
ntdll!NtWaitForMultipleObjects+0x15:
77d60bc5 c21400 ret 14h
0:008> !clrstack
OS Thread Id: 0x9fc (8)
ESP EIP
05faf1f0 77d60bc5 [GCFrame: 05faf1f0]
05faf32c 77d60bc5 [HelperMethodFrame: 05faf32c] System.Threading.Monitor.Enter(System.Object)
05faf380 008622ad HangDemo.Form1.SavToChk()
05faf3a4 793b0d1f System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
05faf3ac 79373ecd System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
05faf3c4 793b0c68 System.Threading.ThreadHelper.ThreadStart()
05faf5ec 79e7c74b [GCFrame: 05faf5ec]
我們看到程式在執行SavToChk函式後,呼叫了Monitor.Enter方法。
9. 切換到第9條thread,同樣來看看它的call stack
0:008> ~9s
eax=00000004 ebx=00000001 ecx=00000000 edx=00000000 esi=00000000 edi=00000000
eip=77d60bc5 esp=0619f144 ebp=0619f1dc iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
ntdll!NtWaitForMultipleObjects+0x15:
77d60bc5 c21400 ret 14h
0:009> !clrstack
OS Thread Id: 0x19b0 (9)
ESP EIP
0619f3d0 77d60bc5 [GCFrame: 0619f3d0]
0619f50c 77d60bc5 [HelperMethodFrame: 0619f50c] System.Threading.Monitor.Enter(System.Object)
0619f560 0086254d HangDemo.Form1.ChkToSav()
0619f584 793b0d1f System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
0619f58c 79373ecd System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0619f5a4 793b0c68 System.Threading.ThreadHelper.ThreadStart()
0619f7cc 79e7c74b [GCFrame: 0619f7cc]
這call stack看起來好眼熟喔~但仔細一看,這裏執行的是ChkToSav (第8條執行的是SavToChk),對照我們剛剛在程式裏操作的步驟:支票轉存款,然後存款轉支票~Bingo!!好像離答案愈來愈近了。
既然有source code,當然是拿來看看這兩個方法在做什麼最快了!請看以下程式碼:
void ChkToSav()
{
double amount = Convert.ToDouble(transf_textBox.Text);
Monitor.Enter(acnt_checking);
acnt_checking.withdraw_checking(123, amount);
Thread.Sleep(5000);
Monitor.Enter(acnt_saving);
acnt_saving.deposit_saving(123, amount);
Monitor.Exit(acnt_saving);
Monitor.Exit(acnt_checking);
}
void SavToChk()
{
double amount = Convert.ToDouble(transf_textBox.Text);
Monitor.Enter(acnt_saving);
acnt_saving.withdraw_saving(123, amount);
Thread.Sleep(5000);
Monitor.Enter(acnt_checking);
acnt_checking.deposit_checking(123, amount);
Monitor.Exit(acnt_checking);
Monitor.Exit(acnt_saving);
}
看到了嗎? ChkToSav是支票轉存款,它會先lock acnt_checking,然後sleep 5秒後再lock acnt_saving. SavToChk則剛好相反。所以我們就是利用這5秒鐘造成一個deaklcok。
可是~~剛剛我們在dump裏看到的不是物件位址029729c0 和 029c3774 嗎? 怎麼確定它就是acnt_checking和acnt_saving呢? 給各位一個提示,用!do指令去看看Form物件裏有哪些欄位吧!
另外要思考的是,我們找出deadlock的原因了,但怎麼解決呢? 也許我們應該要思考的是,轉帳這個動作用lock適合嗎?有沒有其他的做法呢? 留給各位當練習囉~