007. Producer consumer queue (read and write first and last)

Keywords: Windows

The so-called producer consumer queue is the queue that a thread reads and a thread writes
What we have implemented here is an unlocked producer consumer queue. The key points of the algorithm are as follows:
1.pop only modifies the head node, and push only modifies the tail node
2. Ensure that head and tail are not empty, because once head and tail are empty, you must rewrite head and tail at the same time when you push

Special note:
1. Theoretically, only one thread is allowed to read and one thread is allowed to write
2. Multiple read threads can be allowed to apply for reading, but only one real read is allowed at each time. Similarly, multiple write threads can be allowed to apply for writing, but only one write is allowed at each time. For specific users, please refer to the following implementation

Analysis description:

  1. Change new NODE to member memory pool to allocate memory. Read and write will be very trivial. Instead, allocate memory in the memory pool to avoid a lot of memory fragmentation and improve efficiency.

Compare flip queue
Flip queue: more suitable for one producer (without outer lock) and multiple consumers, or one consumer (without outer lock) and multiple producers
First and last reading and writing: more suitable for multiple producers and multiple consumers

Flip queue https://mp.csdn.net/posted/103961636

/*******************************************************************
** Filename: producer · consumer · queue. H
** Description:
** Application:
********************************************************************/

#pragma once
#include "Trace.h"
#include "Common.h"
#include "MemPool.h"

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
The so-called producer consumer queue is the queue that a thread reads and a thread writes
 What we have implemented here is an unlocked producer consumer queue. The key points of the algorithm are as follows:

1.pop Modify only the head node and push only the tail node
2.Ensure that head and tail are not empty, because once head and tail are empty, you must rewrite head and tail at the same time when you push

Special note:
1.Theoretically, only one thread is allowed to read and one thread is allowed to write
2.Multiple read threads can be allowed to apply for reading, but only one real read is allowed at each time. Similarly, multiple write threads can be allowed to apply for writing, but only one write is allowed at each time. For specific users, please refer to the following implementation
*/
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename _DATA>
class producer_consumer_queue
{
	struct NODE
	{
		_DATA   data;
		NODE *  next;
	};

	NODE *  m_pHead;
	NODE *  m_pTail;

	volatile unsigned long    m_nPushCount;
	volatile unsigned long    m_nPopCount;
	tstring m_strName;	// Name, used to debug the display owner

public:
	producer_consumer_queue() : m_nPushCount(0), m_nPopCount(0)
	{
		//m_pHead = new NODE;
		m_pHead = (NODE*)MT_Allocator::getInstance().allocate(sizeof(NODE));
		assert(m_pHead);

		// Ensure that Head and Tail are not empty
		m_pHead->next = 0;
		m_pTail = m_pHead;
	};

	virtual ~producer_consumer_queue()
	{
		_DATA temp_data;
		while (pop(temp_data)) {}

		if (m_pHead)
		{
			delete m_pHead;
			m_pHead = 0;
			m_pTail = 0;
		}
	}


	// Only one thread push is allowed at the same time
	void push(_DATA & data)
	{
		//NODE * pNode = new NODE;
		NODE * pNode = (NODE*)MT_Allocator::getInstance().allocate(sizeof(NODE));
		pNode->data = data;
		pNode->next = 0;

		m_pTail->next = pNode;
		m_pTail = pNode;

		++m_nPushCount;

		static DWORD s_dwTick = 0;
		if (m_nPushCount - m_nPopCount > 1024 && GetTickCount() - s_dwTick>10000)
		{
			s_dwTick = GetTickCount();
			DWORD dwPackCounts = m_nPushCount - m_nPopCount;
			if (dwPackCounts>100000)
			{
				WarningLn(0, "[" << m_strName.c_str() << "]Queue busy, Pressing stack" << m_nPushCount << "second, Stack out" << m_nPopCount << "second, Stack length" << dwPackCounts << ", Data length" << sizeof(_DATA) << "byte");
			}
			else
			{
				WarningLn(0, "[" << m_strName.c_str() << "]Queue busy, Pressing stack" << m_nPushCount << "second, Stack out" << m_nPopCount << "second, Stack length" << dwPackCounts << ", Data length" << sizeof(_DATA) << "byte");
			}
		}

	}

