在平时使用软件或是.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秒钟时间运行资源清