.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.Threading.ManualResetEvent
System.Xaml.XamlBackgroundReader


事件


内核对象


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
System.Drawing.BufferedGraphicsManager
System.Drawing.Bitmap


位图


GDI对象


System.Drawing.SolidBrush
System.Drawing.TextureBrush


画刷


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是如何工作的。

时间: 2024-10-18 02:27:22

.NET对象与Windows句柄(二):句柄分类和.NET句柄泄露的例子的相关文章

.NET对象与Windows句柄(三):句柄泄露实例分析

在上篇文章.NET对象与Windows句柄(二):句柄分类和.NET句柄泄露的例子中,我们有一个句柄泄露的例子.例子中多次创建和Dispose了DataReceiver和DataAnalyzer对象,但由于忘记调用DataAnalyzer的Stop方法,导致产生句柄泄露.本文假定我们已经发现了泄露现象但还不知道原因,讨论如何在这种情况下分析问题. 一.发现问题 在程序运行约一个小时以后,通过任务管理器发现句柄数超过5000,线程数也超过1000.对于一段只需要并行接收和分析数据的简易代码来说,这

.NET对象与Windows句柄(一):句柄的基本概念

在.NET编程中,得益于有效的内存管理机制,对象的创建和使用比较方便,大多数情况下我们无须关心对象创建和分配内存的细节,也可以放心的把对象的清理交给自动垃圾回收来完成.由于.NET类库对系统底层对象进行了封装,我们也不需要调用Windows API来操作非托管对象.但不直接操作非托管对象,并不意味着程序不会间接创建这些对象,如果不了解.NET对象与非托管资源的关系,我们很有可能因为不恰当的使用这些托管对象,而导致非托管资源泄露.本文尝试说明Windows对象和句柄的基本概念,以及.NET编程中的

Delphi对象变成Windows控件的前世今生(关键是句柄和回调函数)

----------------------------------------------------------------------第一步,准备工作:预定义一个全局Win控件变量,以及一个精简化的Win控件类var CreationControl: TWinControl = nil; // 定义全局变量,用来表示每次刚创建的Win控件 TWinControl = class(TControl) private FDefWndProc: Pointer; // 记录原有的窗口过程,但只有

Win32 Windows编程 二

一.第一个窗口程序 1  入口函数 WinMain 2  窗口处理函数 LRESULT CALLBACK WndProc( HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam ) { return DefWindowProc( hWnd, nMsg, wParam, lParam ); } 当窗口处理消息事件时 调用该函数 LRESULT CALLBACK WndProc( HWND hWnd, UINT nMsg, WPARAM wParam,

Windows Forms(二)

导读 1.用VS创建一个Windows Forms程序 2.分析上面的程序 3.Mediator pattern(中介者模式) 4.卡UI怎么办——BackgroundWorker组件 用VS创建一个Windows Forms程序 博主应为项目需要用的VS2005,所以这个系列的Windows Forms 特指Windows Forms 2.0. 打开VS2005 –> 建立 Windows应用程序,过程截图如下(其实这种简单的步骤,做C#开发都会) 拖拽一个Button 和 TextBox 控

PHP 类与对象 全解析( 二)

目录 PHP 类与对象 全解析( 一) PHP 类与对象 全解析( 二) PHP 类与对象 全解析(三 ) 7.Static关键字 声明类成员或方法为static,就可以不实例化类而直接访问.不能通过一个对象来访问其中的静态成员(静态方法除外). 静态成员属于类,不属于任何对象实例,但类的对象实例都能共享. 小结: 在类内部访问静态成员属性或者方法,使用 self::(没有 $ 符号),如:  self:: $country  //类内部访问静态成员属性  self:: myCountry()

【Hibernate步步为营】--核心对象+持久对象全析(二)

上篇文章讨论了Hibernate的核心对象,在开发过程中经常用到的有JTA.SessionFactory.Session.JDBC,其中SessionFactory可以看做数据库的镜像,使用它能够创建Session对象,JTA用来管理事务,在对象模型修改后同步到数据库中,另外还有Hibernate作为持久层它封装了持久层的转化过程,下面着重讨论持久对象的转换过程. 一.状态解析 Hibernate的持久对象主要分为三个状态,Transient.Persistent.Detached,其中Tran

C++ 在Windows下截取整个屏幕 和 指定句柄窗口的屏幕

#include <windows.h> #include <stdint.h> #include <stdio.h> void ShootScreen(const char* filename, HWND hWnd) { HDC hdc = CreateDC("DISPLAY", NULL, NULL, NULL); int32_t ScrWidth = 0, ScrHeight = 0; RECT rect = { 0 }; if (hWnd =

Windows服务二:测试新建的服务、调试Windows服务

一.测试Windows服务 为了使Windows服务程序能够正常运行,我们需要像创建一般应用程序那样为它创建一个程序的入口点.像其他应用程序一样,Windows服务也是在Program.cs的Main()函数中完成这个操作.首先我们在Main()函数中创建一个Windows服务的实例,该实例应该是ServiceBase类的某个子类的对象,然后我们调用由基类ServiceBase类定义的一个Run()方法.然而调用Run()方法并不意味着就开始了Windows服务程序,必须要等到该对象的OnSta