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

上一篇文章介绍了句柄的基本概念,也描述了C#中建立文件句柄的过程。咱们已经知道句柄表明Windows内部对象,文件对象就是其中一种,但显然系统中还有更多其它类型的对象。本文将简单介绍Windows对象的分类。windows

句柄能够表明的Windows对象分为三类,内核对象(Kernel Object)、用户对象(GDI Object)和GDI对象,上一篇文章中任务管理器中的“句柄数”、“用户对象”和“GDI对象”计数就是与这几类对象对应的。为何要这样分类呢?缘由就在于这几类对象对于操做系统而言有不一样的做用,管理和引用的方式也不一样。内核对象主要用于内存管理、进程执行以及进程间通讯,用户对象用于系统的窗口管理,而GDI对象用来支持图形界面。缓存

1、观察句柄变化的小实验架构

在列举Windows对象的分类以前,咱们再看一个关于句柄数量的实验,与以前文件对象的句柄不一样,本例中的句柄属于用户对象。程序运行过程当中,对象的建立和销毁是动态进行的,句柄数量也随之动态变化,即便是一个最简单的Windows Form程序也能够直观的反映这一点。下图是一个只有文本框和按钮的窗体程序,程序启动后默认输入焦点在文本框上,能够按下Tab键将焦点在文本框和按钮之间交替切换。当咱们这样作时,在任务管理器中能够看到:用户对象的数量在21和20之间不断变化。这一数字在你的运行环境中可能不一样,但至少说明在焦点切换过程当中有一个用户对象在不断的被建立销毁,这个对象就是Caret(插入符号)。工具

Caret是用户对象的一种,这个闪烁的光标指示输入的位置。咱们能够经过Windows API建立这个符号,定制它的样式,也能够设置闪烁时间。建立Caret时,Windows API并不返回它的句柄,缘由是一个窗口只能显示一个插入符号,能够经过窗口的句柄对它进行访问,或者更简单的,看哪一个线程在调用这些API便可。但不管如何,Caret对象和其句柄是真实存在的,即使咱们不须要获取这个句柄。测试

 

2、Windows对象的分类字体

前面提到了Windows对象分为内核对象、用户对象和GDI对象,也举了文件对象和Caret对象的例子,除此以外还有不少其它类型的对象。Windows对象的完整列表,能够参考MSDN中关于Object Categories (Windows) 的描述,其中列举了每一个类别的对象,而且针对每种对象都有详细的说明,你能够从中找到这些对象的用法,和对应的Windows API等。本文主要讨论.NET对象和Windows对象的关系,所以在这里只简单列举这些对象以供快速参考。this

内核对象:访问令牌、更改通知、通讯设备、控制台输入、控制台屏幕缓冲区、桌面、事件、事件日志、文件、文件映射、堆、做业、邮件槽、模块、互斥量、管道、进程、信号量、套接字、线程、定时器、定时器队列、定时器队列定时器、更新资源和窗口站。spa

用户对象:加速键表、插入符号、光标、动态数据交换会话、钩子、图标、菜单、窗口和窗口位置。操作系统

GDI对象:位图、画刷、设备上下文、加强型图元文件、加强型图元文件设备上下文、字体、内存设备上下文、图元文件、图元文件设备上下文、调色板、画笔和区域。线程

如前所述,不一样类别的对象具备不一样的做用和特色。内核对象主要用于内存管理、进程执行以及进程间通讯。多个进程能够共用同一个内核对象(如文件和事件),但每一个进程必须独自建立或打开这个对象以获取本身的句柄,并指定不一样的访问权限,这种状况下,一个内核对象会被多个进程的句柄引用;用户对象用于系统的窗口管理,与内核对象不一样的是,一个用户对象仅能有一个句柄,但句柄是对其它进程公开的,所以其它进程能够获取并使用这个句柄来访问用户对象。以窗口(Windows)对象为例,一个进程能够获取另外一个进程建立的窗口对象的句柄,并向其发送各类消息,这也是不少自动化测试工具得以实现的前提;而GDI对象用来支持图形界面,也只支持单个对象单个句柄,但与用户对象不一样的是,GDI对象的句柄是进程私有的。

3、与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对象

 

4、.NET中与句柄泄露相关的异常和现象

上一篇文章提到了句柄的限制,当进程或系统的句柄数量达到上限时,程序运行就会出现异常。常见的错误是System.ComponentModel.Win32Exception的“Error creating window handle”,或者“存储空间不足,没法处理此命令”等,错误出现时内存每每也会有显著增加。若是是达到了系统级别的句柄上限,其它程序的运行也受到影响,系统可能没法打开任何新的菜单和窗口、窗口也会出现绘制不完整的状况。这时及时抓取Dump并终止泄露句柄的进程,系统每每当即恢复正常。

5、第一个句柄泄露的例子

下面的示例代码包含句柄泄露的问题,为了演示方便,实现代码被最简单化,设计的合理性也暂且不做深究。代码模拟了一个应用场景:程序包含一个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是如何工做的。