在多线程程序运行过程中,日志写入是非常高频的操作,如果采用传统的锁机制保证写入安全,很容易出现线程争抢锁资源导致的性能下降问题。无锁设计可以通过队列和原子操作避免锁的使用,同时保证日志写入的线程安全。

核心设计思路
无锁文件日志记录器的核心设计分为三个部分:
- 日志消息队列:使用线程安全的无锁队列存储待写入的日志内容,生产者线程将日志放入队列,消费者线程从队列取出日志写入文件。
- 后台写入线程:单独启动一个后台线程作为消费者,持续从队列中获取日志并写入文件,避免业务线程被文件IO阻塞。
- 原子状态控制:使用原子变量控制日志记录器的启动和停止状态,避免多线程操作状态时的冲突。
无锁队列的实现
C#中可以使用System.Collections.Concurrent命名空间下的ConcurrentQueue作为无锁队列,它是线程安全的先进先出集合,内部通过原子操作和自旋等待实现无锁入队出队。如果需要自定义无锁队列,也可以基于原子操作实现,以下是简单的自定义无锁队列示例:
using System;
using System.Threading;
namespace LockFreeLogger
{
// 无锁队列节点定义
internal class LockFreeQueueNode<T>
{
public T Value { get; set; }
public LockFreeQueueNode<T> Next { get; set; }
}
// 无锁队列实现
internal class LockFreeQueue<T>
{
private LockFreeQueueNode<T> _head;
private LockFreeQueueNode<T> _tail;
public LockFreeQueue()
{
// 初始化头尾节点为哨兵节点
_head = _tail = new LockFreeQueueNode<T>();
}
// 入队操作,无锁
public void Enqueue(T item)
{
var newNode = new LockFreeQueueNode<T> { Value = item };
LockFreeQueueNode<T> oldTail;
LockFreeQueueNode<T> oldNext;
do
{
oldTail = _tail;
oldNext = oldTail.Next;
// 如果尾节点的下一个节点不为空,说明其他线程已经修改了尾节点,重新获取
if (oldNext != null)
{
Interlocked.CompareExchange(ref _tail, oldNext, oldTail);
}
} while (oldNext != null);
// 尝试将新节点设置为尾节点的下一个节点
if (Interlocked.CompareExchange(ref oldTail.Next, newNode, null) == null)
{
// 成功后将尾节点指向新节点
Interlocked.CompareExchange(ref _tail, newNode, oldTail);
}
}
// 出队操作,无锁
public bool TryDequeue(out T result)
{
result = default;
LockFreeQueueNode<T> oldHead;
LockFreeQueueNode<T> oldTail;
LockFreeQueueNode<T> oldNext;
do
{
oldHead = _head;
oldTail = _tail;
oldNext = oldHead.Next;
// 如果头节点等于尾节点,且下一个节点为空,说明队列为空
if (oldHead == oldTail)
{
if (oldNext == null)
{
return false;
}
// 尾节点落后,更新尾节点
Interlocked.CompareExchange(ref _tail, oldNext, oldTail);
}
} while (oldHead != oldTail || oldNext != null);
// 取出节点值
result = oldNext.Value;
// 尝试将头节点移动到下一个节点
if (Interlocked.CompareExchange(ref _head, oldNext, oldHead) == oldHead)
{
return true;
}
return false;
}
// 判断队列是否为空
public bool IsEmpty()
{
return _head.Next == null;
}
}
}
日志记录器完整实现
基于无锁队列和后台写入线程,我们实现完整的日志记录器,以下是核心代码:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace LockFreeLogger
{
public class LockFreeFileLogger : IDisposable
{
// 日志队列,存储待写入的日志内容
private readonly LockFreeQueue<string> _logQueue = new LockFreeQueue<string>();
// 日志记录器运行状态,使用原子操作保证线程安全
private int _isRunning = 0;
// 后台写入任务的取消令牌
private CancellationTokenSource _cancellationTokenSource;
// 日志文件路径
private readonly string _logFilePath;
// 后台写入线程
private Task _writeTask;
public LockFreeFileLogger(string logFilePath)
{
_logFilePath = logFilePath;
}
// 启动日志记录器
public void Start()
{
// 原子操作设置运行状态为1,只有状态为0时才启动
if (Interlocked.CompareExchange(ref _isRunning, 1, 0) == 0)
{
_cancellationTokenSource = new CancellationTokenSource();
// 启动后台写入任务
_writeTask = Task.Run(() => WriteLogLoop(_cancellationTokenSource.Token));
}
}
// 停止日志记录器
public void Stop()
{
// 原子操作设置运行状态为0
if (Interlocked.CompareExchange(ref _isRunning, 0, 1) == 1)
{
_cancellationTokenSource?.Cancel();
// 等待后台任务完成剩余日志写入
_writeTask?.Wait(TimeSpan.FromSeconds(5));
_cancellationTokenSource?.Dispose();
}
}
// 写入日志的公共方法,多线程调用安全
public void Log(string message)
{
// 如果日志记录器未运行,直接返回
if (Volatile.Read(ref _isRunning) == 0)
{
return;
}
// 拼接日志时间和内容
string logContent = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}";
// 无锁入队
_logQueue.Enqueue(logContent);
}
// 后台写入循环
private void WriteLogLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested || !_logQueue.IsEmpty())
{
// 尝试从队列取出日志
if (_logQueue.TryDequeue(out string logContent))
{
try
{
// 写入文件,追加模式
using (var writer = new StreamWriter(_logFilePath, true))
{
writer.WriteLine(logContent);
}
}
catch
{
// 实际项目中可以添加写入失败的重试逻辑
}
}
else
{
// 队列为空时短暂等待,避免空转消耗CPU
Thread.Sleep(10);
}
}
}
// 释放资源
public void Dispose()
{
Stop();
}
}
}
使用示例
以下是多线程场景下使用该日志记录器的示例:
using System;
using System.Threading;
namespace LockFreeLogger
{
class Program
{
static void Main(string[] args)
{
// 初始化日志记录器,指定日志文件路径
using (var logger = new LockFreeFileLogger("app.log"))
{
// 启动日志记录器
logger.Start();
// 启动10个线程同时写入日志
for (int i = 0; i < 10; i++)
{
int threadId = i;
Thread thread = new Thread(() =>
{
for (int j = 0; j < 100; j++)
{
logger.Log($"线程{threadId}写入第{j}条日志");
}
});
thread.Start();
}
// 等待所有线程执行完成
Thread.Sleep(2000);
// 停止日志记录器
logger.Stop();
}
Console.WriteLine("日志写入完成");
}
}
}
注意事项
该无锁文件日志记录器虽然避免了锁的使用,但仍有需要注意的点:
- 日志文件写入是IO操作,单线程写入可以保证日志顺序,如果需要更高的写入性能,可以考虑批量写入,减少IO次数。
- 如果日志产生速度远超过写入速度,队列会不断堆积,需要添加队列长度限制,避免内存溢出。
- 实际生产环境中需要添加日志分级、日志文件滚动、写入异常重试等更完善的功能。