I'm trying to implement a multiple producer (via interrupt), single consumer (via application thread) queue on an embedded target in "MpscQueue.h" below.
I'm wondering if I can safely remove the some of the volatile
usage below (see inline questions). I would also consider using volatile std::array
in place of the C-style buffer_[]
shown below, but I was unsure if I could trust its implementation to match the intent of the below. A third alternative would be to mark the MpscQueue object itself as volatile
and qualify the relevant methods volatile
, but it's not clear if that would result in all member variables (and what they point to, in the case of pointers) being treated as volatile.
Any guidance on this?
template<typename T, uint32_t depth>
class MpscQueue
{
public:
MpscQueue(void);
bool push(T& t);
bool pop(T* const t);
private:
T volatile buffer_[depth]; // Q1: is volatile unnecessary if never access buffer_[]?
T volatile* const begin_; // Q2: is volatile unnecessary if never access value
T volatile* const end_; // via begin_/end_?
T volatile* head_; // volatile required so that thread always checks value
T volatile* volatile tail_; // Q3: is 'T volatile' required so that ISR accounts
// for other ISRs when setting value?
// Q4: is '* volatile' required so that ISR accounts
// for other ISRs when checking pointer?
};
template<typename T, uint32_t depth>
MpscQueue<T, depth>::MpscQueue(void) :
begin_(&buffer_[0]),
end_(&buffer_[depth - 1]),
head_(begin_),
tail_(begin_)
{}
template<typename T, uint32_t depth>
bool MpscQueue<T, depth>::push(T& t)
{
// "Multiple producer" ISRs can use this function to push at tail
// Pseudo-code: if not full, *(tail_++) = t
}
template<typename T, uint32_t depth>
bool MpscQueue<T, depth>::pop(T* const t)
{
// "Single consumer" thread can use this function to pop at head
// Pseudo-code: if not empty, *t = *(head_++)
}
Edit: To focus the question in the right direction, let me clarify that I have taken care of thread-safety, and it is not part of the question here.
Because this queue is single consumer, there is no thread safety required on the read/pop side. On the write/push side, thread safety among interrupts will be handled by setting all relevant interrupts to the same priority level (otherwise, a lock will be used).