第13章 诊断
13.1 条件编译
预编译的指令见 4.16 预处理指令,我们这里的条件编译用到的指令有:
-
#if
、#else
、#endif
、#elif
条件编译指令可以进行 与
&&
、 或 ||
、 非 !
运算。
预定义指令可以通过三种方式定义:
- 在文件中通过
#define
定义 Symbol - 编译时传递 /define 参数
csc Program.cs /define:TESTMODE,PLAYMODE
- Visual Studio 在项目属性中定义编译 Symbol
13.1.2 Conditional
特性
Conditional
特性用法如下:
[Conditional ("LOGGINGMODE")]
static void LogStatus (string msg)
{string logFilePath = ...System.IO.File.AppendAllText (logFilePath, msg + "\r\n");
}
Conditional
特性指示编译器在所有调用 LogStatus
的地方使用 #if LOGGINGMODE
进行包装,如果 LOGGINGMODE
没有定义,则 LogStatus
的调用在编译时将完全忽略,包括参数表达式的计算。即使 LogStatus
和调用者 不在同一程序集 该机制仍然有效。
C7.0 核心技术指南 第7版.pdf - p578 - C7.0 核心技术指南 第 7 版-P578-20240214151621
#if
相较Conditional
的不足:C7.0 核心技术指南 第7版.pdf - p577 - C7.0 核心技术指南 第 7 版-P577-20240214151902
13.2 Debug
和 Trace
类
Debug
和 Trace
类的方法都被 Conditional
进行了标注,区别如下:
- 所有
Debug
类中的方法都标记为 [Conditional("DEBUG")]
。 - 所有
Trace
类中的方法都标记为 [Conditional("TRACE")]
。
默认情况下,VS 在项目的 debug 配置中定义了 ** DEBUG ** 和 ** TRACE ** 符号,在 release 中仅定义了 ** TRACE ** 符号。
13.2.1 Fail
和 Assert
方法
Debug
和 Trace
类型都提供了 Fail
和 Assert
方法,区别如下:
-
Fail
调用时将弹出对话框,询问“忽略”、“终止”还是“重试”。
-
Assert
当
Assert
中的表达式返回false
(意味着程序缺陷),将会自动调用Fail
方法。
从技术上说,根据条件抛出异常也是一种断言,区别如下:
- 抛出异常:反映了 调用者代码 缺陷
-
Assert
:反映了 当前方法的 缺陷
13.2.2 TraceListener
类
Debug
和 Trace
类的 Listeners
属性是 TraceListener
实例的静态集合。它们负责处理 Write
、Fail
和 Trace
方法触发的信息。
默认情况下,该集合包含一个单独的监听器( DefaultTraceListener
)。可以手动添加自定义监听器、移除默认监听器,以改变监听行为。既可以从零开始编写 TraceListener
(从 TraceListener
继承),也可使用以下预定义的监听器类型:
-
TextWriterTraceListener
:将消息写入Stream
或者TextWriter
,或将消息追加到文件中。
TextWriterTraceListener
类还进一步的划分为了:-
ConsoleTraceListener
、 -
DelimitedListTraceListener
、 -
XmlWriterTraceListener
-
EventSchemaTraceListener
.
-
-
EventLogTraceListener
:将事件日志写入到 Windows 事件日志中。 -
EventProviderTraceListener
:将事件写入操作系统的 Windows 事件追踪(EventTracingforWindows,ETW)子系统中(支持 WindowsVista 及其之后的操作系统)。 -
WebPageTraceListener
:写入某个 ASP.NET 页面。
C7.0 核心技术指南 第7版.pdf - p581 - C7.0 核心技术指南 第 7 版-P581-20240215094613
提纲
DefaultTraceListener
这个默认的监听器有两个关键特性:
- 当连接到调试器时(例如 Visual Studio 的调试器),将消息输出到 调试输出窗口 ;否则忽略消息内容。
- 当调用
Fail
方法时(或Assert
失败时),不论是否附加了调试器,都 弹出对话框询问用户是继续、忽略还是重试(附加/调试) 。
EventLogTraceListener
该监听器将信息输出至 Windows 事件日志 ,不同的方法将输出不同类型的日志:
-
信息:
Write
、Fail
、Assert
-
警告:
TraceWarning
-
错误:
TraceError
使用方式
Trace.Listeners.Clear();
Trace.Listeners.Add(new TextWriterTraceListener("D:\\trace.txt"));
TextWriter tw = Console.Out;
Trace.Listeners.Add(new TextWriterTraceListener(tw));
// 如下代码需用管理员权限运行
if (!EventLog.SourceExists("WinFormsApp1"))EventLog.CreateEventSource("WinFormsApp1", "Application");
Trace.Listeners.Add(new EventLogTraceListener("WinFormsApp1"));
TraceListener
的属性
TraceListener
有如下属性进行自定义行为:
-
Filter
属性
TraceFilter
类型,消息过滤器。可以使用其预定义子类(例如EventTypeFilter
、SourceFilter
),也可以派生TraceFilter
并重写ShouldTrace
方法。 -
IndentLevel
、IndentSize
属性用于 控制缩进 。
-
TraceOutputOptions
属性可以用来 写入额外的数据 ,调用
Trace
方法时会应用TraceOutputOptions
中的配置:TextWriterTraceListener tl = new TextWriterTraceListener (Console.Out); tl.TraceOutputOptions = TraceOptions.DateTime | TraceOptions.Callstack; ... Trace.TraceWarning("Orange alert"); // 输出: // DiagTest.vshost.exe Warning: O : Orange alert // DateTime=2007-03-08T05:57:13.6250000Z // Callstack= at System.Environment.GetStackTrace(Exception e,BooleanneedFileInfo) // at System.Environment.get_StackTrace() at..
13.2.3 刷新并关闭监听器
TextWriterTraceListener
会先缓存再写入,需要适时关闭或刷新监听器。方法有三:
-
使用
Debug(Trace).Flush
方法刷新监听器 -
关闭程序时调用
Debug(Trace).Close
方法该方法会隐式调用
Flush
方法,并关闭文件句柄。 -
设置
Debut(Trace).AutoFlush
属性为 true每条消息后强制执行
Flush
方法。
C7.0 核心技术指南 第7版.pdf - p582 - C7.0 核心技术指南 第 7 版-P582-20240215171006
13.3 调试器的集成
13.3.1 附加和断点
System.Diagnostics
命名空间中的静态 Debugger
类提供了与调试器交互的基本函数:
-
Break
断点。启动调试器并将进程附加在调试器上,并在当前执行点挂起。
-
Launch
除挂起外,和
Break
无区别。 -
Log
向调试器窗口输出信息。
-
IsAttached
是否已有调试器附加到了当前应用程序上。
13.3.2 Debugger
特性
DebuggerStepThrough
和 DebuggerHidden
特性可以应用于方法、构造器、和类。
-
DebuggerStepThrough
允许调试器在 单步执行 时跳过某些代码,但如果在该代码中发生异常,调试器 仍会停留 。
-
DebuggerHidden
调试器完全忽略这段代码,即使发生异常 也不停留 。
13.4 进程与线程处理
13.4.1 检查运行中的进程
Process.GetProcessXXX
方法可以获得相应进程(包含托管、非托管进程)。每一个 Process
实例都拥有诸多属性:名称、ID、优先级、内存、处理器利用率、窗口句柄等。
Process.GetCurrentProcess
方法返回当前的进程。如果创建了额外的应用程序域,它们将共享同一个进程。
如果需要终止一个进程,可以调用 Kill
方法。
13.4.2 在进程中检查线程
Process.Threads
属性可用于枚举其他进程内的所有线程,为 ProcessThread
类型。该对象用于管理而非执行同步任务。ProcessThread
对象提供了相应线程的诊断信息,并允许对某些属性进行控制,例如优先级和处理器亲和性。
13.5 StackTrace
和 StackFrame
类
StackTrace
和 StackFrame
类提供了执行调用栈的只读视图,主要用于诊断目的。StackTrace
代表了 一个完整的调用栈 ,而 StackFrame
代表了 调用栈中的一个单独的方法调用 。
获取当前线程调用栈(StackTrace
)的快照,方式有二:
-
使用
StackTrace
的无参构造器。 -
使用
StackTrace
的 bool
参数构造器。若参数传入
true
,且存在 pdb 文件,StackTrace
将读取文件名、行号等数据。
获取调用帧(StackFrame
),方法有二:
-
StackTrace.GetFrame
获取特定调用帧
-
StackTrace.GetFrames
获取所有调用帧
获取整个 StackTrace
基本信息的最简单的方法是调用 ToString
方法。当然也可以手动操作,方式如下:
static void Main() { A(); }
static void A() { B(); }
static void B() { C(); }
static void C()
{StackTrace s = new StackTrace(true);Console.WriteLine("Total frames: " + s.FrameCount);Console.WriteLine("Current method: " + s.GetFrame(0).GetMethod().Name);Console.WriteLine("Calling method: " + s.GetFrame(1).GetMethod().Name);Console.WriteLine("Entry method: " + s.GetFrame(s.FrameCount - 1).GetMethod().Name);Console.WriteLine("Call Stack:");foreach (var f in s.GetFrames()){Console.WriteLine(" File: " + f.GetFileName() +" Line: " + f.GetFileLineNumber() +" Col: " + f.GetFileColumnNumber() +" Offset: " + f.GetILOffset() +" Method: " + f.GetMethod().Name);}
}
其输出如下:
C7.0 核心技术指南 第7版.pdf - p585 - C7.0 核心技术指南 第 7 版-P585-20240215181746
C7.0 核心技术指南 第7版.pdf - p586 - C7.0 核心技术指南 第 7 版-P586-20240215224138
另见22.10 Suspend 和 Resume 方法,获取线程的调用栈信息
StackTrace stackTrace = null; targetThread.Suspend(); try { stackTrace = new StackTrace (targetThread, true); } finally { targetThread.Resume(); }
13.6 Windows 事件日志
标准 Windows 事件日志有三种,按名称分为:
- 应用程序
- 系统
- 安全
应用程序日志是大多数应用程序通常写入日志的地方。
13.6.1 写入事件日志
写入前需创建相应的日志源,创建后,调用 EventLog.WriteEntry
写入时需提供日志名称、源名称和消息数据
使用方式如下:
const string SourceName = "WinFormsApp1";
if (!EventLog.SourceExists(SourceName))EventLog.CreateEventSource(SourceName, "Application");
EventLog.WriteEntry(SourceName, "测试Windows日志", EventLogEntryType.Information);
13.6.2 读取事件日志
步骤如下:
- 传入日志名称(三种日志名称之一),实例化
EventLog
对象 - 指定日志所在计算机的名称(可选)
- 通过
Entries
集合读取日志
使用方式如下:
EventLog log = new EventLog("Application");Console.WriteLine(log.Entries.Count);EventLogEntry last = log.Entries[log.Entries.Count - 1];
Console.WriteLine("Index: " + last.Index);
Console.WriteLine("Source: " + last.Source);
Console.WriteLine("Type: " + last.EntryType);
Console.WriteLine("Time: " + last.TimeWritten);
Console.WriteLine("Message: " + last.Message);
可以使用静态方法 EventLog.GetEventLogs
(需要管理员权限)来枚举当前(或者其他)计算机的所有日志名称,以下代码通常情况下至少会打印“应用程序”、“安全”以及“系统”:
foreach (EventLog log in EventLog.GetEventLogs()) {Console.WriteLine(log.LogDisplayName);
}
13.6.3 监视事件日志
EntryWritten
事件相当于一个钩子,可以在 Windows 事件日志被写入时获得通知。使用步骤如下:
- 实例化
EventLog
并将它的EnableRaisingEvents
属性设置为true
- 处理
EntryWritten
事件。
代码如下:
static void Main()
{using (var log = new EventLog("Application")){log.EnableRaisingEvents = true;log.EntryWritten += DisplayEntry;Console.ReadLine();}
}static void DisplayEntry(object sender, EntryWrittenEventArgs e)
{EventLogEntry entry = e.Entry;Console.WriteLine(entry.Message);
}
Info
仅 .NET Framework 原生支持。
13.7 性能计数器
Windows 日志用于获取信息进行事后分析,性能计数器则提供运行时监控状态的能力。用本节提到的 API,我们可用在软件上显示 CPU 利用率、内存使用情况等。
13.7.1 遍历可用的计数器
以下示例将遍历计算机上的所有可用性能计数器。对于那些支持实例的计数器,则遍历每一个实例的计数器:
var cats = PerformanceCounterCategory.GetCategories();
foreach (var cat in cats) {Console.WriteLine("Category: " + cat.CategoryName);var instances = cat.GetInstanceNames();// 不支持实例的计数器,如CPU核心if(instances.Length == 0) {foreach(var ctr in cat.GetCounters())Console.WriteLine(" Counter: " + ctr.CounterName);}else {// 支持实例的计数器,如应用程序进程foreach (var instance in instances) {Console.WriteLine(" Instance: " + instance);if (cat.InstanceExists(instance))foreach (var ctr in cat.GetCounters(instance))Console.WriteLine(" Counter: " + ctr.CounterName);}}
}
性能计数器类别(Performance Counter Categories)
性能计数器类别是一组逻辑相关的性能计数器的集合。例如,“
Processor
”是一个性能计数器类别,它包含了与 CPU 性能相关的计数器,如 CPU 的利用率。实例(Instances)
某些性能计数器类别下的计数器可以有多个实例。实例通常对应于系统中的资源或对象,例如,在 “
Processor
” 类别下,每个 CPU 核心可能是一个实例;在 “Process
” 类别下,每个运行中的进程都是一个实例。性能计数器(Counters)
在给定的类别下,性能计数器是实际测量的指标。例如,在 “
Processor
” 类别下,可能有 “% Processor Time”(处理器时间百分比)这样的计数器。关于每个进程的性能计数器
不是每个进程都包含自己的性能计数器类别,而是某些性能计数器类别(如 “
Process
”)包含了多个实例,每个实例对应于一个进程。这些实例下的计数器反映了该进程的性能指标,如 CPU 使用率、内存使用量等。因此,通过这种方式,你可以监控每个进程的性能。
13.7.2 检索(查看)性能计数器
查看性能计数器的方式如下:
-
实例化
PerformanceCounter
对象 -
调用
NextValue
或者NextSample
方法。-
NextValue
返回简单的float
值; -
NextSample
返回CounterSample
对象,该对象包含高级属性,例如CounterFrequency
、TimeStamp
、BaseValue
以及RawValue
。
-
下面列举一些简单的用法:
获取 CPU 整体使用率
using (PerformanceCounter pc = new PerformanceCounter("Processor", "% Processor Time", "_Total"))Console.WriteLine(pc.NextValue());
获取当前进程内存消耗
var procName = Process.GetCurrentProcess().ProcessName;
using (PerformanceCounter pc = new PerformanceCounter("Process", "Private Bytes", procName))Console.WriteLine(pc.NextValue());
PerformanceCounter
并没有公开ValueChanged
事件,因此如果需要监视各种变化则必须使用轮询的方法。
13.7.3 创建计数器并写入性能数据
自定义计数器常见场景有:
-
应用性能监控
开发者可以为自己的应用程序创建自定义的性能计数器来监控关键操作的性能,例如,数据库查询的平均响应时间、每秒处理的事务数、队列长度等。这些数据可以帮助开发者了解应用程序在实际运行中的性能状况,并及时发现性能瓶颈。
-
系统健康检查
-
动态性能调优
-
软件测试和质量保证
-
安全监控
如下实例代码演示了如何创建分组和该分组下的所有计数器:
var category = "Nutshell Monitoring";var eatenPerMin = "Macadamias eaten so far";
var tooHard = "Macadamias deemed too hard";
if (!PerformanceCounterCategory.Exists(category))
{var cd = new CounterCreationDataCollection();cd.Add(new CounterCreationData(eatenPerMin,"Number of macadamias consumed, including shelling time",PerformanceCounterType.NumberOfItems32));cd.Add(new CounterCreationData(tooHard,"Number of macadamias that will not crack, despite much effort",PerformanceCounterType.NumberOfItems32));PerformanceCounterCategory.Create(category, "Test Category", PerformanceCounterCategoryType.SingleInstance, cd);
}
如果之后希望在该分组下添加更多的性能计数器,必须先调用
PerformanceCounterCategory.Delete
方法删除旧的分组。C7.0 核心技术指南 第7版.pdf - p593 - C7.0 核心技术指南 第 7 版-P593-20240216163440
一旦性能计数器创建完成,就可以实例化 PerformanceCounter
,将 ReadOnly
属性设置为 false
,并对 RawValue
属性赋值来更新计数器的值。也可以使用 Increament
和 IncreamentBy
方法来更新现有的值:
var category = "Nutshell Monitoring";
var eatenPerMin = "Macadamias eaten so far";using(PerformanceCounter pc = new PerformanceCounter(category, eatenPerMin, ""))
{pc.ReadOnly = false;pc.RawValue = 1000;pc.Increment();pc.IncrementBy(10);Console.WriteLine(pc.NextValue());
}
13.8 Stopwatch
类
此处简单介绍几个Stopwatch
属性:
-
ElapsedTicks
long
类型,返回计数值。 -
Stopwatch.Frequency
long 类型,表示计数频率。
ElapsedTicks
除以该值可用得到对应的秒数。
可以直接通过 Stopwatch.ElapsedMilliseconds
属性获得用时,更为简便。