|
Boost : |
From: Dan W. (danw_at_[hidden])
Date: 2003-12-29 19:55:01
Warning: un-tested. Might make a nice addition to the thread library. If
there's any interest in it, I could use some advice on how to test it properly.
In short, this is like a thread safe, alloc-free producer/consumer, shallow
but wide, two-way buffer. Should be very efficient and portable, since it does
not make use of locks or mutexes, and since it avoids
allocation/de-allocation by re-using the two same buffers. The buffer type
is generic (<T>).
Inherently portable, as it uses no platform specific threading mechanisms
whatsoever.
Usage: Create a class for the buffer type wanted, inheriting from abstract
buffer.
Call create_double_buffer<my_buff_t>( new my_buff_t, new my_buff_t ), to
allocate a double buffer on the heap. Optionally feed the returned address
to a smart pointer.
The producer can now call pmy_buff->checkout_buff(), and get a
shared_ptr<my_buff_t>. The producer can now write to this buffer via the
shared_ptr.
Once done, it simply lets the shared_ptr go out of scope. This triggers a
mechanism (via custom deleter calling double_buffer::operator()()) that moves
the buffer along from the producer checkout and makes it available to the
consumer.
The consumer uses an exactly symetrical function 'checkout_data'.
There are two buffers moving around a 6-position 'ring'.
Anyways, the comments get pretty graphic towards the bottom of the file.
Sorry to just paste the code below, I have no web-space readily available;
specially now when everybody here is on holidays. Read on...
#ifndef DOUBLE_BUFFER_HPP
#define DOUBLE_BUFFER_HPP
/*
Pattern "double buffer" is intended as a simple and
fast inter-thread data exchange solution. It allows
a producer and a consumer to exchange fixed-size data
buffers. Two such buffers are managed such that the
producer and consumer may simultaneously be working
to/from either buffer, or such that that one buffer
is being processed while the other waits, or such that
both buffers are in waiting --whether one waiting
for each thread, or the two waiting for one thread.
Its speed stems from the fact that thread safety is
achieved without the use of locks or mutexes, and
from the fact that the buffers are re-used, rather
than repeatedly allocated/de-allocated.
Finally, the fact that it uses no platform-specific
threading mechanisms, makes it inherently portable.
Note also that since only pointer variables to the
template type are instantiated, compiler optimization
should reduce generated template instances to a single
instance of the double_buffer class code.
The free function 'create_double_buffer' allocates the
instance on the heap and returns a pointer. This makes
it convenient for assigning that pointer to shared_ptr
objects held by the producer and consumer, such that
when they go out of scope, their commonly owned double
buffer is deallocated.
Note that, although we speak of 'producer'/'consumer',
the pattern is symmetrical: --i.e. the consumer can
just as well write to the buffers it receives before
returning them and the 'producer' may read them prior
to writing to them.
*/
#include <smart_ptr.hpp>
namespace boost
{
namespace dbf
{
/////////////////////////////////////////////////////////
//Class buffer allows polymorphic pointers to buffers;
//and prevents accidental deletion of the buffers by
//the delete shared_ptr<T>.get(). All you have to do is
//to derive your buffer from (abstract) buffer, e.g.:
// struct my_buff : public boost::dbf::buffer
// { char zzz[666];
// void erase(){ for(int i(666);i--;) zzz[i]=0; }
// }; //that's if you *want* to erase prior to producer
//checkout, each time; otherwise.. void erase(){}.
/////////////////////////////////////////////////////////
class buffer
{
buffer( buffer const &); //no copying
buffer & operator=( buffer const &);
protected:
~buffer(){}
public:
buffer(){}
void erase(){ erase_(); }
private:
virtual void erase_() = 0;
};
/////////////////////////////////////////////////////////
//Class double_buffer<T> is the main one here. T is the type of
//of your buffer class, e.g.: my_buff, as per example above.
//The two "checkout" public functions are what matters to ya...
//The producer checks-out buffers and releases data; the con-
//sumer checks-out data and releases a buffer. Data is short
//for 'buffer with valid data', while Buffer means plain, raw
//buffer. The functions return empty shared_ptr<T>'s if data or
//buffers aren't available, respectively, at time of call.
/////////////////////////////////////////////////////////
template< typename T >
class double_buffer
{
public:
typedef shared_ptr<buffer> pbuff_t;
~double_buffer(){}
pbuff_t checkout_data(){ return checkout_data_(); }
pbuff_t checkout_buff(){ return checkout_buff_(); }
private:
virtual pbuff_t checkout_data_() = 0;
virtual pbuff_t checkout_buff_() = 0;
};
/////////////////////////////////////////////////////////
//use this stand-alone function to create a double-buffer;
//you must allocate the buffers; double_buffer will delete
//them in its destructor.
/////////////////////////////////////////////////////////
template< typename T >
double_buffer<T> * create_double_buffer( T * pbuf1, T * pbuf2 );
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// DETAILS FOLLOW...
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//double_buffer's implementation:
template< typename T >
class db_impl : public double_buffer<T>
{
db_impl & operator=(db_impl const &);
db_impl(db_impl const &); db_impl();
public:
~db_impl();
db_impl( T * pbuf1, T * pbuf2 );
void operator()(T * released);
private:
void reset();
pbuff_t checkout_data_();
pbuff_t checkout_buff_();
T * volatile arr[6];
T * const pbuf1_;
T * const pbuf2_;
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
template< typename T > //free-standing create f'n
double_buffer<T> * create_double_buffer( T * pbuf1, T * pbuf2 )
{
return new db_impl<T>( pbuf1, pbuf2 );
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
template< typename T > //reset
void db_impl<T>::reset()
{
for(int i = 0; i < 6; ++i) arr[i] = 0;
pbuf2_->erase();
pbuf1_->erase();
arr[5] = pbuf2_;
arr[0] = pbuf1_;
if( arr[0] == 0 ) //see notes below
{
arr[0] = pbuf2_;
arr[5] = 0;
}
}
template< typename T > //ctor
db_impl<T>::db_impl
(
T * pbuf1,
T * pbuf2
)
: arr()
, pbuf1_(pbuf1)
, pbuf2_(pbuf2)
{
assert( pbuf1_ && pbuf2_ );
reset();
}
template< typename T > //dtor
db_impl<T>::~db_impl()
{
assert( arr[1]==0 && arr[4]==0 );
delete pbuf2_;
delete pbuf1_;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/* checkout_buff_(), checkout_data_(), and operator()()
constitute the core functionality and implement all
the thread-safety tricks. Two 'tokens' (pointers) are
moved around T * arr[6]: a ruby-shaped circuit where
two of the positions are checkout positions, and the
other four are 'waiting' positions, as follows:
[0] [5]
Buffer waiting Buffer waiting
[1] [4]
Producer checkout Consumer checkout
[2] [3]
Data waiting Data waiting
The tokens circulate counter-clock-wise. The producer
can copy a token from positions 0 or 5 to position 1,
and can clear positions 0 or 5, but not set them.
Once it has moved a token to position 1, it can use
the buffer it points to. When finished, the token in
1 is moved to 3, if 3 is clear, or to 2 otherwise.
The producer can set, but not clear, positions 2 and
and 3; unless 3 is free on second check. The consumer
can copy a token from positions 3 or 2 to position 4,
and can clear positions 3 or 2, but not set them.
Once it has moved a token to position 4, it can use
the data it points to. When finished, the token in
4 is moved to 0, if 0 is clear, or to 5 otherwise.
The consumer can set, but not clear, positions 5 and
and 0; unless 0 is free on second check, in which
rare case it will move 5 to 0 and clear 5, very, very
carefully... Some of the conditionals may appear to
be redundant; --they have to be so to be thread-safe.
Almost forgot, when consumer is done with data, it
first calls user function erase() on it.
Note that the checkout functions return shared_ptr<>
objects, which it initializes with "this" in ctor's
second argument...
When the client (producer or consumer) is done with
the buffer (shared_ptr<>'s go out of scope) the last
shared_ptr calls this->operator()(T*), like a custom
deleter. Operator()(), rather than free the memory,
simply moves the token along the circuit. At first it
does not know whether it's a return of a data-filled
buffer, or a used buffer. It finds out by comparing
the address released to those present in positions 1
and 4. This is safe to do.
*/
template< typename T > //producer checkout
double_buffer<T>::pbuff_t db_impl<T>::checkout_buff_()
{
T * the_buffer = 0;
assert( ! arr[1] );
if( ! arr[2] )
{
arr[1] = arr[0];
the_buffer = arr[1];
if( arr[0] && arr[5] )
{
arr[0] = arr[5];
arr[5] = 0;
}
else
{
arr[0] = 0;
}
}
return pbuff_t( the_buffer, this );
}
template< typename T > //consumer checkout
double_buffer<T>::pbuff_t db_impl<T>::checkout_data_()
{
T * the_buffer = 0;
assert( ! arr[4] );
if( ! arr[5] )
{
arr[4] = arr[3];
the_buffer = arr[4];
if( arr[3] && arr[2] )
{
arr[3] = arr[2];
arr[2] = 0;
}
else
{
arr[3] = 0;
}
}
return pbuff_t( the_buffer, this );
}
template< typename T > //"deleter" (moves token along)
void db_impl<T>::operator()(T * released)
{
assert( released );
if( released == arr[1] ) //if a producer release
{
if( arr[3] )
arr[2] = released;
else arr[3] = released;
if( !arr[3] && arr[2] )
{
arr[2] = 0;
arr[3] = released;
}
arr[1] = 0;
}
else //ought to be a consumer release (arr[4])
{
assert( released == arr[4] );
arr[4]->erase(); //clears buffer
if( arr[0] ) arr[5] = released;
else arr[0] = released;
if( !arr[0] && arr[5] )
{
arr[5] = 0;
arr[0] = released;
}
arr[4] = 0;
}
}
}
}
#endif
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk