Upgrading 6.x.x

From liblfds.org
Jump to navigation Jump to search

Introduction

Almost always, it is not possible to use multiple versions of the same library concurrently in a single application and so users expect when a new version is released to have to modify their code to work with the new release.

This is not the case with liblfds. Each version starting with (and including) 6.0.1 and 6.1.1 can be used concurrently in the same application. There is in fact no need to upgrade. All existing code can continue to use the version it was written against, while new code can be written against the new release.

However, it may be that end users wish to upgrade, to be able to remove a dependency on an earlier version, or to obtain improvements provided by a new release.

Upgrading

Release 7.0.0 is fundamentally different to the series 6 releases because the new release performs no memory allocation. There is also a minor change relating to data structure initialization, and a new concern which is to do with passing around a per-thread psuedo-random number generator state to functions which need it. Every function call is pretty much a bit different to before, but the purpose of the functions is unchanged, so the mapping of old functions to new remains the same - with one exception; the ringbuffer API is now sane, and so is simply two calls, read and write. Before it was mildly insane and much more complicated. Finally, the queue now permits elements to be deallocated after dequeuing.

Memory Allocation

Release 7.0.0 performs no memory allocation. Rather, users place liblfds data structure state structures and data structure element state structures in their own structures, allocate their own structures, and then pass pointers to liblfds structures into liblfds functions.

If you understood that, I have a tax form yu can help me fill in :-)

In the series 6 releases, users performed no allocation. They would call functions such as lfds611_stack_new, which would allocate and initialize state. The protoype is this;

int lfds611_stack_new( struct lfds611_stack_state **ss, lfds611_atom_t number_elements );

The pointer-to-pointer of the first argument being so to allow the allocation of store by the function.

In 7.0.0, the equivelent function is lfds700_stack_init_valid_on_current_logical_core, where the prototype is;

void lfds700_stack_init_valid_on_current_logical_core( struct lfds700_stack_state *ss, void *user_state );

Here we see the first argument is now only a pointer. The user must allocate store - the function will only initialize that store.

So we see in the first case, the user declares a struct lfds611_stack_state * on the stack and then call lfds611_stack_new; in the latter, the user declares a struct lfds700_stack_state on the stack (i.e. the struct itself) and passes a pointer to this to lfds700_stack_init_valid_on_current_logical_core for initialization.

Old style;

struct lfds611_stack_state
  *ss;

lfds611_stack_new( &ss, 100 );

New style;

struct lfds700_stack_state
  ss;

lfds700_stack_init_valid_on_current_logical_core( &ss, NULL );

This difference is fully implemented throughout the library and is the main change.

Data Structure Initialization

Passing a data structure state to its init function initializes that state but that initialization is and is only valid for the logical core upon which it occurs.

The macro LFDS700_MISC_MAKE_VALID_ON_CURRENT_LOGICAL_CORE_INITS_COMPLETED_BEFORE_NOW_ON_ANY_OTHER_LOGICAL_CORE is used to make the initialization valid on other logical cores and it will make the initialization valid upon and only upon the logical core which calls the macro.

Expected use is that a thread will initialize a data structure, pass a pointer to its state to other threads, all of whom will then call LFDS700_LIBLFDS_MAKE_USABLE_TO_CURRENT_LOGICAL_CORE_INITS_PERFORMED_BEFORE_NOW_ON_ANY_OTHER_LOGICAL_CORE.

Per-Thread Psuedo-Random Number Generator State

Most API functions which perform lock-free operations now take an argument struct lfds700_misc_prng_state *ps.

This is a pointer to a PRNG state. These states are not thread safe - only one thread can use any one of them at a time; and for performance reasons (with regard to efficient memory accesses and effective caching by the logical processors), there absolutely and definitively should be one such state per thread, which is used by and only by that thread, which is why the init function does not have the usual "valid_on_current_logical_core" postfix - there's not support for making these states, once initialized, valid on other logical cores.

This is an example API which uses a PRNG state;

void lfds700_stack_push( struct lfds700_stack_state *ss,
                         struct lfds700_stack_element *se,
                         struct lfds700_misc_prng_state *ps );

Actually setting up a lfds700_misc_prng_state is as simple as it can be. Allocate one (stack or heap or whatever special magic you might do) and then call lfds700_misc_prng_init, like so;

struct lfds700_misc_prng_state
  ps;

lfds700_misc_prng_init( &ps );

That's it.

No cleanup is required. Remember to keep the store allocated for the structure in scope until you stop using it.

Ringbuffer

What can you say? The 6.x.x API was techncially more functional, but was incomprehensible.

The way the ringbuffer works now is this : the ringbuffer instance is initialized with its set of elements. The caller then has a read function and a write function, and they do what you expect. With regard to reading, each element will only be read by one thread. So if there are say ten writers, that's fine, and then say five readers, also fine - but what will happen is that of all the unread elements, when a reader reads, that reader is the only thread which reads that element.

Queue

It is now safe to deallocate elements after they have been dequeued.