在平时使用软件或是.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、让进程更快退出的几个编程建议
严格来讲,进程延迟退出并无致使任何新问题的产生,只是暴露了代码里本来已经存在的缺陷,这些缺陷几乎都与资源的使用和释放不当有关。当代码中有完善且恰到好处的错误日志时,这些问题或许很快就能被定位和解决,而在另外一些状况下可能要花费一些周折才能找到根源所在。所以在平时的编程中就遵循一些规则来避免这类问题的发生是有必要的,结合本文的小例子,有以下建议: