C# .NET多线程异步记录日声,队列LOG
如果你希望以最简单的方式实现,同时保证线程安全和基本的异步支持,可以使用以下简化方案。这个方案直接使用 File.AppendAllTextAsync
并添加锁机制,适合错误情况较少的场景:
简化实现方案csharp
private readonly object _fileLock = new object();
private readonly string _logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "client_close.log");// 移除客户端
public void RemoveClient(string clientId, string errinfo = null)
{if (_clients.TryRemove(clientId, out _) && _lastHeartbeat.TryRemove(clientId, out _)){// 记录日志(异步但顺序执行)_ = LogClientRemovalAsync(clientId, errinfo);}
}// 异步记录客户端移除信息
private async Task LogClientRemovalAsync(string clientId, string errinfo)
{string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff},{clientId},{errinfo ?? "正常关闭"}";// 使用锁确保文件写入的线程安全lock (_fileLock){try{// 异步写入文件(在锁内使用异步方法,确保顺序执行)File.AppendAllTextAsync(_logFilePath, logEntry + Environment.NewLine).Wait();}catch (Exception ex){Console.WriteLine($"日志写入失败: {ex.Message}");// 可添加更复杂的错误处理逻辑}}
}
关键点说明
-
线程安全:
- 使用
lock
关键字确保同一时间只有一个线程可以写入文件。 - 虽然
File.AppendAllTextAsync
是异步方法,但在锁内部调用其同步版本Wait()
,确保写入顺序。
- 使用
-
异步支持:
- 方法标记为
async
,但使用Wait()
同步等待写入完成,避免异步操作的复杂性。 - 调用方通过
_ = LogClientRemovalAsync(...)
非阻塞地触发日志记录。
- 方法标记为
-
错误处理:
- 简单捕获并打印异常,适合错误情况较少的场景。
日志文件格式
与之前相同:
2023-06-15 14:30:21.123,client123,连接超时
2023-06-15 14:30:35.456,client456,正常关闭
优点
- 代码简单:无需额外的队列或后台任务。
- 资源占用少:不使用额外的线程或信号量。
- 基本异步支持:在锁内使用异步文件操作,减少 IO 阻塞。
注意事项
- 性能限制:在高并发场景下可能成为瓶颈,因为每次写入都需要获取锁。
- 异常处理简化:仅简单打印错误,未实现重试或日志缓冲。
如果你的应用程序错误情况确实很少,且并发量不高,这种实现方式足够简单且可靠。
要实现支持异步多线程并发操作的客户端关闭信息记录功能,需要考虑线程安全和文件写入的异步性。以下是一个完整的解决方案:
实现方案
1. 添加异步日志方法
使用 ConcurrentQueue
缓存日志条目,并通过后台任务异步写入文件,确保线程安全和高性能。
private readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>();
private readonly CancellationTokenSource _logCts = new CancellationTokenSource();
private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
private readonly string _logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "client_close.log");// 初始化时启动日志写入任务
public WebSocketHeartbeatManager()
{// 启动异步日志写入任务_ = Task.Run(() => ProcessLogQueueAsync(_logCts.Token));
}// 异步处理日志队列
private async Task ProcessLogQueueAsync(CancellationToken cancellationToken)
{while (!cancellationToken.IsCancellationRequested){try{if (_logQueue.TryDequeue(out string logEntry)){await WriteLogEntryAsync(logEntry);}else{await Task.Delay(100, cancellationToken); // 短暂等待,避免CPU空转}}catch (OperationCanceledException){// 正常取消,退出循环break;}catch (Exception ex){Console.WriteLine($"日志处理错误: {ex.Message}");// 发生错误时等待更长时间await Task.Delay(1000, cancellationToken);}}
}// 异步写入日志条目
private async Task WriteLogEntryAsync(string logEntry)
{await _fileLock.WaitAsync(); // 获取文件锁try{// 使用File.AppendAllTextAsync确保原子性await File.AppendAllTextAsync(_logFilePath, logEntry + Environment.NewLine);}finally{_fileLock.Release(); // 释放文件锁}
}// 清理资源
public void Dispose()
{_logCts.Cancel();_logCts.Dispose();_fileLock.Dispose();
}
2. 修改 RemoveClient 方法
添加可选参数 errinfo
,并将日志信息加入队列:csharp
// 移除客户端
public void RemoveClient(string clientId, string errinfo = null)
{if (_clients.TryRemove(clientId, out _) && _lastHeartbeat.TryRemove(clientId, out DateTime removedTime)){// 格式化日志条目:时间,CLIENTID,errinfostring logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff},{clientId},{errinfo ?? "正常关闭"}";// 将日志条目加入队列(线程安全操作)_logQueue.Enqueue(logEntry);}
}
关键点说明
-
线程安全:
- 使用
ConcurrentQueue
存储日志条目,确保多线程环境下的入队操作安全。 - 通过
SemaphoreSlim
控制文件写入的并发访问,避免文件冲突。
- 使用
-
异步处理:
- 日志写入操作在单独的后台任务中进行,不阻塞主线程。
- 使用
File.AppendAllTextAsync
实现异步文件写入。
-
异常处理:
- 在日志处理循环中捕获并处理可能的异常,确保系统稳定运行。
-
资源释放:
- 实现
Dispose
方法,在对象销毁时正确释放CancellationTokenSource
和SemaphoreSlim
。
- 实现
日志文件格式示例plaintext
2023-06-15 14:30:21.123,client123,连接超时
2023-06-15 14:30:35.456,client456,正常关闭
2023-06-15 14:31:02.789,client789,协议错误
使用示例csharp
// 正常关闭
manager.RemoveClient("client123");// 异常关闭
manager.RemoveClient("client456", "心跳超时");
这种实现方式在高并发场景下依然能保持良好性能,同时确保所有客户端关闭信息都被可靠记录。