关于new等函数对任务管理器中内存、句柄数变化的影响

本文通过在XP系统和VS2008 DEBUG模式下测试,探讨new操作如何影响任务管理器显示的内存和句柄数变化。详细分析了内存分配和对象创建对系统资源的实际占用情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以下代码在XP+VS2008 DEBUG环境下测试:

 

#include <stdio.h>
#include <windows.h>

typedef void (*PFNDo)();

void runtest(PFNDo pfn) { while(1) { pfn();Sleep(100); } }

void do_nop() { ; }
void do_new() { int* p = new int[5]; }
void do_event() { HANDLE hEvent = CreateEvent(NULL,FALSE,TRUE,NULL); }
void do_mapping() { HANDLE hFile = CreateFileMapping(NULL,NULL,0,0,1024,"CHINA20130327CCTVTEST"); }
void do_file() { HANDLE hFile = CreateFile("D:\\testfile",GENERIC_READ,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); }
void do_thread() { ; }
void do_process() { HANDLE hProcess = OpenProcess(0,FALSE,3092); }

int main()
{
	//
	// (内存使用,虚拟内存大小,句柄数,线程数)
	//
	unsigned int op = 1;
	switch(op)
	{
	case 0:runtest(do_nop);break;	// 一直维持在(652K,224K,7,1)
	case 1:runtest(do_new);break;	// 内存和虚拟内存持续增加,某个时刻结果为(700K,276K,7,1)
	case 2:runtest(do_event);break;	// 句柄数持续增加,某个时刻结果为(656K,224K,604,1)
	case 3:runtest(do_mapping);break;	// 一直维持在(656K,224K,7,1)
	case 4:runtest(do_file);break;	// 一直维持在(656K,224K,8,1)
	case 5:runtest(do_process);break;	// 一直维持在(660K,224K,7,1)
	default:break;
	}
	return 0;
}

