/* epoch.c - epoch based memory reclamation */
/* $OpenLDAP$ */
/* This work is part of OpenLDAP Software .
*
* Copyright 2018-2024 The OpenLDAP Foundation.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted only as authorized by the OpenLDAP
* Public License.
*
* A copy of this license is available in the file LICENSE in the
* top-level directory of the distribution or, alternatively, at
* .
*/
/** @file epoch.c
*
* Implementation of epoch based memory reclamation, in principle
* similar to the algorithm presented in
* https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-579.pdf
*
* Not completely lock-free at the moment.
*
* Also the problems with epoch based memory reclamation are still
* present - a thread actively observing an epoch getting stuck will
* prevent managed objects (in our case connections and operations)
* from being freed, potentially running out of memory.
*/
#include "portable.h"
#include "lload.h"
#include
/* Has to be >= 3 */
#define EPOCH_MASK ( 1 << 2 )
#define EPOCH_PREV(epoch) ( ( (epoch) + EPOCH_MASK - 1 ) % EPOCH_MASK )
#define EPOCH_NEXT(epoch) ( ( (epoch) + 1 ) % EPOCH_MASK )
struct pending_ref {
void *object;
dispose_cb *dispose;
struct pending_ref *next;
};
ldap_pvt_thread_rdwr_t epoch_mutex;
static epoch_t current_epoch;
static uintptr_t epoch_threads[EPOCH_MASK];
static struct pending_ref *references[EPOCH_MASK];
void
epoch_init( void )
{
epoch_t epoch;
current_epoch = 0;
for ( epoch = 0; epoch < EPOCH_MASK; epoch++ ) {
assert( !epoch_threads[epoch] );
assert( !references[epoch] );
}
ldap_pvt_thread_rdwr_init( &epoch_mutex );
}
void
epoch_shutdown( void )
{
epoch_t epoch;
struct pending_ref *old, *next;
for ( epoch = 0; epoch < EPOCH_MASK; epoch++ ) {
assert( !epoch_threads[epoch] );
}
/*
* Even with the work in epoch_leave(), shutdown code doesn't currently
* observe any epoch, so there might still be references left to free.
*/
epoch = EPOCH_PREV(current_epoch);
next = references[epoch];
references[epoch] = NULL;
for ( old = next; old; old = next ) {
next = old->next;
old->dispose( old->object );
ch_free( old );
}
epoch = current_epoch;
next = references[epoch];
references[epoch] = NULL;
for ( old = next; old; old = next ) {
next = old->next;
old->dispose( old->object );
ch_free( old );
}
/* No references should exist anywhere now */
for ( epoch = 0; epoch < EPOCH_MASK; epoch++ ) {
assert( !references[epoch] );
}
ldap_pvt_thread_rdwr_destroy( &epoch_mutex );
}
epoch_t
epoch_join( void )
{
epoch_t epoch;
struct pending_ref *old, *ref = NULL;
retry:
/* TODO: make this completely lock-free */
ldap_pvt_thread_rdwr_rlock( &epoch_mutex );
epoch = current_epoch;
__atomic_add_fetch( &epoch_threads[epoch], 1, __ATOMIC_ACQ_REL );
ldap_pvt_thread_rdwr_runlock( &epoch_mutex );
if ( __atomic_load_n(
&epoch_threads[EPOCH_PREV(epoch)], __ATOMIC_ACQUIRE ) ) {
return epoch;
}
__atomic_exchange(
&references[EPOCH_PREV(epoch)], &ref, &ref, __ATOMIC_ACQ_REL );
Debug( LDAP_DEBUG_TRACE, "epoch_join: "
"advancing epoch to %zu with %s objects to free\n",
EPOCH_NEXT(epoch), ref ? "some" : "no" );
ldap_pvt_thread_rdwr_wlock( &epoch_mutex );
current_epoch = EPOCH_NEXT(epoch);
ldap_pvt_thread_rdwr_wunlock( &epoch_mutex );
if ( !ref ) {
return epoch;
}
/*
* The below is now safe to free outside epochs and we don't want to make
* the current epoch last any longer than necessary.
*
* Looks like there might be fairness issues in massively parallel
* environments but they haven't been observed on 32-core machines.
*/
epoch_leave( epoch );
for ( old = ref; old; old = ref ) {
ref = old->next;
old->dispose( old->object );
ch_free( old );
}
goto retry;
}
void
epoch_leave( epoch_t epoch )
{
struct pending_ref *p, *next, *old_refs = NULL, *current_refs = NULL;
/* Are there other threads observing our epoch? */
if ( __atomic_sub_fetch( &epoch_threads[epoch], 1, __ATOMIC_ACQ_REL ) ) {
return;
}
/*
* Optimisation for the case when we're mostly idle. Otherwise we won't
* release resources until another thread comes by and joins the epoch
* (twice), and there's no idea how soon (or late) that is going to happen.
*
* NB. There is no limit to the number of threads executing the following
* code in parallel.
*/
ldap_pvt_thread_rdwr_rlock( &epoch_mutex );
/*
* Anything could happen between the subtract and the lock being acquired
* above, so check again. But once we hold this lock (and confirm no more
* threads still observe either prospective epoch), noone will be able to
* finish epoch_join until we've released epoch_mutex since we *first* make
* sure it holds that:
*
* epoch_threads[EPOCH_PREV(current_epoch)] == 0
*
* and that leads epoch_join() to acquire a write lock on &epoch_mutex.
*/
if ( epoch != current_epoch && epoch != EPOCH_PREV(current_epoch) ) {
/* Epoch counter has run away from us, no need to do anything */
ldap_pvt_thread_rdwr_runlock( &epoch_mutex );
return;
}
if ( __atomic_load_n(
&epoch_threads[EPOCH_PREV(current_epoch)],
__ATOMIC_ACQUIRE ) ) {
/* There is another thread still running */
ldap_pvt_thread_rdwr_runlock( &epoch_mutex );
return;
}
if ( __atomic_load_n( &epoch_threads[current_epoch], __ATOMIC_ACQUIRE ) ) {
/* There is another thread still running */
ldap_pvt_thread_rdwr_runlock( &epoch_mutex );
return;
}
/*
* We're all alone (apart from anyone who reached epoch_leave() at the same
* time), it's safe to claim all references and free them.
*/
__atomic_exchange(
&references[EPOCH_PREV(current_epoch)], &old_refs, &old_refs,
__ATOMIC_ACQ_REL );
__atomic_exchange(
&references[current_epoch], ¤t_refs, ¤t_refs,
__ATOMIC_ACQ_REL );
ldap_pvt_thread_rdwr_runlock( &epoch_mutex );
for ( p = old_refs; p; p = next ) {
next = p->next;
p->dispose( p->object );
ch_free( p );
}
for ( p = current_refs; p; p = next ) {
next = p->next;
p->dispose( p->object );
ch_free( p );
}
}
/*
* Add the object to the "current global epoch", not the epoch our thread
* entered.
*/
void
epoch_append( void *ptr, dispose_cb *cb )
{
struct pending_ref *new;
epoch_t epoch = __atomic_load_n( ¤t_epoch, __ATOMIC_ACQUIRE );
/*
* BTW, the following is not appropriate here:
* assert( __atomic_load_n( &epoch_threads[epoch], __ATOMIC_RELAXED ) );
*
* We might be a thread lagging behind in the "previous epoch" with no
* other threads executing at all.
*/
new = ch_malloc( sizeof(struct pending_ref) );
new->object = ptr;
new->dispose = cb;
new->next = __atomic_load_n( &references[epoch], __ATOMIC_ACQUIRE );
while ( !__atomic_compare_exchange( &references[epoch], &new->next, &new, 0,
__ATOMIC_RELEASE, __ATOMIC_RELAXED ) )
/* iterate until we succeed */;
}
int
acquire_ref( uintptr_t *refp )
{
uintptr_t refcnt, new_refcnt;
refcnt = __atomic_load_n( refp, __ATOMIC_ACQUIRE );
/*
* If we just incremented the refcnt and checked for zero after, another
* thread might falsely believe the object was going to stick around.
*
* Checking whether the object is still dead at disposal time might not be
* able to distinguish it from being freed in a later epoch.
*/
do {
if ( !refcnt ) {
return refcnt;
}
new_refcnt = refcnt + 1;
} while ( !__atomic_compare_exchange( refp, &refcnt, &new_refcnt, 0,
__ATOMIC_RELEASE, __ATOMIC_RELAXED ) );
assert( new_refcnt == refcnt + 1 );
return refcnt;
}
int
try_release_ref(
uintptr_t *refp,
void *object,
dispose_cb *unlink_cb,
dispose_cb *destroy_cb )
{
uintptr_t refcnt, new_refcnt;
refcnt = __atomic_load_n( refp, __ATOMIC_ACQUIRE );
/* We promise the caller that we won't decrease refcnt below 0 */
do {
if ( !refcnt ) {
return refcnt;
}
new_refcnt = refcnt - 1;
} while ( !__atomic_compare_exchange( refp, &refcnt, &new_refcnt, 0,
__ATOMIC_RELEASE, __ATOMIC_RELAXED ) );
assert( new_refcnt == refcnt - 1 );
if ( !new_refcnt ) {
if ( unlink_cb ) {
unlink_cb( object );
}
epoch_append( object, destroy_cb );
}
return refcnt;
}