是什麼在.NET程式關閉時阻礙進程的退出?

来源:http://www.cnblogs.com/silverb/archive/2016/03/10/5263868.html
-Advertisement-
Play Games

在平時使用軟體或是.NET程式開發的過程中,我們有時會遇到程式關閉後但進程卻沒有退出的情況,這往往預示著代碼中有問題存在,不能正確的在程式退出時停止代碼執行和銷毀資源。這個現象有時並不容易被察覺,但在另一些情況下卻會產生影響軟體功能的Bug。本文列舉可能影響.NET程式進程退出的因素,並用幾個小例子


在平時使用軟體或是.NET程式開發的過程中,我們有時會遇到程式關閉後但進程卻沒有退出的情況,這往往預示著代碼中有問題存在,不能正確的在程式退出時停止代碼執行和銷毀資源。這個現象有時並不容易被察覺,但在另一些情況下卻會產生影響軟體功能的Bug。本文列舉可能影響.NET程式進程退出的因素,並用幾個小例子說明這些因素如何導致Form Application和Windows Service的Bug。

一、進程不能退出對於某些Windows Form程式的影響

在傳統C/S結構的系統中,客戶端會通過Socket或WCF服務利用特定的埠與服務端保持通信。因此在很多應用場景中,為避免埠衝突,單台電腦同一時刻只允許啟動一個客戶端,這也符合一個客戶端代表單個用戶角色的業務設計。這可以通過Mutex類,或者在客戶端啟動時檢查是否已有同名的進程存在來實現。有些客戶端啟動邏輯被設計成當存在已有進程時,不初始化用戶界面,而是自動切換到已經打開的客戶端並關閉自身。

在這種情況下,如果前一次從客戶端界面中退出,但是進程沒有關閉,那隨後再次啟動客戶端時就再也無法正常顯示出用戶界面,除非手動殺掉進程再次啟動。

二、Foreground線程導致進程無法退出的例子

用如下代碼來模擬進程無法退出的情況。簡單起見,這個小視窗程式沒有任何網路或資料庫操作,僅僅是用一個線程定時刷新UI。設想是當程式界面構建完成後啟動一個Thread,隨後每隔1秒刷新當前時間,當點擊窗體關閉按鈕之後,程式退出,Thread和進程一同被銷毀。

 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也沒有停止工作。

本例中,進程無法退出的原因就在於worker線程的IsBackground屬性。創建Thread時沒有對它賦值,IsBackground就保留它的預設值false,這種方式啟動的線程也叫前臺線程。可以看出,從Thread類創建出來的線程預設為前臺線程。按照MSDN的解釋,前臺線程與後臺線程唯一的區別,就是前者在完成執行代碼之前會阻止進程的終止。也即.NET進程在退出時,會先等待前臺線程執行完所有的操作,而後直接終止正在運行中的後臺線程。

三、什麼情況下使用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並不會導致任何問題,因為線程池會在重用線程時重置這個值。

四、控制線程正常退出

回到上面的示例代碼,假如我們已經決定要使用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 }

五、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秒。

六、Finalize方法導致的延遲

假定所有的線程都被妥善管理,Service停止之後進程退出的時間仍然可能由於Finalize方法的執行產生延遲。進程退出時會導致進程中的AppDomain被卸載和CLR被關閉,這一動作會觸發對所有對象的垃圾回收,並調用它們的Finalize方法。Finalize方法被允許的最長執行時間是2秒,因此進程可能會在Service停止2秒之後才退出。

七、進程延遲退出可能暴露出來的問題

進程延遲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。

八、讓進程更快退出的幾個編程建議

嚴格來說,進程延遲退出並沒有導致任何新問題的產生,只是暴露了代碼里原本已經存在的缺陷,這些缺陷幾乎都與資源的使用和釋放不當有關。當代碼中有完善且恰到好處的錯誤日誌時,這些問題或許很快就能被定位和解決,而在另一些情況下可能要花費一些周折才能找到根源所在。因此在平時的編程中就遵循一些規則來避免這類問題的發生是有必要的,結合本文的小例子,有如下建議:

  1. 根據需要決定使用Foreground或者Background線程。Foreground線程可以保證重要的工作在進程退出前有機會完成,更重要的,需要為包括Foreground線程在內的所有線程設定退出條件。
  2. 當需要使用Foreground線程時,Thread類型是最好的選擇,直接設置Thread.CurrentThread.IsBackground屬性是不推薦的方式。
  3. 程式退出時,應該手動釋放所有非托管資源,並且越關鍵的資源要越早釋放,如示例中的網路埠。
  4. 相對於在Finalize方法中釋放資源,Dispose模式是更好的方式。Dispose模式不依賴於垃圾回收,可以自主決定何時釋放不用的對象,而不是把釋放資源的壓力都集中在Finalize這一步驟。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 有些時候,有些作業遇到問題執行時間過長,因此我寫了一個腳本可以根據歷史記錄,找出執行時間過長的作業,在監控中就可以及時發現這些作業並儘早解決,代碼如下: SELECT sj.name , sja.start_execution_date,DATEDIFF (SECOND ,sja.start_exe...
  • 五、bash運算及啟動腳本01.使用bash的命令歷史#history……#set(顯示所有的變數) | grep HISHISTFILE=/root/.bash_historyHISTFILESIZE=1000(歷史文件個數)HISTSIZE=1000(文件的歷史大小)#vi /root/.bas
  • 序列化是將一個對象轉換成位元組流以達到將其長期保存在記憶體、資料庫或文件中的處理過程。它的主要目的是保存對象的狀態以便以後需要的時候使用。與其相反的過程叫做反序列化。 序列化一個對象 為了序列化一個對象,我們需要一個被序列化的對象,一個容納被序列化了的對象的(位元組)流和一個格式化器。進行序列化之前我們先
  • 註冊用戶有一段時間了,一直很忙,到現在還沒有寫一篇,忽然覺的一定要花點時間記錄和總結一些東西。好吧,就從這裡開始了。 今天客戶提出要點擊菜單(TreeView實現的)的父級節點時,展開節點。心想這個應該是很常見的功能吧,特意google了一下,發現大部分是將的不是js實現的,有些js實現的寫的麻煩,
  • 它們只是不起眼的小技巧。日積月累,它們讓我們的工作、學習更有效率,讓我們更加專註於邏輯本身,它們是.NET程式員的好朋友,它們是Visual Studio的小技巧……我們,真的認識它們嗎? 如果想儘快掌握這些技巧,請打開Visual Studio親自試一下這些技巧,希望找到你喜歡的技巧的。 (圖片來
  • 泛型(generic)是C#語言2.0和通用語言運行時(CLR)的一個新特性。泛型為.NET框架引入了類型參數(type parameters)的概念。類型參數使得設計類和方法時,不必確定一個或多個具體參數,其的具體參數可延遲到客戶代碼中聲明、實現。這意味著使用泛型的類型參數T,寫一個類MyList
  • 可選參數和命名參數 不多說,上代碼,自然懂 class Program { static void Main(string[] args) { var troy = new Troy(); troy.HelloWorld(1);//此時b和c都為0 troy.HelloWorld(1,2);//此時
  • 回到目錄 之前寫的一篇文章,主要針對View視圖,它可以放在N級目錄下,不必須非要在views/controller/action這種關係了,而在程式運行過程中,發現分頁視圖對本功能並不支持,原因很簡單,在RazorViewEngine有不同的屬於來修飾這兩個東西,對於View的查找,通過ViewL
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...