在平时使用软件或是.NET程序开发的过程中,我们有时会遇到程序关闭后但进程却没有退出的情况,这往往预示着代码中有问题存在,不能正确的在程序退出时停止代码执行和销毁资源。这个现象有时并不容易被察觉,但在另一些情况下却会产生影响软件功能的Bug。本文列举可能影响.NET程序进程退出的因素,并用几个小例子说明这些因素如何导致Form Application和Windows Service的Bug。

一、进程不能退出对于某些Windows Form程序的影响

在传统C/S结构的系统中,客户端会通过Socket或WCF服务利用特定的端口与服务端保持通信。因此在很多应用场景中,为避免端口冲突,单台计算机同一时刻只允许启动一个客户端,这也符合一个客户端代表单个用户角色的业务设计。这可以通过Mutex类,或者在客户端启动时检查是否已有同名的进程存在来实现。有些客户端启动逻辑被设计成当存在已有进程时,不初始化用户界面,而是自动切换到已经打开的客户端并关闭自身。

在这种情况下,如果前一次从客户端界面中退出,但是进程没有关闭,那随后再次启动客户端时就再也无法正常显示出用户界面,除非手动杀掉进程再次启动。

二、Foreground线程导致进程无法退出的例子

用如下代码来模拟进程无法退出的情况。简单起见,这个小窗口程序没有任何网络或数据库操作,仅仅是用一个线程定时刷新UI。设想是当程序界面构建完成后启动一个Thread,随后每隔1秒刷新当前时间,当点击窗体关闭按钮之后,程序退出,Thread和进程一同被销毁。

 public partial class Form1 : Form
{
Thread worker = null; public Form1()
{
InitializeComponent();
Load += new EventHandler(Form1_Load);
} void Form1_Load(object sender, EventArgs e)
{
worker = new Thread(new ThreadStart(DoWork));
worker.Start();
} private void DoWork()
{
while (true)
{
Thread.Sleep();
if (IsHandleCreated && !IsDisposed)
{
Invoke((MethodInvoker)(() => label1.Text = DateTime.Now.ToString()));
}
}
}
}

在关闭窗体之后,实际的运行结果却是,用户看不到任何界面,但进程一直停留在任务管理器中,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线程,那需要做的就是给线程的执行代码一个退出条件,让它在恰当的时候优雅的停止,而非无休止的运行下去。可以设置一个变量指示主窗口是否正在退出,再由线程定期检查这个变量,决定是否结束。

 public partial class Form1 : Form
{
Thread worker = null;
bool isClosing = false; public Form1()
{
InitializeComponent(); worker = new Thread(new ThreadStart(DoWork));
worker.Start();
} private void DoWork()
{
while (!isClosing)
{
Thread.Sleep();
if (IsHandleCreated && !IsDisposed)
Invoke((MethodInvoker)(() => label1.Text = DateTime.Now.ToString()));
}
} protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
isClosing = true;
}
}

五、Foreground导致Windows Service进程延迟退出

对于Windows Service程序来讲,Foreground线程仍然会阻止Service进程的退出,但是情况稍有不同。一段最简单的Service程序代码如下,服务启动代码写在OnStart方法中,创建了一个线程对象循环执行任务,OnStop方法会在服务停止时被调用,这里假设需要5秒钟时间运行资源清理代码。

 public partial class Service1 : ServiceBase
{
Thread worker; public Service1()
{
InitializeComponent();
} protected override void OnStart(string[] args)
{
worker = new Thread(new ThreadStart(DoWork));
worker.Start();
} protected override void OnStop()
{
// Clean up resources.
Thread.Sleep();
} private void DoWork()
{
while (true)
{
// Time consuming work task.
Thread.Sleep();
}
}
}

在服务中停止这个名为“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秒时间被终止……

 public partial class Service1 : ServiceBase
{
TypeA objectA = null; public Service1()
{
InitializeComponent();
} protected override void OnStart(string[] args)
{
objectA = new TypeA(); TcpListener listener1 = new TcpListener(IPAddress.Parse("127.0.0.1"), );
listener1.Start();
} protected override void OnStop()
{
// Clean up resources.
Thread.Sleep();
}
} public class TypeA
{
~TypeA()
{
// Clean up resources.
Thread.Sleep();
}
}

现在,启动这个服务,再停止这个服务,然后再次启动,虽然Finalize方法导致进程退出晚了两秒,但到目前为止并没有造成任何麻烦。然而当想要尝试“重新启动”这个服务的时却得到了“本地计算机上的服务启动后停止”的提示,服务无法启动成功。

检查事件查看器,我们可以很快发现问题出在对网络端口的争用上。在用户尝试“重新启动”时,服务控制器仅仅是简单的停止并启动服务。停止的时候,完成OnStop方法需要5秒钟,之后控制器认为服务停止过程已完成(实际上也确实如此),再次启动服务,并开始监听同一网络端口。但这时前一次停止的服务进程还没有完全退出,端口也没有释放,因此新的进程打开这一端口就产生了SocketException。

