.NET对象与Windows句柄(二):句柄分类和.NET句柄泄露的例子
上一篇文章介绍了句柄的基本概念,也描述了C#中创建文件句柄的过程。我们已经知道句柄代表Windows内部对象,文件对象就是其中一种,但显然系统中还有更多其它类型的对象。本文将简单介绍Windows对象的分类。
句柄可以代表的Windows对象分为三类,内核对象(Kernel Object)、用户对象(GDI Object)和GDI对象,上一篇文章中任务管理器中的“句柄数”、“用户对象”和“GDI对象”计数就是与这几类对象对应的。为什么要这样分类呢?原因就在于这几类对象对于操作系统而言有不同的作用,管理和引用的方式也不同。内核对象主要用于内存管理、进程执行以及进程间通信,用户对象用于系统的窗口管理,而GDI对象用来支持图形界面。
一、观察句柄变化的小实验
在列举Windows对象的分类之前,我们再看一个关于句柄数量的实验,与之前文件对象的句柄不同,本例中的句柄属于用户对象。程序运行过程中,对象的创建和销毁是动态进行的,句柄数量也随之动态变化,即使是一个最简单的Windows Form程序也可以直观的反映这一点。下图是一个只有文本框和按钮的窗体程序,程序启动后默认输入焦点在文本框上,可以按下Tab键将焦点在文本框和按钮之间交替切换。当我们这样做时,在任务管理器中可以看到:用户对象的数量在21和20之间不断变化。这一数字在你的运行环境中可能不同,但至少说明在焦点切换过程中有一个用户对象在不断的被创建销毁,这个对象就是Caret(插入符号)。
Caret是用户对象的一种,这个闪烁的光标指示输入的位置。我们可以通过Windows API创建这个符号,定制它的样式,也可以设置闪烁时间。创建Caret时,Windows API并不返回它的句柄,原因是一个窗口只能显示一个插入符号,可以通过窗口的句柄对它进行访问,或者更简单的,看哪个线程在调用这些API即可。但无论如何,Caret对象和其句柄是真实存在的,即便我们不需要获取这个句柄。
二、Windows对象的分类
前面提到了Windows对象分为内核对象、用户对象和GDI对象,也举了文件对象和Caret对象的例子,除此之外还有很多其它类型的对象。Windows对象的完整列表,可以参考MSDN中关于Object Categories (Windows) 的描述,其中列举了每个类别的对象,并且针对每种对象都有详细的说明,你可以从中找到这些对象的用法,和对应的Windows API等。本文主要讨论.NET对象和Windows对象的关系,因此在这里只简单列举这些对象以供快速参考。
内核对象:访问令牌、更改通知、通信设备、控制台输入、控制台屏幕缓冲区、桌面、事件、事件日志、文件、文件映射、堆、作业、邮件槽、模块、互斥量、管道、进程、信号量、套接字、线程、定时器、定时器队列、定时器队列定时器、更新资源和窗口站。
用户对象:加速键表、插入符号、光标、动态数据交换会话、钩子、图标、菜单、窗口和窗口位置。
GDI对象:位图、画刷、设备上下文、增强型图元文件、增强型图元文件设备上下文、字体、内存设备上下文、图元文件、图元文件设备上下文、调色板、画笔和区域。
如前所述,不同类别的对象具有不同的作用和特点。内核对象主要用于内存管理、进程执行以及进程间通信。多个进程可以共用同一个内核对象(如文件和事件),但每个进程必须独自创建或打开这个对象以获取自己的句柄,并指定不同的访问权限,这种情况下,一个内核对象会被多个进程的句柄引用;用户对象用于系统的窗口管理,与内核对象不同的是,一个用户对象仅能有一个句柄,但句柄是对其它进程公开的,因此其它进程可以获取并使用这个句柄来访问用户对象。以窗口(Windows)对象为例,一个进程可以获取另一个进程创建的窗口对象的句柄,并向其发送各种消息,这也是很多自动化测试工具得以实现的前提;而GDI对象用来支持图形界面,也只支持单个对象单个句柄,但与用户对象不同的是,GDI对象的句柄是进程私有的。
三、与Windows对象对应的.NET对象
.NET中有不少类型封装了上面所列举Windows对象,我们在使用时要特别注意对这些对象的进行重用和适时销毁。下表是一些对应关系的例子(注意这不是完整列表,也并非严格的一一对应关系),后续文章将会讨论其中一些重要类型的用法。
.NET对象 |
引用到的Windows对象句柄 |
分类 |
System.Threading.Tasks.Task |
访问令牌 |
内核对象 |
System.IO.FileSystemWatcher |
更改通知 |
内核对象 |
System.IO.FileStream |
文件 |
内核对象 |
System.Threading.AutoResetEvent |
事件 |
内核对象 |
System.Diagnostics.EventLog |
事件日志 |
内核对象 |
System.Threading.Thread |
线程 |
内核对象 |
System.Threading.Mutex |
互斥量 |
内核对象 |
System.Threading.Semaphore |
信号量 |
内核对象 |
System.Windows.Forms.Cursor |
光标 |
用户对象 |
System.Drawing.Icon |
图标 |
用户对象 |
System.Windows.Forms.Menu |
菜单 |
用户对象 |
System.Windows.Forms.Control |
窗口 |
用户对象 |
System.Windows.Forms.Control |
位图 |
GDI对象 |
System.Drawing.SolidBrush |
画刷 |
GDI对象 |
System.Drawing.Font |
字体 |
GDI对象 |
四、.NET中与句柄泄露相关的异常和现象
上一篇文章提到了句柄的限制,当进程或系统的句柄数量达到上限时,程序运行就会出现异常。常见的错误是System.ComponentModel.Win32Exception的“Error creating window handle”,或者“存储空间不足,无法处理此命令”等,错误出现时内存往往也会有显著增长。如果是达到了系统级别的句柄上限,其它程序的运行也受到影响,系统可能无法打开任何新的菜单和窗口、窗口也会出现绘制不完整的情况。这时及时抓取Dump并终止泄露句柄的进程,系统往往立即恢复正常。
五、第一个句柄泄露的例子
下面的示例代码包含句柄泄露的问题,为了演示方便,实现代码被最简单化,设计的合理性也暂且不作深究。代码模拟了一个应用场景:程序包含一个DataReceiver不断从某个数据源获取实时数据,DataReceiver同时会启动一个DataAnalyzer,定时分析这些数据。设想程序有一个专门的子窗口来显示这些数据,当子窗口被临时关闭时,数据的实时获取和分析过程也可以暂时终止。程序长时间运行的过程中,子窗口可能被用户多次关闭和打开,因此DataReceiver会被创建多次,程序启动后的代码模拟DataReceiver被创建和Dispose了1000次。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Timer = System.Threading.Timer; namespace LeakExample
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent(); // 模拟程序运行过程中多次创建DataReceiver的情况
Task.Factory.StartNew(() => {
for (int i = 0; i < 1000; i++)
{
using (IDisposable receiver = new DataReceiver())
{
Thread.Sleep(100);
}
}
});
}
} public class DataReceiver : IDisposable
{
private Timer dataSyncTimer = null;
private IAnalyzer analyzer = null;
private bool isDisposed = false; public DataReceiver() : this(new DataAnalyzer()) { } public DataReceiver(IAnalyzer dataAnalyzer)
{
dataSyncTimer = new Timer(GetData, null, 0, 500);
analyzer = dataAnalyzer; analyzer.Start();
} private void GetData(object state)
{
// 获取数据并放入缓存
} public void Dispose()
{
if (isDisposed)
return; if (dataSyncTimer != null)
{
dataSyncTimer.Dispose();
} isDisposed = true;
}
} public interface IAnalyzer
{
void Start();
void Stop();
} public class DataAnalyzer : IAnalyzer
{
private Timer analyzeTimer = null; public void Start()
{
analyzeTimer = new Timer(DoAnalyze, null, 0, 1000);
} public void Stop()
{
if (analyzeTimer != null)
{
analyzeTimer.Dispose();
}
} private void DoAnalyze(object state)
{
// 从缓存中取得数据并分析,耗时600毫秒
Thread.Sleep(600);
}
}
}
当运行这段程序时,可以从任务管理器观察到句柄数持续增长,最终基本稳定在某一个较高的数字。虽然DataReceiver被多次创建,但句柄数的增长最终远远超过其被创建的次数。由于代码简单,你很可能已经看出问题所在,然而在实际的项目中,由于软件架构和业务逻辑代码更为复杂,很难一眼就看出问题的根源。下一篇文章将从这个例子入手,结合一些工具来分析问题存在的原因,并讨论Timer是如何工作的。
.NET对象与Windows句柄(二):句柄分类和.NET句柄泄露的例子的更多相关文章
- .NET对象与Windows句柄(三):句柄泄露实例分析
在上篇文章.NET对象与Windows句柄(二):句柄分类和.NET句柄泄露的例子中,我们有一个句柄泄露的例子.例子中多次创建和Dispose了DataReceiver和DataAnalyzer对象, ...
- .NET对象与Windows句柄(一):句柄的基本概念
在.NET编程中,得益于有效的内存管理机制,对象的创建和使用比较方便,大多数情况下我们无须关心对象创建和分配内存的细节,也可以放心的把对象的清理交给自动垃圾回收来完成.由于.NET类库对系统底层对象进 ...
- 【C# 线程】转载 句柄的基本概念 .NET对象与Windows句柄
转载自:https://www.cnblogs.com/silverb/p/5300255.html 句柄的基本概念 1.句柄就是进程句柄表中的索引.2.句柄是对进程范围内一个内核对象地址的引用,一个 ...
- Delphi对象变成Windows控件的前世今生(关键是设置句柄和回调函数)goodx
----------------------------------------------------------------------第一步,准备工作:预定义一个全局Win控件变量,以及一个精简 ...
- Windows Forms(二)
导读 1.用VS创建一个Windows Forms程序 2.分析上面的程序 3.Mediator pattern(中介者模式) 4.卡UI怎么办——BackgroundWorker组件 用VS创建一个 ...
- 从普通函数到对象方法 ------Windows窗口过程的面向对象封装
原文地址:http://blog.csdn.net/linzhengqun/article/details/1451088 从普通函数到对象方法 ------Windows窗口过程的面向对象封装 开始 ...
- PHP 类与对象 全解析( 二)
目录 PHP 类与对象 全解析( 一) PHP 类与对象 全解析( 二) PHP 类与对象 全解析(三 ) 7.Static关键字 声明类成员或方法为static,就可以不实例化类而直接访问.不能通过 ...
- C++ 在Windows下截取整个屏幕 和 指定句柄窗口的屏幕
#include <windows.h> #include <stdint.h> #include <stdio.h> void ShootScreen(const ...
- XAML实例教程系列 - 对象和属性(二)
XAML实例教程系列 - 对象和属性 2012-05-22 14:18 by jv9, 1778 阅读, 6 评论, 收藏, 编辑 在前一篇已经介绍XAML概念:“XAML语言是Extensible ...
随机推荐
- [LintCode] Word Break
Given a string s and a dictionary of words dict, determine if s can be break into a space-separated ...
- 简单几何(求凸包点数) POJ 1228 Grandpa's Estate
题目传送门 题意:判断一些点的凸包能否唯一确定 分析:如果凸包边上没有其他点,那么边想象成橡皮筋,可以往外拖动,这不是唯一确定的.还有求凸包的点数<=2的情况一定不能确定. /********* ...
- HTML的表单元素
html的表单元素主要用于让用户输入数据并提交给服务器 基本语法:<form action="url" method="提交的方法,get/post,默认为get& ...
- datetime与smalldatetime之间的区别
1.一直以为smalldatetime和datetime的差别只是在于时间范围: smalldatetime的有效时间范围1900/1/1~2079/6/6datetime的有效时间范围1753/1/ ...
- 转载关于KeyPress和KeyDown事件的区别和联系
KeyDown:在控件有焦点的情况下按下键时发生. KeyPress:在控件有焦点的情况下按下键时发生. KeyUp:在控件有焦点的情况下释放键时发生. 1.KeyPress主要用来接收字母.数字等A ...
- 理解cookie的path和domain属性
今天在做验证码时发现一个问题:A.B窗口都打开同一个页面,A先生成一个验证码,B再生成验证码,这时A所生成的验证码被B覆盖掉了.原因是使用了同名的cookie来存储验证码.一时找不到解决方法就参考了W ...
- 常用移动web开发框架研究分析
纯粹的总结一下移动web开发框架,移动web开发框架有jQuery Mobile .Sencha Touch等等,他们都来源于web开发,是成熟的框架,jQuery Mobile出自于jQuery家族 ...
- Redis中7种集合类型应用场景
StringsStrings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字.使用Strings类型,你可以完全实现目前 Memcached 的功能,并且效率更 ...
- 关于EnumerateObjectsUsingBlock和for-in之间的较量
遍历一个数组看谁快 参赛选手 ForLoop, For - in, enumerateObjectsUsingBlock这个三个方法: NSMutableArray *test = [NSMuta ...
- mysql5.6安装
mysql5.6安装 #卸载原有的mysqlyum remove mysql*ls /etc/my.cnf*mv /etc/my.cnf* /tmp/ #安装依赖包yum install make c ...