Я реализовал кольцевой буфер без блокировки один производитель-один-потребитель. Эталонная реализация — та, что находится по адресу: http://home.comcast.net/~lang.dennis/code/ring/ring.html (Базовая реализация, первый листинг сверху).
РЕДАКТИРОВАТЬ: Для справки оригинальный код:
template <class T, size_t RingSize>
class RingBuffer
{
public:
RingBuffer(size_t size = 100)
: m_size(size), m_buffer(new T[size]), m_rIndex(0), m_wIndex(0)
{ assert(size > 1 && m_buffer != NULL); }
~RingBuffer()
{ delete [] m_buffer; };
size_t Next(size_t n) const
{ return (n+1)%m_size; }
bool Empty() const
{ return (m_rIndex == m_wIndex); }
bool Full() const
{ return (Next(m_wIndex) == m_rIndex); }
bool Put(const T& value)
{
if (Full())
return false;
m_buffer[m_wIndex] = value;
m_wIndex = Next(m_wIndex);
return true;
}
bool Get(T& value)
{
if (Empty())
return false;
value = m_buffer[m_rIndex];
m_rIndex = Next(m_rIndex);
return true;
}
private:
T* m_buffer;
size_t m_size;
// volatile is only used to keep compiler from placing values in registers.
// volatile does NOT make the index thread safe.
volatile size_t m_rIndex;
volatile size_t m_wIndex;
};
модификацияЯ храню индексы чтения и записи в локальных переменных и использую только локальные переменные в выражениях. Я обновляю их, прежде чем вернуться из функций (получить и поставить).
Для наглядности функция get:
bool Get(T& value)
{
size_t w=m_wIndex;
size_t r=m_rIndex;
if (Empty(w,r))
return false;
value = m_buffer[r];
//just in case the compiler decides to be extra smart
compilerbarrier();
m_rIndex = Next(r);
return true;
}
Затем я создал отдельные потоки производителя и потребителя:
Нить производителя
uint64_t i = 0;
while (i <= LOOPS) {
if (buf.put(i)) {
i += 1;
}
}
consumerthread.join(); //pseudocode: wait for the consumer to finish
Поток потребительской нити:
uint64_t i=0;
while (i < LOOPS) {
buf.get(i);
}
Producer помещает целые числа [0, LOOPS] в буфер, и потребитель получает их по одному из буфера, пока, наконец, не получит целочисленное значение LOOPS, и цикл потребителя не завершится. Обратите внимание, что размер буфера намного меньше, чем ПЕТЛИ.
Если я оставлю ключевое слово volatile для индексов чтения и записи, все будет работать как шарм. Цикл Consumer завершается, а производитель возвращается.
Но если я уберу ключевое слово volatile, потребитель никогда не вернется.
Как ни странно, этот потребительский цикл завершается:
uint64_t i=0;
while (i < LOOPS) {
buf.get(i);
fprintf(stderr,"%lu\n",i);
}
И этот тоже заканчивается:
uint64_t i=0, j=0;
while (j < LOOPS) {
if(buf.get(i)) {
j=i;
}
}
Что происходит? Я скомпилировал код, используя gcc 4.8.4 с установленным флагом -O3 на 64-битной машине intel (i3) под управлением Ubuntu.
Так как проблема возникает только при удалении volatile
эта проблема, скорее всего, вызвана оптимизацией компилятора, которая оптимизирует чтение m_wIndex в потоке читателей. Компилятор считает, что он знает все, что может изменить это значение, поэтому нет смысла читать его из памяти более одного раза.
Добавление вызова к fprintf побеждает эту оптимизацию, потому что вполне возможно, что fprintf может изменить эту переменную.
Добавление j
сделал ваш код более сложным, а также, кажется, победить оптимизацию.