利用双缓冲队列来减少锁的竞争
在日常的开发中,日志的记录是必不可少的。但是我们也清楚对同一个文本进行写日志只能单线程的去写,那么我们也经常会使用简单lock锁来保证只有一个线程来写入日志信息。但是在多线程的去写日志信息的时候,由于记录日志信息是需要进行I/O交互的,导致我们占用锁的时间会加长,从而导致大量线程的阻塞与等待。
这种场景下我们就会去思考,我们该怎么做才能保证当有多个线程来写日志的时候我们能够在不利用锁的情况下让他们依次排队去写呢?这个时候我们就可以考虑下使用双缓冲队列来完成。
所谓双缓冲队列就是有两个队列,一个是用来专门负责数据的写入,另一个是专门负责数据的读取,当逻辑线程读取完数据后负责将自己的队列与I/O线程的队列进行交换。
我们该怎么利用这双缓冲队列来完成我们想要的效果呢?
当有多个线程来写日志的时候,这个时候我们要这些要写的信息先放到我们负责写入的队列当中,然后将逻辑读的线程设为非阻塞。此时逻辑读的线程就可以开始工作了。(一开始时逻辑读的队列是空的)在当逻辑读的线程读取他自己队列的数据(并执行一些逻辑)之后,将逻辑读的队列的引用和负责写入的队列进行引用交换。这就是简单的一个双缓冲队列实现的一个思路。具体实现代码如下:

public class User { public string Mobile { get; set; } public string Pwd { get; set; } public override string ToString() { return $"{Mobile},{Pwd}"; } }
public class DoubleQueue { private ConcurrentQueue<User> _writeQueue; private ConcurrentQueue<User> _readQueue; private volatile ConcurrentQueue<User> _currentQueue; private AutoResetEvent _dataEvent; private ManualResetEvent _finishedEvent; private ManualResetEvent _producerEvent; public DoubleQueue() { _writeQueue = new ConcurrentQueue<User>(); _readQueue = new ConcurrentQueue<User>(); _currentQueue = _writeQueue; _dataEvent = new AutoResetEvent(false); _finishedEvent = new ManualResetEvent(true); _producerEvent = new ManualResetEvent(true); Task.Factory.StartNew(() => ConsumerQueue(), TaskCreationOptions.None); } public void ProducerFunc(User user) { _producerEvent.WaitOne(); _finishedEvent.Reset(); _currentQueue.Enqueue(user); _dataEvent.Set(); _finishedEvent.Set(); } public void ConsumerQueue() { ConcurrentQueue<User> consumerQueue; User user; int allcount = 0; Stopwatch watch = Stopwatch.StartNew(); while (true) { _dataEvent.WaitOne(); if (_currentQueue.Count > 0) { _producerEvent.Reset(); _finishedEvent.WaitOne(); consumerQueue = _currentQueue; _currentQueue = (_currentQueue == _writeQueue) ? _readQueue : _writeQueue; _producerEvent.Set(); while (consumerQueue.Count > 0) { if (consumerQueue.TryDequeue(out user)) { FluentConsole.White.Background.Red.Line(user.ToString()); allcount++; } FluentConsole.White.Background.Red.Line($"当前个数{allcount.ToString()},花费了{watch.ElapsedMilliseconds.ToString()}ms;"); System.Threading.Thread.Sleep(20); } } } } }
FluentConsole 是一个控制台应用程序的输出插件,开源的,有兴趣的可以自己去玩玩。

internal class Program { private static object obj = new object(); private static void Main(string[] args) { DoubleQueue doubleQueue = new DoubleQueue(); Parallel.For(0, 3000, i => { User user = new User() { Mobile = i.ToString().PadLeft(11, '0'), Pwd = i.ToString().PadLeft(8, '8') }; doubleQueue.ProducerFunc(user); }); Stopwatch watch = Stopwatch.StartNew(); int allcount = 0; Parallel.For(0, 3000, i => { User user = new User() { Mobile = i.ToString().PadLeft(11, '0'), Pwd = i.ToString().PadLeft(8, '8') }; lock (obj) { FluentConsole.White.Background.Red.Line(user.ToString()); allcount++; FluentConsole.White.Background.Red.Line($"当前个数{allcount.ToString()},花费了{watch.ElapsedMilliseconds.ToString()}ms;"); System.Threading.Thread.Sleep(20); } }); FluentConsole.Black.Background.Red.Line("执行完成"); Console.Read(); } }
第一个利用双缓冲队列来执行,第二个利用lock锁来执行。下面分别是第一种方法和第二种方法执行时CPU的消耗。
我们可以发现利用双队列缓冲的情况下我们减少了CPU的占有。但是我们可能会增加执行的时间。
参考文章:http://www.codeproject.com/Articles/27703/Producer-Consumer-Using-Double-Queues
别人在08年就已经想到了,而我却在现在才稍微有点想法。
后面再大家的评论和建议之下,将代码改为如下:

public class DoubleQueue { private ConcurrentQueue<User> _writeQueue; private ConcurrentQueue<User> _readQueue; private volatile ConcurrentQueue<User> _currentQueue; private AutoResetEvent _dataEvent; public DoubleQueue() { _writeQueue = new ConcurrentQueue<User>(); _readQueue = new ConcurrentQueue<User>(); _currentQueue = _writeQueue; _dataEvent = new AutoResetEvent(false); Task.Factory.StartNew(() => ConsumerQueue(), TaskCreationOptions.None); } public void ProducerFunc(User user) { _currentQueue.Enqueue(user); _dataEvent.Set(); } public void ConsumerQueue() { ConcurrentQueue<User> consumerQueue; User user; int allcount = 0; Stopwatch watch = Stopwatch.StartNew(); while (true) { _dataEvent.WaitOne(); if (!_currentQueue.IsEmpty) { _currentQueue = (_currentQueue == _writeQueue) ? _readQueue : _writeQueue; consumerQueue = (_currentQueue == _writeQueue) ? _readQueue : _writeQueue; while (!consumerQueue.IsEmpty) { while (!consumerQueue.IsEmpty) { if (consumerQueue.TryDequeue(out user)) { FluentConsole.White.Background.Red.Line(user.ToString()); allcount++; } } FluentConsole.White.Background.Red.Line($"当前个数{allcount.ToString()},花费了{watch.ElapsedMilliseconds.ToString()}ms;"); System.Threading.Thread.Sleep(20); } } } } }
作者:yjq
欢迎任何形式的转载,但请务必注明出处。
.netcore相关功能需要定制的可以找我。有丰富的并发处理,性能优化经验。单点登录,限流,熔断,读写分离等功能均可定制。也可以帮忙系统优化处理,系统诊断,请联系博主(备注添加原因)。微信:yjq425527169 QQ:425527169
本篇文章如有些许帮助请点击推荐让更多需要帮助的人可以看到,请支持原创,请大方打赏(右边点击打赏)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?