在平時使用軟件或是.NET程序開發的過程當中,咱們有時會遇到程序關閉後但進程卻沒有退出的狀況,這每每預示着代碼中有問題存在,不能正確的在程序退出時中止代碼執行和銷燬資源。這個現象有時並不容易被察覺,但在另外一些狀況下卻會產生影響軟件功能的Bug。本文列舉可能影響.NET程序進程退出的因素,並用幾個小例子說明這些因素如何致使Form Application和Windows Service的Bug。程序員
1、進程不能退出對於某些Windows Form程序的影響數據庫
在傳統C/S結構的系統中,客戶端會經過Socket或WCF服務利用特定的端口與服務端保持通訊。所以在不少應用場景中,爲避免端口衝突,單臺計算機同一時刻只容許啓動一個客戶端,這也符合一個客戶端表明單個用戶角色的業務設計。這能夠經過Mutex類,或者在客戶端啓動時檢查是否已有同名的進程存在來實現。有些客戶端啓動邏輯被設計成當存在已有進程時,不初始化用戶界面,而是自動切換到已經打開的客戶端並關閉自身。編程
在這種狀況下,若是前一次從客戶端界面中退出,可是進程沒有關閉,那隨後再次啓動客戶端時就再也沒法正常顯示出用戶界面,除非手動殺掉進程再次啓動。服務器
2、Foreground線程致使進程沒法退出的例子網絡
用以下代碼來模擬進程沒法退出的狀況。簡單起見,這個小窗口程序沒有任何網絡或數據庫操做,僅僅是用一個線程定時刷新UI。設想是當程序界面構建完成後啓動一個Thread,隨後每隔1秒刷新當前時間,當點擊窗體關閉按鈕以後,程序退出,Thread和進程一同被銷燬。ide
1 public partial class Form1 : Form 2 { 3 Thread worker = null; 4 5 public Form1() 6 { 7 InitializeComponent(); 8 Load += new EventHandler(Form1_Load); 9 } 10 11 void Form1_Load(object sender, EventArgs e) 12 { 13 worker = new Thread(new ThreadStart(DoWork)); 14 worker.Start(); 15 } 16 17 private void DoWork() 18 { 19 while (true) 20 { 21 Thread.Sleep(1000); 22 if (IsHandleCreated && !IsDisposed) 23 { 24 Invoke((MethodInvoker)(() => label1.Text = DateTime.Now.ToString())); 25 } 26 } 27 } 28 }
在關閉窗體以後,實際的運行結果倒是,用戶看不到任何界面,但進程一直停留在任務管理器中,Thread也沒有中止工做。spa
本例中,進程沒法退出的緣由就在於worker線程的IsBackground屬性。建立Thread時沒有對它賦值,IsBackground就保留它的默認值false,這種方式啓動的線程也叫前臺線程。能夠看出,從Thread類建立出來的線程默認爲前臺線程。按照MSDN的解釋,前臺線程與後臺線程惟一的區別,就是前者在完成執行代碼以前會阻止進程的終止。也即.NET進程在退出時,會先等待前臺線程執行完全部的操做,然後直接終止正在運行中的後臺線程。線程
3、什麼狀況下使用Foreground線程設計
因爲Background線程在進程程退出時被當即停止可能致使處理中斷或數據丟失,當線程處理的任務和數據比較重要時,須要考慮用Foreground線程。例如但願退出程序時仍然能完整保存數據,或者在退出時須要完成到服務器的數據上傳工做,或者須要確保某些資源得以釋放。而在另外一些狀況下,若是線程執行的任務在並非很是重要,則能夠考慮用Background線程,如監聽網絡通訊或臨時計算任務等。日誌
.NET中有多種方式能夠建立或使用一個新線程,除了Thread類以外,還有ThreadPool.QueueUserWorkItem方法、BackgroundWorker類、Task類、Parallel類以及各類Timer。在這之中,只有從Thread類建立出來的線程纔會默認是Foreground,其它的類多數是使用線程池中的線程來執行任務,而線程池中所有是Background線程。
除了使用Thread類建立Foreground線程外,設置Thread.CurrentThread.IsBackground屬性值可讓運行中的Background線程變爲Foreground線程。但這種方式應該謹慎使用,主要緣由在於執行該語句的線程可能由線程池進行管理,咱們難以在應用程序中對該線程的行爲和生命週期進行控制,也不該該這樣作。假如該線程執行任務非關鍵任務,又耗時比較長,那將其IsBackground設置爲false一樣會阻礙進程的退出,也不符合使用線程池的原則。但若是有明確的意圖須要這樣作,惟一須要保證的是讓線程的任務快速完成。使用完線程池中的線程後忘記重置IsBackground爲true並不會致使任何問題,由於線程池會在重用線程時重置這個值。
4、控制線程正常退出
回到上面的示例代碼,假如咱們已經決定要使用Foreground線程,那須要作的就是給線程的執行代碼一個退出條件,讓它在恰當的時候優雅的中止,而非無休止的運行下去。能夠設置一個變量指示主窗口是否正在退出,再由線程按期檢查這個變量,決定是否結束。
1 public partial class Form1 : Form 2 { 3 Thread worker = null; 4 bool isClosing = false; 5 6 public Form1() 7 { 8 InitializeComponent(); 9 10 worker = new Thread(new ThreadStart(DoWork)); 11 worker.Start(); 12 } 13 14 private void DoWork() 15 { 16 while (!isClosing) 17 { 18 Thread.Sleep(1000); 19 if (IsHandleCreated && !IsDisposed) 20 Invoke((MethodInvoker)(() => label1.Text = DateTime.Now.ToString())); 21 } 22 } 23 24 protected override void OnClosing(CancelEventArgs e) 25 { 26 base.OnClosing(e); 27 isClosing = true; 28 } 29 }
5、Foreground致使Windows Service進程延遲退出
對於Windows Service程序來說,Foreground線程仍然會阻止Service進程的退出,可是狀況稍有不一樣。一段最簡單的Service程序代碼以下,服務啓動代碼寫在OnStart方法中,建立了一個線程對象循環執行任務,OnStop方法會在服務中止時被調用,這裏假設須要5秒鐘時間運行資源清理代碼。
1 public partial class Service1 : ServiceBase 2 { 3 Thread worker; 4 5 public Service1() 6 { 7 InitializeComponent(); 8 } 9 10 protected override void OnStart(string[] args) 11 { 12 worker = new Thread(new ThreadStart(DoWork)); 13 worker.Start(); 14 } 15 16 protected override void OnStop() 17 { 18 // Clean up resources. 19 Thread.Sleep(5000); 20 } 21 22 private void DoWork() 23 { 24 while (true) 25 { 26 // Time consuming work task. 27 Thread.Sleep(50); 28 } 29 } 30 }
在服務中中止這個名爲「Windows Service Stop Test」的服務,帶有進度條的服務控制對話框出現,並在5秒鐘後關閉。對於服務控制器來講,OnStop方法執行完畢即意味着服務中止動做已經完成,服務控制器最多等待OnStop方法執行125秒,超過這個時間以後會彈出錯誤1053:「服務沒有及時響應啓動或控制請求」並返回,以後OnStop方法中的代碼仍然會繼續運行直到完成。這時因爲Foreground線程還在運行,服務對應的進程也沒有退出,仍然在任務管理器裏面。然而與Windows Form程序不一樣的是,30秒後這個進程會被強制退出。這種狀況下,沒有正確退出的Foreground會致使的進程延遲時間是30秒。
6、Finalize方法致使的延遲
假定全部的線程都被妥善管理,Service中止以後進程退出的時間仍然可能因爲Finalize方法的執行產生延遲。進程退出時會致使進程中的AppDomain被卸載和CLR被關閉,這一動做會觸發對全部對象的垃圾回收,並調用它們的Finalize方法。Finalize方法被容許的最長執行時間是2秒,所以進程可能會在Service中止2秒以後才退出。
7、進程延遲退出可能暴露出來的問題
進程延遲2秒或30秒退出會有什麼問題呢?下面這個示例在Service啓動時監聽本機某個端口,在中止時花5秒鐘時間作了一些清理工做,可是因爲種種緣由沒有關閉對端口的監聽。在實際的項目中,這種狀況時有發生。多是某個程序員認爲進程終止後對端口的監聽天然消失,沒有必要手動關閉;也多是因爲要釋放的資源太多,漏掉了關閉端口代碼。固然還有另一種狀況,設想關閉端口的代碼位於某個類型的Finalize方法中,而Finalize方法尚未執行到這一行代碼就由於超出2秒時間被終止……
1 public partial class Service1 : ServiceBase 2 { 3 TypeA objectA = null; 4 5 public Service1() 6 { 7 InitializeComponent(); 8 } 9 10 protected override void OnStart(string[] args) 11 { 12 objectA = new TypeA(); 13 14 TcpListener listener1 = new TcpListener(IPAddress.Parse("127.0.0.1"), 12345); 15 listener1.Start(); 16 } 17 18 protected override void OnStop() 19 { 20 // Clean up resources. 21 Thread.Sleep(5000); 22 } 23 } 24 25 public class TypeA 26 { 27 ~TypeA() 28 { 29 // Clean up resources. 30 Thread.Sleep(3000); 31 } 32 }
如今,啓動這個服務,再中止這個服務,而後再次啓動,雖然Finalize方法致使進程退出晚了兩秒,但到目前爲止並無形成任何麻煩。然而當想要嘗試「從新啓動」這個服務的時卻獲得了「本地計算機上的服務啓動後中止」的提示,服務沒法啓動成功。
檢查事件查看器,咱們能夠很快發現問題出在對網絡端口的爭用上。在用戶嘗試「從新啓動」時,服務控制器僅僅是簡單的中止並啓動服務。中止的時候,完成OnStop方法須要5秒鐘,以後控制器認爲服務中止過程已完成(實際上也確實如此),再次啓動服務,並開始監聽同一網絡端口。但這時前一次中止的服務進程尚未徹底退出,端口也沒有釋放,所以新的進程打開這一端口就產生了SocketException。
8、讓進程更快退出的幾個編程建議
嚴格來講,進程延遲退出並無致使任何新問題的產生,只是暴露了代碼裏本來已經存在的缺陷,這些缺陷幾乎都與資源的使用和釋放不當有關。當代碼中有完善且恰到好處的錯誤日誌時,這些問題或許很快就能被定位和解決,而在另外一些狀況下可能要花費一些周折才能找到根源所在。所以在平時的編程中就遵循一些規則來避免這類問題的發生是有必要的,結合本文的小例子,有以下建議: