利用双缓冲队列来减少锁的竞争

  在日常的开发中,日志的记录是必不可少的。但是我们也清楚对同一个文本进行写日志只能单线程的去写,那么我们也经常会使用简单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);
                    }
                }
            }
        }
    }

原文地址:https://www.cnblogs.com/jjg0519/p/8309514.html

时间: 2024-10-11 10:50:55

利用双缓冲队列来减少锁的竞争的相关文章

双缓冲队列来减少锁的竞争

双缓冲队列来减少锁的竞争 在日常的开发中,日志的记录是必不可少的.但是我们也清楚对同一个文本进行写日志只能单线程的去写,那么我们也经常会使用简单lock锁来保证只有一个线程来写入日志信息.但是在多线程的去写日志信息的时候,由于记录日志信息是需要进行I/O交互的,导致我们占用锁的时间会加长,从而导致大量线程的阻塞与等待. 这种场景下我们就会去思考,我们该怎么做才能保证当有多个线程来写日志的时候我们能够在不利用锁的情况下让他们依次排队去写呢?这个时候我们就可以考虑下使用双缓冲队列来完成. 所谓双缓冲

双缓冲队列-减少生产者消费者锁的调用

在生产者-消费者模式中,我们常常会使用到队列,这个队列在多个线程共享访问时存在互斥和竞争操作, 意味着每次访问都要加锁.如何更好的如何减少锁竞争次数呢 ?今天要介绍的双缓冲队列就是个不错的选择. 双缓冲队列就是冲着同步/互斥的开销来的.我们知道,在多个线程并发访问同一个资源的时候,需要特别注意线程的同步问题.稍稍不注意,噢货,程序结果不正确了. 原理 直接上图: 这样为什么会减少锁的调用呢? 举个例子, 两个List,一个用来存,一个用来取.有点迷糊?就是有一个listP从工厂那里得到玩具对象,

多线程操作C++ STL vector出现概率coredump问题及尽量避免锁的双缓冲队列

多线程操作全局变量,必须考虑同步问题,否则可能出现数据不一致, 甚至触发coredump. 前段时间, 遇到一个多线程操作了全局的vector的问题,  程序崩了.场景是这样的:某全局配置参数保存在一个vector中,需要定时更新(更新线程), 另外的工作线程去读取配置. 这种场景是非常普遍的. 在该场景中,程序没有枷锁,概率coredump, 实际情况是,服务跑了一段时间后,必然coredump.   很显然, 更新线程执行clear,然后在push_back操作时, 会导致工作线程的vect

服务器应用--双缓冲队列

在服务器开发中 通常的做法是 把 逻辑处理线程和I/O处理线程分离. 逻辑处理线程:对接收的包进行逻辑处理. I/0处理线程:网络数据的发送和接收,连接的建立和维护. 通常 逻辑处理线程和I/O处理线程是通过数据队列来交换数据,就是生产者--消费者模型. 这个数据队列是多个线程在共享,每次访问都需要加锁,因此如何减少 互斥/同步的开销就显得尤为重要.                                                                         解

HashMap并发出现死循环 及 减少锁的竞争

线程不安全的HashMap, HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环. https://www.cnblogs.com/dongguacai/p/5599100.html https://coolshell.cn/articles/9606.html 减少锁的竞争3种方法: (1)减少锁的持有时间(缩小锁的范围) (2)降低锁的请求频率(降低锁的粒度) (3)放弃使用独占锁,使用并发容器,原子变量,读

MFC利用双缓冲刷新绘图

在VC中进行绘图过程处理时,如果图形刷新很快, 经常出现图形闪烁的现象.利用先在内存绘制,然后 拷贝到屏幕的办法可以消除屏幕闪烁,具体的方法是先在内存 中创建一个与设备兼容的内存设备上下文,也就是开辟一快内 存区来作为显示区域,然后在这个内存区进行绘制图形.在绘制完成后利用 BitBlt函数把内存的图形直接拷贝到屏幕上即可. 具体想实现的是: 在Dialog客户区的一个图片控件(IDC_MAP)中绘制几个动态的点,如果不用双缓冲的技术,在屏幕刷新的时候会有闪烁的现象. CRect rect; C

双缓冲List

前言 声明:本文从Enode之父汤雪华QQ群里提取的,分享给各位小伙伴们供参考. 双缓冲,顾名思义为两个List,此设计用于解决收发分离过程中最大限度避免锁的竞争.有效的降低CPU开销.降低锁的竞争冲突而生. 应用场景 双缓冲List的设计思路,应用在多线程产生消息,单线程处理消息的场景. 设计思路 声明2个list,分别为_requestsWrite,_requestsRead,写入的消息放入_requestsWrite,读取从_requestsRead读取: 再声明一个bool类型的_has

Win32下双缓冲绘图技术

一:双缓冲原理 为了解决窗口刷新频率过快所带来的闪烁问题,利用双缓冲技术进行绘图.所谓双缓冲技术,就是将资源加载到内存,然后复制内存数据到设备DC(这个比较快),避免了直接在设备DC上绘图(这个比较慢).打个简单的比方:有个画家在街边办了一个即时画展,在同一块画布上根据观众的要求画不同的图像,每当有一位观众制定要看什么画时,画家先把之前画布上的东西全部擦干净,再重新绘画.显然有一些经典的画像是大家都想看的,按照以前的老办法,画家每次都要重新画这幅图像,但这种擦了画,画了擦的方式很费时.所以画家想

C++11 实现生产者消费者双缓冲

基础的生产者消费者模型,生产者向公共缓存区写入数据,消费者从公共缓存区读取数据进行处理,两个线程访问公共资源,加锁实现数据的一致性. 通过加锁来实现 1 class Produce_1 { 2 public: 3 Produce_1(std::queue<int> * que_, std::mutex * mt_) { 4 m_mt = mt_; 5 m_que = que_; 6 m_stop = false; 7 } 8 void runProduce() { 9 while (!m_st