	// Only one thread pop is allowed at the same time
	bool pop(_DATA & data)
	{
		if (m_pHead->next != 0)
		{
			++m_nPopCount;

			NODE * pNode = m_pHead;
			m_pHead = m_pHead->next;
			data = m_pHead->data;
			//delete pNode;
			MT_Allocator::getInstance().deallocate(pNode);

			return true;
		}

		return false;
	}

	unsigned long count()
	{
		return m_nPushCount - m_nPopCount;
	}

public:

	bool setName(const tchar * pstrName)
	{
		if (pstrName == nullptr) { return false; }

		m_strName = pstrName;
		return true;
	}
	const tchar * getName()
	{
		return m_strName.c_str();
	}
};

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
Producer consumer queue with read-write rights control
 That is to say, multiple read threads are allowed to apply for reading, but they need to obtain read rights before reading, and multiple write threads are also allowed to apply for writing, but they need to obtain write rights before writing
 Usage:
1.Call push and pop ex directly
2.Call the acquire? Read? Permission and pop functions, but pay attention to the thread exclusive logic
*/
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename _DATA>
class producer_consumer_queue_ex : public producer_consumer_queue<_DATA>
{
	volatile	LONG  m_nReadBlock;		// Read mark
	volatile    LONG  m_nWriteBlock;    // Writing signs

public:
	producer_consumer_queue_ex() : producer_consumer_queue(), m_nReadBlock(0), m_nWriteBlock(0)
	{
	}

	// Writing with rights
	bool push_ex(_DATA & data)
	{
		AtomLock __lock__(&m_nWriteBlock);
		producer_consumer_queue::push(data);
		return true;
	}

	// Reading with rights
	bool pop_ex(_DATA & data)
	{
		if (count() == 0)  // Add a judgment to prevent too many calls to atom operation fearless lock memory bus
		{
			return false;
		}

		AtomLock __lock__(&m_nReadBlock);
		return producer_consumer_queue::pop(data);
	}
};

Test:

#include "stdafx.h"
#include <iostream>

#include <WinSock2.h>
#include <windows.h>

#include "Thread.h"
#include "WorkerThreadPool.h"
#include "producer_consumer_queue.h"
#include "Api.h"
using namespace std;


producer_consumer_queue_ex<size_t> lp;
//Producer
class RunnableAdd : public IRunnable
{
public:
	void run()
	{
		for (size_t i = 0; i < 10000; i++)
		{
			lp.push_ex(i);
		}
	};

	void release(){};
};

//Consumer
class RunnableGet : public IRunnable
{
public:
	void run()
	{
		size_t num = 0;
		for (size_t i = 0; i < 10000; i++)
		{
			lp.pop_ex(num);
		}
	};

	void release(){};
};


void Fun()
{
	const int gThreadId = 2;
	HANDLE hd[gThreadId];
	Thread *pThread = nullptr;

	for (size_t i = 0; i < 10; i++)
	{
		size_t num = 0;
		while (lp.pop_ex(num)) { }

		for (size_t i = 0; i < 10000; i++)
		{
			lp.push_ex(i); // Add data first to ensure that every time you access data
		}

		pThread = WorkerThreadPool::getInstancePtr()->add(new RunnableAdd);
		if (pThread == nullptr) { return; }
		hd[0] = pThread->GetHandle();

		pThread = WorkerThreadPool::getInstancePtr()->add(new RunnableGet);
		if (pThread == nullptr) { return; }
		hd[1] = pThread->GetHandle();

		DWORD result = ::WaitForMultipleObjects(gThreadId, hd, true, INFINITE);
		cout << "result: " << result << "count: " << lp.count() << endl;
	}

	cout << "End" << endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	new WorkerThreadPool;
	CreateReactor();

	Fun();
	system("pause");
	return 0;
}

Result:

Published 80 original articles, won praise 16, visited 90000+
Private letter follow

Posted by naskoo on Tue, 14 Jan 2020 02:05:27 -0800