八、让进程更快退出的几个编程建议

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

  1. 根据需要决定使用Foreground或者Background线程。Foreground线程可以保证重要的工作在进程退出前有机会完成,更重要的,需要为包括Foreground线程在内的所有线程设定退出条件。
  2. 当需要使用Foreground线程时,Thread类型是最好的选择,直接设置Thread.CurrentThread.IsBackground属性是不推荐的方式。
  3. 程序退出时,应该手动释放所有非托管资源,并且越关键的资源要越早释放,如示例中的网络端口。
  4. 相对于在Finalize方法中释放资源,Dispose模式是更好的方式。Dispose模式不依赖于垃圾回收,可以自主决定何时释放不用的对象,而不是把释放资源的压力都集中在Finalize这一步骤。

是什么在.NET程序关闭时阻碍进程的退出?的更多相关文章

  1. qt 单文档程序关闭时在delete ui处出现segmentation fault

    做了个显示图片的单文档程序. qt 单文档程序关闭时在delete ui处出现segmentation fault. 调试发现调用两次mainwindow析构函数. http://blog.csdn. ...

  2. indy9在程序关闭时出现terminate thread timeout的BUG解决办法

    indy9在程序关闭时出现terminate thread timeout的BUG解决办法 INDY9线程有BUG,在退出程序的时候会报错:terminate thread timeout(终止线程超 ...

  3. Runtime.getRuntime().addShutdownHook(Thread thread) 程序关闭时钩子,优雅退出程序

    根据 Java API, 所谓 shutdown hook 就是已经初始化但尚未开始执行的线程对象.在Runtime 注册后,如果JVM要停止前,这些 shutdown hook 便开始执行.也就是在 ...

  4. QSettings 使用实例 当需要在程序关闭时保存”状态“信息

    用户对应用程序经常有这样的要求:要求它能记住它的settings,比如窗口大小,位置,一些别的设置,还有一个经常用的,就是recent files,等等这些都可以通过Qsettings来实现. 我们知 ...

  5. 黑马程序员 关于c# windows窗体关闭时线程未能完全退出问题(专题一)

    <a href="http://edu.csdn.net"target="blank">ASP.Net+Android+IO开发S</a> ...

  6. 小程序关闭时暂停webview里的音乐

    document.addEventListener("visibilitychange", () => {  if(document.hidden) {     // 页面被 ...

  7. C# 程序异常关闭时的捕获

    本文主要以一个简单的小例子,描述C# Winform程序异常关闭时,如何进行捕获,并记录日志. 概述 有时在界面的事件中,明明有try... catch 进行捕获异常,但是还是会有异常关闭的情况,所以 ...

  8. VC++ 实现VC程序启动时最小化到任务栏(完美解决闪烁问题)

    之前写的一个VC应用程序,是程序启动时就直接出现在任务栏, 窗体不出现,等用户点击任务栏图标再出现窗口.和一些防火墙什么的软件类似. 这种效果实现并不是很困难的,硬是找不到最好的.为什么呢? 首先,在 ...

  9. 编写SqlHelper使用,在将ExecuteReader方法封装进而读取数据库中的数据时会产生Additional information: 阅读器关闭时尝试调用 Read 无效问题,解决方法与解释

    在自学杨中科老师的视频教学时,拓展编写SqlHelper使用,在将ExecuteReader方法封装进而读取数据库中的数据时 会产生Additional information: 阅读器关闭时尝试调用 ...

随机推荐

  1. 水题 Codeforces Round #296 (Div. 2) A. Playing with Paper

    题目传送门 /* 水题 a或b成倍的减 */ #include <cstdio> #include <iostream> #include <algorithm> ...

  2. c# windows service

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  3. BZOJ3630 : [JLOI2014]镜面通道

    从左边不能到达右边当且仅当存在一条与上下底边相连的分割线将它们分开 设下底边为S,上底边为T,每个元件作为点,有公共部分的两个点互相连边 最后拆点求最小割 #include<cstdio> ...

  4. TYVJ P1020 导弹拦截 Label:水

    题目描述 某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度.某天,雷达捕捉到敌国的导弹 ...

  5. Google Code Jam 2010 Round 1B Problem B. Picking Up Chicks

    https://code.google.com/codejam/contest/635101/dashboard#s=p1   Problem A flock of chickens are runn ...

  6. PHP中有关Session的函数比较多,最常用到的也就这么几个函数

    php中的cookie与session技术详解 一.cookie介绍 cookie常用于识别用户.cookie是服务器留在用户计算机中的小文件.每当相同的计算机通过浏览器请求页面时,它同时会发送coo ...

  7. 增加Activity Monitor中的作业保存数量

    在Master Server的注册表中加入如下两个键值即可: (1500的单位是小时)  

  8. 在CSV文件中增加一列属性值

    具体参见:系统管理\将文件夹复制到列表中的远程主机   修改前: column1, column2 1,b 2,c 3,5   修改后: column1, column2, column3 1,b, ...

  9. debug阶段工作期站立会议2(进度推进)

    组名:天天向上 组长:王森 组员:张政.张金生.林莉.胡丽娜 代码地址:HTTPS:https://git.coding.net/jx8zjs/llk.git SSH:git@git.coding.n ...

  10. iOS segue 跳转

    场景描述: 要实现在tableViewController 的界面A里,点击一个cell ,跳转到第二个viewController的界面B .在第二个界面里做相应操作. 我的做法,利用sb,在A 里 ...