C# 双缓冲队列
using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.IO; namespace DoubleQueue { public class UsingEvents { private Queue<int> m_Q1; private Queue<int> m_Q2; private volatile Queue<int> m_CurrentWriteQueue; private Thread m_ProducerThread; private Thread m_ConsumerThread; private ManualResetEvent m_HandlerFinishedEvent; private ManualResetEvent m_UnblockHandlerEvent; private AutoResetEvent m_DataAvailableEvent; private Random m_Random; private StreamWriter m_ProducerData; private StreamWriter m_ConsumerData; private StreamWriter m_Log; public UsingEvents() { m_Q1 = new Queue<int>(); m_Q2 = new Queue<int>(); m_CurrentWriteQueue = m_Q1; m_ProducerThread = new Thread(new ThreadStart(ProducerFunc)); m_ConsumerThread = new Thread(new ThreadStart(ConsumerFunc)); m_HandlerFinishedEvent = new ManualResetEvent(true); m_UnblockHandlerEvent = new ManualResetEvent(true); m_DataAvailableEvent = new AutoResetEvent(false); m_Random = new Random((int)DateTime.Now.Ticks); m_ProducerData = new StreamWriter("Producer.txt"); m_ProducerData.AutoFlush = true; m_ConsumerData = new StreamWriter("Consumer.txt"); m_ConsumerData.AutoFlush = true; m_Log = new StreamWriter("Log.txt"); m_Log.AutoFlush = true; } public void Run() { m_ProducerThread.Start(); m_ConsumerThread.Start(); } public void ProducerFunc() { int data = 0; for (int i = 0; i < 10000; i++) { data += 1; MessageHandler(data); Thread.Sleep(m_Random.Next(0, 2)); } } private void MessageHandler(int data) { m_UnblockHandlerEvent.WaitOne(); m_HandlerFinishedEvent.Reset(); m_CurrentWriteQueue.Enqueue(data); m_ProducerData.WriteLine(data); // logging m_DataAvailableEvent.Set(); m_HandlerFinishedEvent.Set(); } public void ConsumerFunc() { int count; int data; Queue<int> readQueue; while (true) { m_DataAvailableEvent.WaitOne(); m_UnblockHandlerEvent.Reset(); // block the producer m_HandlerFinishedEvent.WaitOne(); // wait for the producer to finish readQueue = m_CurrentWriteQueue; m_CurrentWriteQueue = (m_CurrentWriteQueue == m_Q1) ? m_Q2 : m_Q1; // switch the write queue m_UnblockHandlerEvent.Set(); // unblock the producer count = 0; while (readQueue.Count > 0) { count += 1; data = readQueue.Dequeue(); m_ConsumerData.WriteLine(data); // logging Thread.Sleep(m_Random.Next(0, 2)); } Console.WriteLine("Removed {0} items from queue: {1}", count, readQueue.GetHashCode()); } } } }
背景知识:
所谓双缓冲数据就是两个队列 一个负责从里写入数据,一个负责读取数据,当逻辑线程读完数据后负责将自己的队列和I/O线程的队列进行交换。
这样需要加锁的地方 有两个从队列中写入数据和两个队列进行交换时。如果是一块缓冲区,读,写操作是不分离的,双缓冲区起码节省了单缓冲区时读部分操作互斥/同步的开销。本质是采用空间换时间的优化思路。
缓冲区状态分析:
两个缓冲区分别对应着两个互斥锁locka,lockb,生产者消费者要控制那个缓冲区先要取得对应的锁。
1 并发读写
大多数情况下,生产者控制着某个队列进行写操作,消费者控制着另外一个队列进行读操作。
也就是说,逻辑线程和I/O线程进行着独占操作。这样就大大降低了互斥/同步的开销。
2 缓冲区切换
当消费者将自己的队列(对应locka)读完,立即释放对locka的控制,等待控制lockb。一旦生产者释放lockb,消费者立即控制lockb,开始 读取lockb对应的队列数据。同时生产者控制刚才locka,开始写操作。这样就完成队列交换。
这里注意的操作是无论生产者消费者在完成对自己的队列操作后一定要先释放锁资源再去尝试控制另外的队列。
因为如果生产者操作完成 不释放自己的锁 去尝试控制另外的锁,同时消费者也不释放资源也去尝试控制另外的锁,那么就会出现“死锁”。
队列交换策略分析:
优先保证读,即消费者完成对队列的操作后,立即进行交换。
一开始 两个队列都为空, 交换线程阻塞在生产者的条件变量处,工作线程阻塞在消费者条件变量处,生产者队列数据个数大于1 时 立即发出该条件通知,随即发生队列交换,交换完毕后发现,消费者队列数据个数大于1时 立即发出消费者条件变量通知,此时交换线程阻塞在消费者的条件变量处,工作线程结束等待开始处理数据,处理完毕等待交换线程发出的条件通知,同时发出工作 现场结束工作的条件变量,通知交换线程继续进行工作。
要保证:
1 如果工作线程在处理数据 确保不进行队列交换 同时可以正常向生产者队列放入数据。
2 队列交换完毕后 交换线程 需要立即等待消费者线程执行完毕的条件通知,此时同时可以向生产者队列中放入数据。
3 消费者队列处理完毕需要做两个动作: 1 唤醒交换线程,发送对应的条件变量通知 2 等待交换线程完成一次队列交换的条件通知。