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

相關文章
相關標籤/搜索