程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET對象與Windows句柄(二):句柄分類和.NET句柄洩露的例子

.NET對象與Windows句柄(二):句柄分類和.NET句柄洩露的例子

編輯:關於.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是如何工作的。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved