結構化異常處理SEH:__finally終止處理。
結構化異常處理(Structuredexception handling)簡稱SEH。是windows系統提供的異常處理機制。促使windows將SEH加入到windows系統的一個關鍵原因就是:它可以簡化作業系統本身的開發工作,同時還讓系統更加健壯。
我們當然也可以在我們的程式中添加SEH機制,這樣我們的應用程式也可以變得更加健壯。使用SEH,我們在編寫代碼時可以先集中精力完成軟體的正常工作流程。也就是說將軟體主要功能編寫和軟體異常處理這兩個任務分離開,最後再去處理軟體可能遇到的各種錯誤情況。
為了實現SEH,編譯器完成了很多的工作。在進入和離開異常處理代碼時編譯器會插入一些額外的代碼,有時這會導致很大的開銷。後面的文章我會介紹編譯器都是做了哪些工作。
雖然不同的廠商按照不同的方式來實現SEH,但是大部分的編譯廠商都遵循了microsoft的文法規則。因此本文我們將介紹Microsoft visualC++編譯器規定的文法。
SEH包括兩個方面的內容:終止處理和異常處理。本文我們介紹終止處理,下一篇文章將介紹異常處理。
終止處理
終止處理常式確保無論一個代碼塊是如何退出的,都能保證該終止處理常式能夠被執行。其文法如下:
__try{ //被保護的代碼。}__finally//終止處理。{ //終止處理代碼。}
注意:try和finally前都有兩個底線。
終止程式也分為兩個部分:__try塊為中止處理常式要保護的代碼。無論程式以何種方式從該塊中退出(如return、goto等語句),__finally塊都會被執行。
__finally塊為終止處理常式塊,該塊中的代碼會在控制流程從__try塊退出後、程式結束之前被調用。一般用來執行一些清理操作,如釋放資源、釋放佔有的互斥量等。
但是上面所說的”無論何種方式“有些絕對。因為這個世界本來就沒有絕對的東西。當在try塊中使用了ExitProcess、ExitThread、TerminateProcess、TerminateThread來終止線程或進程時,finally塊就不會被調用。要特別注意這些例外,禁止在try塊中使用這些函數。
下面通過代碼給大家講解終止處理常式的使用:
__try{ WaitForSingleObject(hMutex,INFINITE); if(x==false) return-1;}__finally{ ReleaseMutex(hMutex); return -2;}return 0;
可以看到上面的代碼在try中首先等待互斥量核心對象被觸發,等待成功後互斥量就屬於該線程所有。如果此時執行return-1,則該線程結束,就會導致互斥量對象一直被佔有的情況的發生,其他線程會一直處於等待狀態。這就是所謂的資源泄漏。有了finally塊的保護後,當執行return-1程式試圖退出try塊時,編譯器會讓finally塊在return之前執行,同時在return(其他語句,如goto等語句也一樣)語句之前插入一些代碼。return的傳回值會被儲存在一個臨時變數中。
但上面的代碼程式的傳回值是多少呢?是-1,還是-2呢?答案是-2。當編譯器在try塊中檢測到return語句時,雖然會產生一些代碼將傳回值儲存在一個臨時變數中。但是由於finally塊代碼中也有一個return,finally塊中return的值會將原來的傳回值覆蓋。所以函數最終返回-2。
這個過程被稱為局部展開。局部展開會在系統因為try塊中的代碼提前退出時發生。局部展開會導致非常大的額外開銷,因為編譯器必須插入代碼來保證finally塊在程式退出之前執行。最理想的情況就是代碼控制流程正常的離開try塊而進入到finally塊中,這時的額外開銷最小。在x86體系下離開try塊正常進入到finally塊只需要執行一條指令。
因此為了將效能開銷降到最低,我們改進了上面的代碼:
__try{ WaitForSingleObject(hMutex,INFINITE); if(x==false) gotoEndOfTryBlock; //一些代碼。 EndOfTryBlock:}__finally{ ReleaseMutex(hMutex); return -2;}return 0;
在上面改進後的代碼中,當在try塊中檢查到錯誤發生時不再直接調用return強制退出。而是執行了一條goto語句到try塊的末尾。執行此跳轉後,控制流程從try塊中正常退出,直接進入到finally塊中。由於沒有發生局部展開,編譯器不需要插入額外指令,也就沒有導致額外的開銷。
由於goto語句會破壞程式的執行流程,很多書上都再三強調禁止使用goto。其實我們也沒有必要使用goto,因為microsoft提供給我們一個關鍵字_leave,也可以執行類似的操作。關鍵字_leave會導致代碼執行控制流程跳轉到try塊的末尾,從而代碼將正常的從try塊進入到finally塊中。
下面為使用__leave關鍵字的改進代碼:
__try{ WaitForSingleObject(hMutex,INFINITE); if(x==false) __leave //一些代碼。}__finally{ ReleaseMutex(hMutex); return -2;}return 0;
本章開頭曾介紹過使用SEH可以讓程式員將程式正常執行流程和錯誤處理分開。下面我們分別展示兩個例子,一個沒有使用SEH,而另一個使用SEH,看下它們到底有何差別,通過比較我們也可以更好的知道SEH是如何完成上述工作的。
下面是沒有使用SEH的程式:
bool fun(char* fileName){ HANDLEhFile=CreateFile(....); if(hFile==INVALID_HANDLE_VALUE) { returnfalse; } HANDLEhFileMapping=CreateFileMapping(...); if(hFile==NULL) { CloseHandle(hFile); returnfalse; } char*p=MapViewOfFile(..); if(p==NULL) { CloseHandle(hFile); CloseHandle(hFileMapping); return false; } //其他工作....... returntrue;}
相信我們很多人都寫過類似上面的代碼。我們可以看到,上面的代碼中包含了很多錯誤檢查和資源清理的代碼。過多的錯誤碼檢查和資源清理工作會使得代碼難以閱讀,同時也難以編寫、修改和維護。
現在讓我們來通過使用SEH機制改進上面的代碼:
bool fun(char* fileName){ HANDLEhFile=INVALID_HANDLE_VALUE; HANDLEhFileMapping=NULL; char*p=NULL; __try { hFile=CreateFile(....); if(hFile==INVALID_HANDLE_VALUE) __leave; hFileMapping=CreateFileMapping(...); if(hFile==NULL) __leave; p=MapViewOfFile(..); if(p==NULL) __leave; //其他代碼。 } __finally { if(hFile!=INVALID_HANDLE_VALUE) CloseHandle(hFile); if(hFileMapping) CloseHandle(hFileMapping); } returntrue;}
從上面的代碼我們可以看到代碼簡潔多了。清理工作放在最後執行,且能夠保證能得到執行,代碼看起來簡潔而有序,提高了可讀性也有利於以後的維護。
注意事項:前面我們介紹了兩種會引起finally塊執行的情形:
一:從try塊正常退出,進入到finally塊。
二:局部展開:從try塊中提前退出,將程式控制流程強制轉到finally塊。
除了上面的情況外,還有一種情況:全域展開,也會導致finally塊被執行。關於全域展開由於涉及到異常處理的內容,我們將在下一篇文章中詳細介紹!
finally的執行都是由上面三種情況之一造成的。
如果要確定到底正常進入finally還是異常退出try塊退出,可以調用AbnormalTermination函數來判斷:
BOOL AbnormalTermination();
注意只能在finally塊中調用該函數。當返回true時,表示控制流程從try塊中正常進入到finally塊中。返回false時,則表示控制流程從try塊中異常退出,通常是由於執行goto、break、return或continue語句而導致局部展開,或是try塊中的代碼拋出異常而引起全域展開所致。
如有紕漏,請指正!謝謝!
2013、3、11於浙江杭州