<think>我们有一个需求:使用C#异步写入不同类型的日志(内存泄露、软件异常、报警信息、运行信息、操作信息)到txt文件中。 我们需要设计一个日志记录器,它能够异步地将日志消息写入文件,以避免阻塞主线程。同时,我们需要支持多种日志类型。 设计思路: 1. 使用一个队列来存储日志消息(生产者-消费者模式)。 2. 使用一个后台线程或异步任务来从队列中取出日志并写入文件。 3. 提供公共方法供其他代码调用以记录日志,这些方法将日志消息放入队列。 4. 考虑线程安全,因为多个线程可能同时调用日志记录方法。 步骤: a. 创建一个日志消息类(LogEntry),包含日志内容、类型、时间戳等。 b. 使用一个线程安全的队列(如ConcurrentQueue)来存储日志消息。 c. 使用一个ManualResetEvent或SemaphoreSlim来通知后台线程有新日志需要处理。 d. 在后台线程中,循环等待信号,然后从队列中取出日志并写入文件。 e. 提供公共的日志记录方法(如LogMemoryLeak, LogException, LogWarning, LogInfo, LogOperation等),这些方法将日志消息加入队列并触发信号。 f. 考虑程序退出时,确保所有日志都被写入(例如在析构函数或应用程序退出事件中刷新队列)。 另外,为了避免频繁地打开和关闭文件,我们可以按天创建日志文件,并且在写入时保持文件流打开一段时间(但要注意,如果程序崩溃,可能丢失缓存中的日志)。或者,每次写入都打开和关闭文件(性能较差,但确保每条日志都写入)。为了平衡,我们可以使用缓冲区,定期将日志刷入文件。 然而,为了简单起见,我们这里采用每次写入都打开文件(追加模式)并写入,然后关闭文件。这样确保每条日志都写入,但性能可能不高。如果日志量很大,建议使用缓冲区并定时写入。 但是,考虑到异步和性能,我们可以采用批量写入的方式:即后台线程每次从队列中取出多条日志(例如最多100条,或等待100毫秒),然后一次性写入文件。 由于需求是异步写入,我们采用以下方式: 方案1:使用专门的日志线程,使用一个阻塞队列(BlockingCollection)来收集日志,然后由该线程负责写入文件。 方案2:使用异步方法,但是文件写入本身是IO操作,我们可以使用异步IO(如FileStream的异步写入)来避免阻塞线程池线程,但要注意并发写入的问题。 这里我们采用方案1,因为它更简单,且容易控制写入顺序。 实现步骤: 1. 定义日志级别(类型)枚举 2. 定义日志消息类 3. 创建日志服务类(Logger),包含: - 一个BlockingCollection<LogEntry>作为队列 - 一个CancellationTokenSource用于停止后台线程 - 一个后台线程(Task)用于处理日志 - 公共的日志记录方法 4. 在后台线程中,循环从BlockingCollection中取出日志消息,然后写入文件(可以批量取,然后批量写入) 5. 提供停止日志服务的方法,用于应用程序退出时调用,确保所有日志都被写入。 注意:为了避免频繁打开关闭文件,我们可以按日期创建文件,并在同一天内保持文件流打开?但是,如果保持打开,在程序突然崩溃时,可能最后几条日志还在缓冲区没写入。所以,我们可以每次写入都flush,或者按时间间隔flush。但为了简单,我们每次写入都打开文件(追加模式)并关闭,这样系统会自动flush。 但是,这样性能会比较差。我们可以采用以下优化:在同一个时间点(比如同一天)的日志,我们打开文件流并保持打开状态,直到日期变化或者一段时间没有写入,再关闭。这样就需要更复杂的逻辑。 为了简化,我们这里采用每次写入都打开和关闭文件(追加写入)。如果日志量很大,可能需要考虑性能优化。 另一种方式:我们按日期创建文件,并且在处理日志时,如果当前打开的文件不是当天的,则关闭并重新打开当天的文件。在后台线程中,我们记录当前打开的文件流,然后每次写入时检查日期是否变化。这样,在一天内我们只打开和关闭一次文件(除非日期变化)。 我们选择第二种方式:按日期创建文件,并在同一天内保持文件流打开。后台线程在写入日志时,检查日期,如果日期变化,则关闭旧文件,创建新文件。 具体实现: 1. 日志服务类(Logger): - 私有字段:当前文件流(StreamWriter),当前日期(用于判断是否换天) - 初始化时,创建日志目录(如果不存在) - 后台线程循环处理日志,每次处理时检查日期,如果日期变化则切换文件 - 写入日志后,可以定期Flush(或者每条日志都Flush,确保不丢失) 2. 为了确保程序退出时能正确关闭文件,我们需要在停止日志服务时关闭文件流。 3. 考虑到异常情况,写入文件时可能会发生异常,我们需要捕获异常并处理(例如,写入失败,可以尝试重新打开文件等),但不要影响主程序。 4. 日志格式:时间戳 [日志类型] 日志内容 5. 我们使用BlockingCollection来保证线程安全。 代码结构: 首先,定义日志级别枚举: ``` public enum LogLevel { MemoryLeak, // 内存泄露 Exception, // 软件异常 Warning, // 报警信息 Information, // 运行信息 Operation // 操作日志 } ``` 然后,定义日志条目: ``` public class LogEntry { public DateTime Timestamp { get; set; } public LogLevel Level { get; set; } public string Message { get; set; } } ``` 日志服务类(单例模式,便于全局使用): ``` public sealed class Logger : IDisposable { private BlockingCollection<LogEntry> _logQueue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>()); private Task _processingTask; private CancellationTokenSource _cts; private StreamWriter _writer; private string _logDirectory; private DateTime _currentDate; private static readonly Lazy<Logger> _instance = new Lazy<Logger>(() => new Logger()); public static Logger Instance => _instance.Value; private Logger() { // 初始化日志目录,默认在当前目录的Logs文件夹下 _logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); Directory.CreateDirectory(_logDirectory); _currentDate = DateTime.Today; OpenLogFile(_currentDate); _cts = new CancellationTokenSource(); _processingTask = Task.Factory.StartNew(ProcessLogs, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } private void OpenLogFile(DateTime date) { // 关闭之前的文件流 _writer?.Close(); // 根据日期创建文件名 string filePath = Path.Combine(_logDirectory, $"log-{date:yyyyMMdd}.txt"); _writer = new StreamWriter(filePath, append: true, encoding: Encoding.UTF8); _writer.AutoFlush = true; // 自动刷新,确保写入文件 } private void ProcessLogs() { while (!_cts.IsCancellationRequested) { try { // 从队列中取出日志,如果没有日志则等待,可设置超时时间以便检查日期变化 if (_logQueue.TryTake(out LogEntry entry, TimeSpan.FromMilliseconds(100))) { // 检查日期是否变化 DateTime today = DateTime.Today; if (today != _currentDate) { _currentDate = today; OpenLogFile(_currentDate); } // 写入日志 string logLine = $"{entry.Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{entry.Level}] {entry.Message}"; _writer.WriteLine(logLine); } else { // 没有日志,检查日期是否变化(即使没有日志,日期变化也要切换文件) DateTime today = DateTime.Today; if (today != _currentDate) { _currentDate = today; OpenLogFile(_currentDate); } } } catch (Exception ex) { // 日志处理内部错误,可以尝试重新打开文件等,这里简单输出到控制台 Console.WriteLine($"Logger internal error: {ex.Message}"); } } // 取消被请求,将剩余日志全部写入 while (_logQueue.TryTake(out LogEntry entry)) { string logLine = $"{entry.Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{entry.Level}] {entry.Message}"; _writer.WriteLine(logLine); } // 关闭文件 _writer?.Close(); } public void Log(LogLevel level, string message) { _logQueue.Add(new LogEntry { Timestamp = DateTime.Now, Level = level, Message = message }); } public void LogMemoryLeak(string message) => Log(LogLevel.MemoryLeak, message); public void LogException(string message) => Log(LogLevel.Exception, message); public void LogWarning(string message) => Log(LogLevel.Warning, message); public void LogInformation(string message) => Log(LogLevel.Information, message); public void LogOperation(string message) => Log(LogLevel.Operation, message); public void Dispose() { if (!_cts.IsCancellationRequested) { _cts.Cancel(); _processingTask.Wait(); // 等待处理任务完成 } _writer?.Dispose(); _logQueue.Dispose(); _cts.Dispose(); } } ``` 使用方式: 在程序启动时,可以开始记录日志。在程序退出时,调用Logger.Instance.Dispose()。 注意: - 这个实现是单例的,全局只有一个实例。 - 在后台线程中,我们使用TryTake并设置超时,这样我们可以定期检查日期是否变化(即使没有日志写入)。 - 自动按天创建日志文件,每天一个文件。 - 在Dispose时,会取消后台任务,并等待任务完成,同时将队列中剩余日志写入。 但是,这个实现有一个问题:如果日志量非常大,队列可能会积压,导致内存占用过高。我们可以设置队列的最大容量,当达到最大容量时,添加日志的方法会阻塞,直到队列有空间。在BlockingCollection初始化时可以指定一个容量。 修改初始化:`_logQueue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>(), 10000);` // 设置容量为10000 然后,在Log方法中,如果队列已满,添加操作会阻塞,直到有空间。这可能会影响调用线程。为了避免阻塞,我们可以使用TryAdd,如果失败可以选择丢弃日志或者采取其他措施。但这里我们为了确保日志不丢失,使用阻塞。 另外,为了确保程序退出时能够调用Dispose,我们可以在应用程序的退出事件(如Console程序的Ctrl+C,WinForm的FormClosed事件,ASP.NET Core的Shutdown事件)中调用。 示例使用: 在程序入口: ``` static void Main(string[] args) { try { // 记录日志 Logger.Instance.LogInformation("程序启动"); // ... 其他代码 } catch (Exception ex) { Logger.Instance.LogException($"程序异常: {ex.Message}"); } finally { Logger.Instance.Dispose(); } } ``` 或者使用using模式(但单例通常不便于使用using,所以我们在程序退出时显式调用Dispose)。 注意:这个日志类没有考虑多个进程同时写入同一个文件的问题。如果多个进程使用同一个日志目录,可能会同时写入同一个文件,导致混乱。如果需要多进程写入,需要考虑使用互斥锁或者使用单独的日志服务进程。 以上实现可以满足基本需求。对于更高级的需求(如日志分级过滤、日志滚动、网络日志等),可以考虑使用成熟的日志库如NLog、Log4Net等。 根据用户引用[1]和[3]中提到的异常记录和内存问题,我们的日志方法可以捕获异常和内存泄露信息,并记录到文件。 现在,我们根据用户需求,提供了异步写入日志的解决方案。 相关问题: 1. 如何避免日志文件过大? 2. 在多线程环境下,如何确保日志写入的顺序? 3. 如何扩展此日志系统以支持网络日志服务器(如Elasticsearch)? 4. 如何配置日志级别,只记录特定级别以上的日志(例如只记录Warning及以上)? 5. 在分布式系统中,如何保证日志的准确性和一致性?</think>### C# 异步日志写入方案 以下是实现异步日志写入的完整方案,支持内存泄露、软件异常、报警信息、运行日志和操作日志的分类记录: #### 核心实现代码 ```csharp using System; using System.Collections.Concurrent; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; public enum LogType { MemoryLeak, // 内存泄露 SoftwareError, // 软件异常 Alert, // 报警信息 Runtime, // 运行日志 Operation // 操作日志 } public static class AsyncLogger { private static readonly BlockingCollection<LogEntry> _logQueue = new(); private static readonly CancellationTokenSource _cts = new(); private static StreamWriter _writer; private static DateTime _currentDate; static AsyncLogger() { // 初始化日志目录 var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); Directory.CreateDirectory(logDir); // 启动后台写入任务 Task.Run(() => ProcessLogQueue(_cts.Token)); } public static void Log(LogType type, string message) { _logQueue.Add(new LogEntry { Type = type, Message = message, Timestamp = DateTime.Now }); } private static void ProcessLogQueue(CancellationToken token) { try { while (!token.IsCancellationRequested) { var entry = _logQueue.Take(token); EnsureLogFile(entry.Timestamp); // 格式化日志条目 var logLine = $"{entry.Timestamp:yyyy-MM-dd HH:mm:ss.fff} " + $"[{entry.Type}] {entry.Message}"; // 异步写入文件 _writer.WriteLineAsync(logLine).Wait(token); } } finally { _writer?.Close(); } } private static void EnsureLogFile(DateTime timestamp) { // 按日期创建新日志文件 if (_writer == null || timestamp.Date != _currentDate) { _writer?.Close(); _currentDate = timestamp.Date; var logPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Logs", $"log-{_currentDate:yyyyMMdd}.txt"); _writer = new StreamWriter(logPath, true, Encoding.UTF8) { AutoFlush = true // 自动刷新缓冲区 }; } } public static void Shutdown() => _cts.Cancel(); private class LogEntry { public LogType Type { get; init; } public string Message { get; init; } public DateTime Timestamp { get; init; } } } ``` #### 使用示例 ```csharp // 记录内存泄露 AsyncLogger.Log(LogType.MemoryLeak, "检测到内存泄漏: 对象MyService未释放,已累积增长50MB"); // 记录软件异常 try { // 业务代码 } catch (Exception ex) { AsyncLogger.Log(LogType.SoftwareError, $"文件处理失败: {ex.Message}\n{ex.StackTrace}"); } // 记录报警信息 AsyncLogger.Log(LogType.Alert, "CPU使用率超过90%阈值,当前值95.7%"); // 记录运行日志 AsyncLogger.Log(LogType.Runtime, "服务启动完成,监听端口8080"); // 记录操作日志 AsyncLogger.Log(LogType.Operation, "用户admin执行了配置更新操作"); ``` #### 关键技术说明 1. **异步处理架构** - 使用 `BlockingCollection` 作为线程安全队列 - 后台任务通过 `Task.Run` 实现异步处理 - 生产者-消费者模式分离日志产生和写入操作 2. **文件管理策略** - 按日期自动创建日志文件(如 `log-20231015.txt`) - 使用 `AutoFlush = true` 确保异常时不丢失数据 - UTF-8 编码支持中文日志内容 3. **日志格式规范** ```plaintext 2023-10-15 14:30:25.123 [MemoryLeak] 检测到内存泄漏... 2023-10-15 14:31:10.456 [SoftwareError] 文件处理失败... ``` 4. **资源管理** - `CancellationToken` 实现优雅关闭 - `finally` 块确保文件句柄释放 - 静态构造函数初始化单例实例 #### 高级优化建议 1. **批量写入优化** ```csharp // 修改ProcessLogQueue中的处理循环 var batch = new List<LogEntry>(); while (!token.IsCancellationRequested) { batch.Add(_logQueue.Take(token)); // 批量处理100条或等待50ms while (_logQueue.TryTake(out var item, 50, token)) { batch.Add(item); if (batch.Count >= 100) break; } EnsureLogFile(batch[0].Timestamp); await _writer.WriteAsync(string.Join("\n", batch.Select(FormatLog))); batch.Clear(); } ``` 2. **异常恢复机制** ```csharp private static void EnsureLogFile(DateTime timestamp) { try { // 原有实现 } catch (IOException) { // 自动重试机制 Thread.Sleep(100); _writer = null; EnsureLogFile(timestamp); } } ``` 3. **日志分级过滤** ```csharp // 添加配置属性 public static LogType MinimumLevel { get; set; } = LogType.Runtime; // 修改Log方法 public static void Log(LogType type, string message) { if (type >= MinimumLevel) { _logQueue.Add(new LogEntry { ... }); } } ``` #### 应用场景示例 1. **内存泄露监控** ```csharp // 在内存检查服务中 var leakedObjects = MemoryProfiler.FindLeaks(); if (leakedObjects.Count > 0) { AsyncLogger.Log(LogType.MemoryLeak, $"发现{leakedObjects.Count}个泄露对象: " + string.Join(", ", leakedObjects)); } ``` 2. **全局异常处理** ```csharp AppDomain.CurrentDomain.UnhandledException += (s, e) => { AsyncLogger.Log(LogType.SoftwareError, $"未处理异常: {e.ExceptionObject}"); }; ``` 3. **操作审计追踪** ```csharp public void UpdateSettings(Settings newSettings) { // ...更新逻辑 AsyncLogger.Log(LogType.Operation, $"用户{CurrentUser}更新了系统设置: " + $"超时={newSettings.Timeout}秒"); } ``` #### 关闭时清理资源 ```csharp // 在应用程序退出时调用 Application.ApplicationExit += (s, e) => AsyncLogger.Shutdown(); ``` > **重要提示**:实际部署时建议: > 1. 添加日志文件滚动策略(如按大小分割) > 2. 集成云原生日志系统(如ELK或Azure Application Insights)[^2] > 3. 敏感信息过滤避免记录密码等数据 > 4. 添加速率限制防止日志洪泛
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值