2007/02/05

Ring buffers and anonymous functions

Yesterday I was working on o project I've started sometime ago, for sending and receiving GSM audio over a socket (a client/server skype). I was implementing a ring buffer, to be used by the encoder/decoder, the audio library (PortAudio) and the socket reading/writing function, when I started to wonder how could I make it really concurrent, and easy to understand.

The first ring buffer was a C++ struct which simply copied the values inside and outside a pre-alocated memory area, in a thread safe way. The minor input / output for the GSM codec is 33 bytes / 160 shorts (320 bytes), so I started with a templated buffer which stored N values of type T. There was two functions for accessing the buffer:

1 void consume(value_type &buffer);
2 void produce(const value_type &buffer);

These functions consume/produce one value_type at each call, copying the value inside 'buffer' to the write position of the ring buffer, or copying the value on the read position of the ring buffer inside 'buffer'. But that was not good enought.

The second try involved a way to use the ring buffer directly, without copying buffers around. For that these functions arised:

1 void consume_ptr(const value_type **buf);
2 void consume_commit();
3
4 void produce_ptr(value_type **buf);
5 void produce_commit();

The functions ending with 'ptr(..)' have a special meaning attached: they do not copy buffers, but return the pointer to the actual position of the ring buffer - according to the requested operation. If you want to write to the ring buffer, 'produce_ptr(..)' returns a pointer to a buffer which must be filled with values. When done, user must call 'produce_commit()' so that pointer is forwarded and sleeping threads are awaked. The 'consume_ptr(..)' function does the same, but in opposite way: buffer must be read, and after that, 'consume_commit()' must be called.

But if I want to pass a function to the "produce" and "consume" of my ring buffer, which would be called receiving the needed pointer as argument, and whose return would automagically call the respective commit function?

You can make this in C++, with functional objects. You'll get an interface like this:

1 template <class T>
2 struct Ringbuffer
3 {
4 template <class F>
5 void produce(F f);
6
7 template <class F>
8 void consume(F f);
9 }

To use this feature, you'll need to make a code like this:

1 [...]
2 struct ring_read_abc
3 {
4 typedef Ringbuffer<short> R;
5
6 void operator()(R::value_type *v)
7 { /* send v via socket */ }
8 }
9 [...]
10 int tmp1, tmp2;
11 ring_read_abc::R rb;
12 rb.produce(ring_read_abc());
13 [...]

The problem about this approach is simple: looking at the above example, you can see that the operator() has no access to the context where produce was called. It cannot access tmp1 or tmp2, for instance. For that, you'll need to pass the arguments in the constructor of the ring_read_abc struct. This "glue code" can beat very hard concepts of "separation of concerns" when you want to nest two or more calls to functional objects, using the interface above.

The fourth interface goes beyond C++, and uses some features only present in functional languages. For that end, the Ringbuffer could have the same functions "produce" and "consume", each one receiving a function as argument, which made the interface like this (using Objective Caml syntax):

1 type buffer = int array
2
3 val consumer: (buffer -> unit) -> unit
4 val producer: (buffer -> unit) -> unit

The great advantage of such interface is that nested functions do not lose context. That means, you keep current bindings when you call another anonymous function. For instance:

1 [..]
2 consume
3 begin fun inp ->
4 produce
5 begin fun out ->
6 write inp out
7 end
8 end
9 [..]

In this case, 'consume' gets called and receives the first block "begin fun ... end", which is a function, as argument. It then calls this argument, that calls the 'produce' function, which will eventually call function 'write'.

The reader should notice that 'inp' in being passed by context, as any other value might have been passed. Functional objects, OTOH, do not have this flexibility.

As a last note, I would say programming as an everyday basis using a language (in this case, C++) bring you a great deal of confidence about it, but when you got some degree of knowledge about other forms of programming (in this case, functional programming), sometimes you want a bit more of what the language can provide you.

1 Comments:

At 17/2/07 15:55, Blogger tautologico said...

Coincidentemente, comecei a postar em inglês também, só que em outro blog.

Quanto a C++, prefiro não chegar perto disso enquanto puder evitar :)

 

Postar um comentário

<< Home