问题
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).
回答1:
As written the code is not interrupt-safe -- if an interrupt occurs while the main thread is doing a read/pop, you have a race condition, and the data structure is likely to be corrupted. The way to fix this is to block interrupts in the main the thread whenever it does a read/pop. If you do that (and the functions that block/unblock interrupts are memory barrieres), the volatiles all become irrelevant and can be removed.
Volatile is pretty much useless for thread synchronization -- its primary use is for interacting with memory-mapped devices.
回答2:
Here's what I will do regarding the private member variables, with rationale in comments:
T volatile buffer_[depth]; // will never touch buffer_[] via array handle,
// but don't want compiler to optimize it out;
// and technically, the elements are volatile due to push()
T volatile* const begin_; // buffer_[] has elements of type 'T volatile', so
// keep type of pointer consistent with what it points to
T volatile* const end_; // "
T volatile* volatile head_; // value must be volatile, as unknown ISR thread will touch;
// also, keep type of pointer consistent
// pointer should be volatile since ISRs will read outside
// of "main" thread context
T volatile* volatile tail_; // value should be volatile since multiple ISRs will touch;
// also, keep type of pointer consistent
// pointer should be volatile since multiple ISRs will touch
If I used a std::array
instead of buffer_[]
, I'm not sure how I would enforce that not only the elements of the array, but also the underlying pointers/iterators were also volatile. E.g., std::array<T volatile, uint32_t depth> volatile
?
If I made the whole MpscQueue
object volatile, I'm not sure how I would enforce that the 'volatility' would not only trickle down to the pointers themselves (i.e. * volatile
), but also to the pointed-to values (i.e. T volatile* volatile
instead of just T* volatile
).
来源:https://stackoverflow.com/questions/60139241/volatile-member-variables-vs-volatile-object