vsync / smr / ebr.h
Epoch Based Reclamation (EBR) SMR scheme.
EBR relies on global and local epoch counters to determine when it is safe to reclaim memory.
- global epoch =
e+1=> all active threads must have a local epoch in {e,e+1} - global epoch =
e+1=> nodes retired on epoche-1are safe to recycle.Example:
#include <vsync/queue/unbounded_queue_lf.h>
#include <vsync/smr/ebr.h>
#include <pthread.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#define IT 10000
#define N 12
typedef struct data_s {
vsize_t id;
} data_t;
vebr_t g_vebr;
vqueue_ub_t g_queue;
pthread_mutex_t g_lock;
static inline void
lock_acq(void *arg)
{
pthread_mutex_t *lock = (pthread_mutex_t *)arg;
int ret = pthread_mutex_lock(lock);
assert(ret == 0);
}
static inline void
lock_rel(void *arg)
{
pthread_mutex_t *lock = (pthread_mutex_t *)arg;
int ret = pthread_mutex_unlock(lock);
assert(ret == 0);
}
smr_lock_lib_t g_lock_lib = {lock_acq, lock_rel, &g_lock};
void
free_cb(smr_node_t *node, void *args)
{
vqueue_ub_node_t *qnode = V_CONTAINER_OF(node, vqueue_ub_node_t, smr_node);
free(qnode);
(void)args;
}
void
retire_cb(vqueue_ub_node_t *qnode, void *args)
{
vebr_thread_t *thrd = args;
vebr_retire(&g_vebr, thrd, &qnode->smr_node, free_cb, NULL);
}
void
destroy_cb(vqueue_ub_node_t *qnode, void *args)
{
free(qnode);
(void)args;
}
vatomic8_t g_stop = VATOMIC_INIT(0);
void
reclaim(void)
{
while (vatomic8_read(&g_stop) == 0) {
vsize_t count = vebr_recycle(&g_vebr);
if (count > 0) {
printf("%zu node(s) were reclaimed\n", count);
}
}
}
void *
run(void *args)
{
vebr_thread_t thread;
data_t *data = NULL;
vsize_t tid = (vsize_t)args;
if (tid == 0) {
reclaim();
} else {
vebr_register(&g_vebr, &thread);
for (vsize_t i = 0; i < IT; i++) {
vebr_enter(&g_vebr, &thread);
data = vqueue_ub_deq(&g_queue, retire_cb, &thread);
if (data == NULL) {
data = malloc(sizeof(data_t));
data->id = i;
vqueue_ub_node_t *qnode = malloc(sizeof(vqueue_ub_node_t));
printf("[T%zu] enq %zu\n", tid, data->id);
vqueue_ub_enq(&g_queue, qnode, data);
} else {
printf("[T%zu] deq %zu\n", tid, data->id);
free(data);
}
vebr_exit(&g_vebr, &thread);
}
vebr_deregister(&g_vebr, &thread);
}
return NULL;
}
int
main(void)
{
pthread_t threads[N];
data_t *data = NULL;
int ret = pthread_mutex_init(&g_lock, NULL);
assert(ret == 0);
vebr_init(&g_vebr, g_lock_lib);
vqueue_ub_init(&g_queue);
for (vsize_t i = 0; i < N; i++) {
pthread_create(&threads[i], NULL, run, (void *)i);
}
for (vsize_t i = 1; i < N; i++) {
pthread_join(threads[i], NULL);
}
vatomic8_write(&g_stop, 1);
pthread_join(threads[0], NULL);
/* dequeue all remaining nodes, to be able to destroy data */
while (data = vqueue_ub_deq(&g_queue, destroy_cb, NULL), data) {
free(data);
}
/* destroy the queue to destroy the remaining sentinel */
vqueue_ub_destroy(&g_queue, destroy_cb, NULL);
vebr_destroy(&g_vebr);
ret = pthread_mutex_destroy(&g_lock);
assert(ret == 0);
return 0;
}
References:
Keir Fraser - Practical lock-freedom
Functions
| Function | Description |
|---|---|
| vebr_init | Initializes the given object ebr. |
| vebr_destroy | Destroys the resources associated with given ebr object. |
| vebr_register | Registers and initializes the given ebr_thrd. |
| vebr_deregister | Deregisters the given ebr_thrd. |
| vebr_enter | Marks the beginning of a critical section, indicates that a reader is active. |
| vebr_exit | Marks the end of a critical section, indicates that a reader is inactive. |
| vebr_retire | Defers reclamation of objects until it is safe to do so. |
| vebr_recycle | Recycles/frees nodes that can safely be freed. |
| vebr_sync | Synchronizes active threads. |
Function vebr_init
static void vebr_init(vebr_t *ebr, smr_lock_lib_t lock_lib)
Initializes the given object ebr.
Parameters:
ebr: address of vebr_t object.lock_lib: smr_lock_lib_t object.
Function vebr_destroy
static void vebr_destroy(vebr_t *ebr)
Destroys the resources associated with given ebr object.
Parameters:
ebr: address of vebr_t object.
Function vebr_register
static void vebr_register(vebr_t *ebr, vebr_thread_t *ebr_thrd)
Registers and initializes the given ebr_thrd.
Note: each thread must be associated with a unique
ebr_thrdthat lives till the thread deregisters.
Parameters:
ebr: address of vebr_t object.ebr_thrd: address of vebr_thread_t object.
Function vebr_deregister
static void vebr_deregister(vebr_t *ebr, vebr_thread_t *ebr_thrd)
Deregisters the given ebr_thrd.
Note: the thread is associated with the given
ebr_thrdshould not attempt to access the givenebrinstance after this call. Precondition:vebr_register.
Parameters:
ebr: address of vebr_t object.ebr_thrd: address of vebr_thread_t object.
Function vebr_enter
static void vebr_enter(vebr_t *ebr, vebr_thread_t *ebr_thrd)
Marks the beginning of a critical section, indicates that a reader is active.
Postcondition: vebr_exit
Parameters:
ebr: address of vebr_t object.ebr_thrd: address of vebr_thread_t object.
Function vebr_exit
static void vebr_exit(vebr_t *ebr, vebr_thread_t *ebr_thrd)
Marks the end of a critical section, indicates that a reader is inactive.
Precondition: vebr_enter
Parameters:
ebr: address of vebr_t object.ebr_thrd: address of vebr_thread_t object.
Function vebr_retire
static void vebr_retire(vebr_t *ebr, vebr_thread_t *ebr_thrd, smr_node_t *smr_node, smr_node_destroy_fun destructor, void *destructor_args)
Defers reclamation of objects until it is safe to do so.
Note: must be called within the critical section. This is important to avoid data race on the local queue when recycle is being called by another thread.
Parameters:
ebr: address of vebr_t object.ebr_thrd: address of vebr_thread_t object.smr_node: address of smr_node_t object.dest: address of callback function used for destroying the retiredsmr_node.
Function vebr_recycle
static vsize_t vebr_recycle(vebr_t *ebr)
Recycles/frees nodes that can safely be freed.
Assesses whether all active threads have observed the current epoch_global. If so, advances the global epoch: epoch_global = epoch_global + 1. And frees nodes associated with with epoch_global - 1, i.e. epoch_global * - 2
Note: It is recommended that you call this function periodically in its own dedicated thread that does not interfere with the readers. As this function acquires a lock and might block the calling thread for a while.
Parameters:
ebr: address of vebr_t object.
Returns: count of recycled nodes.
Function vebr_sync
static void vebr_sync(vebr_t *ebr)
Synchronizes active threads.
The caller can free nodes detached before this call directly after this function returns. In order to give this guarantee. The observed global epoch global_epoch must be advanced twice global_epoch = global_epoch + 2 or all threads are observed as inactive. If a thread is active it must have already observed global_epoch + 1 by the return of this function.
// detach node
vebr_sync(&ebr);
free(node);
Note: this function is designed for the above use-case, and it should not be used unless it is absolutely. This function acquires a lock and uses spin-loops. Thus, it is recommended to use
vebr_retireandvebr_recycleinstead.
Parameters:
ebr: address of vebr_t object.