#include <record_manager.h> record_manager<Reclaim, Alloc, Pool, ObjectType1, ObjectType2, ...> myRecManager;
In this declaration, Reclaim is a class type that specifies how objects will be reclaimed (see reclaimer_[...].h), Alloc is a class type that specifies how objects will be allocated (see alloc_[...].h), and Pool is a class type that specifies how (or if) objects will be pooled (see pool_[...].h). ObjectType1, ObjectType2, ... are the object types that the record_manager will operate on.
Suppose our list consists of nodes of type Node which contain int keys. Then reasonable default choices for Reclaim, Alloc and Pool are:
record_manager<reclaimer_debra<int>, alloc_new<int>, pool_none<int>, Node> * myRecManager = new record_manager<reclaimer_debra<int>, alloc_new<int>, pool_none<int>, Node>( maxNumberOfConcurrentThreads);
All record_manager functions take a thread ID (in the range 0 ... maxNumberOfConcurrentThreads - 1) as an argument.
To allocate a Node, invoke allocate:
auto myNode = myRecManager->template allocate<Node>(myThreadID)At the beginning of each data structure operation, e.g., Search, invoke getGuard:
bool search(int key) { auto guard = myRecManager->getGuard(myThreadID); // rest of search ... }It is safe to access nodes only where guard is in scope. (If you don't want to use a guard variable for automatic RAII scoping, because you want more control over the scope where it is safe to access nodes, you can use the record_manager's startOp and endOp functions, instead of getGuard. Note: The getGuard function works by simply calling startOp, then creating a stack variable whose desctructor calls endOp.)
Whenever you unlink a node from the list, invoke retire:
bool remove(int key) { auto guard = myRecManager->getGuard(myThreadID); // your removal code // ... // unlinkedNode is now unlinked from the list myRecManager->retire(myThreadID, unlinkedNode);The retire function will eventually free unlinkedNode when no thread can reach it.
For reclaimer_debra, that's all you have to do. You've allocated and safely reclaimed memory for your list. (This is also enough for reclaimer_debracap, and of course reclaimer_none. However, for reclaimer_hazardptr, you must use the record_manager's protect and unprotect functions, which are described in the paper.)
A microbenchmark that uses this data structures appears under "microbench/." Compile with make -j n, where n is the number of threads on your system.
Run with, e.g., "LD_PRELOAD=../lib/libjemalloc-5.0.1-25.so bin/brown_ext_abtree_lf.ubench_rdebra -i 10 -d 10 -rq 0 -rqsize 100 -k 1000000 -nrq 0 -nwork 36 -t 1000 -nprefill 36". (Parameters: -nwork is the number of threads, -k is the size of the key range, -i and -d are % insertions/deletions, -t is millis to run, -nprefill is the number of threads that will perform prefilling on the data structure. Just ignore -rq, -rqsize and -nrq. You can optionally pin threads to logical processors, in a specific order, by specifying, e.g., "-pin 0-17,72-89,18-35,90-107,36-53,108-125,54-71,126-143".)
Note that we use LD_PRELOAD to replace malloc/free (new/delete) with the scalable jemalloc allocator, since otherwise the default glibc allocator is used, and it is quite slow at high thread counts. I've included the .so file for linux/x64 in the repository (in lib/). (On other systems, you should build jemalloc or use whatever scalable allocator you like.)
In the command above, memory reclamation is done with DEBRA, an efficient EBR algorithm. A few variants of DEBRA, as well as a no-reclamation variant, may also be compiled, depending on the contents of the Makefile (binaries: *.ubench_rdebra,*.ubench_rdebracap, *.ubench_rnone). Several other data structures are likely also compiled.
Harder: if you want to import only the minimal required code into your project, I think you can just copy common/errors.h and common/recordmgr/* (fixing includes / include paths as necessary to allow the headers to find each other).