├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── test.cc └── lockfree_linkedlist.h /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | test 3 | info.plist -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "HazardPointer"] 2 | path = HazardPointer 3 | url = https://github.com/bhhbazinga/HazardPointer 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXX = g++ 2 | CXXFLAGS = -Wall -Wextra -pedantic -std=c++2a -g -o3 3 | #-fsanitize=address -fsanitize=leak 4 | #-fsanitize=thread 5 | SRC = test.cc 6 | EXEC = test 7 | 8 | all: $(EXEC) 9 | 10 | $(EXEC): test.cc lockfree_linkedlist.h HazardPointer/reclaimer.h 11 | $(CXX) $(CXXFLAGS) -o $(EXEC) test.cc 12 | 13 | HazardPointer/reclaimer.h: 14 | git submodule update --remote --recursive --init 15 | 16 | .Phony : clean 17 | 18 | clean: 19 | rm -rf $(EXEC) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LockFreeLinkedList 2 | Lock Free Linked List Based On Harris'OrderedListBasedSet And Michael's Hazard Pointer. 3 | ## Feature 4 | * Thread-safe and Lock-free. 5 | * ABA safe. 6 | * Set implemented through singly ordered linked list[1]. 7 | * Use Hazard pointer to manage memory[2]. 8 | * Support Multi-producer & Multi-consumer. 9 | ## Benchmark 10 | 11 | Magnitude | Insert | Delete | Insert & Delete| 12 | :----------- | :-----------| :-----------| :----------------- 13 | 1K | 1.2ms | 0ms | 3.6ms 14 | 10K | 147.1ms | 18.9ms | 293.5ms 15 | 100K | 15064.4ms | 1647ms | 27176ms 16 | 17 | The above data was tested on my 2013 macbook-pro with Intel Core i7 4 cores 2.3 GHz. 18 | 19 | The data of first and second column was obtained by starting 8 threads to insert concurrently and delete concurrently, the data of third column was obtained by starting 4 threads to insert and 4 threads to delete concurrently, each looped 10 times to calculate the average time consumption. 20 | See also [test](test.cc). 21 | ## Build 22 | ``` 23 | make && ./test 24 | ``` 25 | ## API 26 | ```C++ 27 | bool Insert(const T& data); 28 | bool Insert(T&& data); 29 | bool Emplace(Args&&... args); 30 | bool Delete(const T& data); 31 | bool Find(const T& data); 32 | size_t size() const; 33 | ``` 34 | ## Reference 35 | [1]A Pragmatic Implementation of Non-BlockingLinked-Lists. Timothy L.Harris\ 36 | [2]Hazard Pointers: Safe Memory Reclamation for Lock-Free Objects. Maged M. Michael 37 | -------------------------------------------------------------------------------- /test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "lockfree_linkedlist.h" 10 | 11 | const int kMaxThreads = std::thread::hardware_concurrency(); 12 | 13 | int maxElements; 14 | LockFreeLinkedList list; 15 | 16 | // Insert sucessfully then ++cnt, delete succesfully then --cnt. 17 | std::atomic cnt = 0; 18 | std::atomic start = false; 19 | std::unordered_map elements2timespan; 20 | 21 | void onInsert(int divide) { 22 | while (!start) { 23 | std::this_thread::yield(); 24 | } 25 | 26 | int n = maxElements / divide; 27 | for (int i = 0; i < n; ++i) { 28 | if (list.Insert(rand() % n)) { 29 | ++cnt; 30 | } 31 | } 32 | } 33 | 34 | void onDelete(int divide) { 35 | while (!start) { 36 | std::this_thread::yield(); 37 | } 38 | 39 | int n = maxElements / divide; 40 | for (int i = 0; i < n; ++i) { 41 | if (list.Delete(rand() % n)) { 42 | --cnt; 43 | } 44 | } 45 | } 46 | 47 | void TestConcurrentInsert() { 48 | int old_size = list.size(); 49 | std::vector threads; 50 | for (int i = 0; i < kMaxThreads; ++i) { 51 | threads.push_back(std::thread(onInsert, kMaxThreads)); 52 | } 53 | 54 | start = true; 55 | auto t1_ = std::chrono::steady_clock::now(); 56 | for (int i = 0; i < kMaxThreads; ++i) { 57 | threads[i].join(); 58 | } 59 | auto t2_ = std::chrono::steady_clock::now(); 60 | 61 | assert(cnt + old_size == static_cast(list.size())); 62 | int ms = 63 | std::chrono::duration_cast(t2_ - t1_).count(); 64 | elements2timespan[maxElements][0] += ms; 65 | std::cout << maxElements << " elements insert concurrently, timespan=" << ms 66 | << "ms" 67 | << "\n"; 68 | start = false; 69 | } 70 | 71 | void TestConcurrentDelete() { 72 | int old_size = list.size(); 73 | std::vector threads; 74 | for (int i = 0; i < kMaxThreads; ++i) { 75 | threads.push_back(std::thread(onDelete, kMaxThreads)); 76 | } 77 | 78 | cnt = 0; 79 | start = true; 80 | auto t1_ = std::chrono::steady_clock::now(); 81 | for (int i = 0; i < kMaxThreads; ++i) { 82 | threads[i].join(); 83 | } 84 | auto t2_ = std::chrono::steady_clock::now(); 85 | 86 | assert(cnt + old_size == static_cast(list.size())); 87 | int ms = 88 | std::chrono::duration_cast(t2_ - t1_).count(); 89 | elements2timespan[maxElements][1] += ms; 90 | std::cout << maxElements << " elements delete concurrently, timespan=" << ms 91 | << "ms" 92 | << "\n"; 93 | 94 | cnt = 0; 95 | start = false; 96 | } 97 | 98 | void TestConcurrentInsertAndDequeue() { 99 | int old_size = list.size(); 100 | 101 | int divide = kMaxThreads / 2; 102 | std::vector insert_threads; 103 | for (int i = 0; i < divide; ++i) { 104 | insert_threads.push_back(std::thread(onInsert, divide)); 105 | } 106 | 107 | std::vector delete_threads; 108 | for (int i = 0; i < divide; ++i) { 109 | delete_threads.push_back(std::thread(onDelete, divide)); 110 | } 111 | 112 | cnt = 0; 113 | start = true; 114 | auto t1_ = std::chrono::steady_clock::now(); 115 | for (int i = 0; i < divide; ++i) { 116 | insert_threads[i].join(); 117 | } 118 | 119 | for (int i = 0; i < divide; ++i) { 120 | delete_threads[i].join(); 121 | } 122 | auto t2_ = std::chrono::steady_clock::now(); 123 | 124 | assert(cnt + old_size == static_cast(list.size())); 125 | int ms = 126 | std::chrono::duration_cast(t2_ - t1_).count(); 127 | elements2timespan[maxElements][2] += ms; 128 | std::cout << maxElements 129 | << " elements insert and delete concurrently, timespan=" << ms 130 | << "ms" 131 | << "\n"; 132 | 133 | cnt = 0; 134 | start = false; 135 | } 136 | 137 | const int kElements1 = 1000; 138 | const int kElements2 = 10000; 139 | const int kElements3 = 100000; 140 | 141 | int main(int argc, char const* argv[]) { 142 | (void)argc; 143 | (void)argv; 144 | 145 | srand(std::time(0)); 146 | 147 | std::cout << "Benchmark with " << kMaxThreads << " threads:" 148 | << "\n"; 149 | 150 | int elements[] = {kElements1, kElements2, kElements3}; 151 | int timespan1[] = {0, 0, 0}; 152 | int timespan2[] = {0, 0, 0}; 153 | int timespan3[] = {0, 0, 0}; 154 | 155 | elements2timespan[kElements1] = timespan1; 156 | elements2timespan[kElements2] = timespan2; 157 | elements2timespan[kElements3] = timespan3; 158 | 159 | for (int i = 0; i < 10; ++i) { 160 | for (int j = 0; j < 3; ++j) { 161 | maxElements = elements[j]; 162 | TestConcurrentInsert(); 163 | TestConcurrentDelete(); 164 | TestConcurrentInsertAndDequeue(); 165 | std::cout << "\n"; 166 | } 167 | } 168 | 169 | for (int i = 0; i < 3; ++i) { 170 | maxElements = elements[i]; 171 | float avg = static_cast(elements2timespan[maxElements][0]) 172 | / 10.0f; std::cout << maxElements 173 | << " elements insert concurrently, average timespan=" << avg 174 | << "ms" 175 | << "\n"; 176 | avg = static_cast(elements2timespan[maxElements][1]) / 10.0f; 177 | std::cout << maxElements 178 | << " elements delete concurrently, average timespan=" << avg 179 | << "ms" 180 | << "\n"; 181 | avg = static_cast(elements2timespan[maxElements][2]) / 10.0f; 182 | std::cout << maxElements 183 | << " elements insert and delete concurrently, average timespan=" 184 | << avg << "ms" 185 | << "\n"; 186 | std::cout << "\n"; 187 | } 188 | 189 | return 0; 190 | } -------------------------------------------------------------------------------- /lockfree_linkedlist.h: -------------------------------------------------------------------------------- 1 | #ifndef LOCKFREE_LINKEDLIST_H 2 | #define LOCKFREE_LINKEDLIST_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "HazardPointer/reclaimer.h" 10 | 11 | template 12 | class ListReclaimer; 13 | 14 | template 15 | class LockFreeLinkedList { 16 | static_assert(std::is_copy_constructible_v, "T requires copy constructor"); 17 | friend ListReclaimer; 18 | 19 | struct Node; 20 | 21 | public: 22 | LockFreeLinkedList() : head_(new Node()), size_(0) {} 23 | 24 | LockFreeLinkedList(const LockFreeLinkedList& other) = delete; 25 | LockFreeLinkedList(LockFreeLinkedList&& other) = delete; 26 | 27 | LockFreeLinkedList& operator=(const LockFreeLinkedList& other) = delete; 28 | LockFreeLinkedList& operator=(LockFreeLinkedList&& other) = delete; 29 | 30 | ~LockFreeLinkedList() { 31 | Node* p = head_; 32 | while (p != nullptr) { 33 | Node* tmp = p; 34 | p = p->next.load(std::memory_order_acquire); 35 | // We can safely delete node, because each thread exits before list 36 | // destruct, while thead exiting, it wait all hazard pointers hand over. 37 | delete tmp; 38 | } 39 | } 40 | 41 | // Find the first node which data is greater than the given data, 42 | // then insert the new node before it then return true, else if 43 | // data is already exist in list then return false. 44 | template 45 | bool Emplace(Args&&... args); 46 | 47 | bool Insert(const T& data) { 48 | static_assert(std::is_copy_constructible::value, 49 | "T must be copy constructible"); 50 | return Emplace(data); 51 | } 52 | 53 | bool Insert(T&& data) { 54 | static_assert(std::is_constructible_v, 55 | "T must be constructible with T&&"); 56 | return Emplace(std::forward(data)); 57 | } 58 | 59 | // Find the first node which data is equals to the given data, 60 | // then delete it and return true, if not found the given data then 61 | // return false. 62 | bool Delete(const T& data); 63 | 64 | // Find the first node which data is equals to the given data, if not found 65 | // the given data then return false. 66 | bool Find(const T& data) { 67 | Node* prev; 68 | Node* cur; 69 | HazardPointer prev_hp, cur_hp; 70 | bool found = Search(data, &prev, &cur, prev_hp, cur_hp); 71 | return found; 72 | } 73 | 74 | // Get size of the list. 75 | size_t size() const { return size_.load(std::memory_order_relaxed); } 76 | 77 | private: 78 | bool InsertNode(Node* new_node); 79 | 80 | bool Search(const T& data, Node** prev_ptr, Node** cur_ptr, 81 | HazardPointer& prev_hp, HazardPointer& cur_hp); 82 | 83 | bool Less(const T& data1, const T& data2) const { return data1 < data2; } 84 | 85 | bool GreaterOrEquals(const T& data1, const T& data2) const { 86 | return !(Less(data1, data2)); 87 | } 88 | 89 | bool Equals(const T& data1, const T& data2) const { 90 | return !Less(data1, data2) && !Less(data2, data1); 91 | } 92 | 93 | bool is_marked_reference(Node* next) const { 94 | return (reinterpret_cast(next) & 0x1) == 0x1; 95 | } 96 | 97 | Node* get_marked_reference(Node* next) const { 98 | return reinterpret_cast(reinterpret_cast(next) | 0x1); 99 | } 100 | 101 | Node* get_unmarked_reference(Node* next) const { 102 | return reinterpret_cast(reinterpret_cast(next) & 103 | ~0x1); 104 | } 105 | 106 | static void OnDeleteNode(void* ptr) { delete static_cast(ptr); } 107 | 108 | struct Node { 109 | Node() : data(nullptr), next(nullptr){}; 110 | 111 | template 112 | Node(Args&&... args) 113 | : data(new T(std::forward(args)...)), next(nullptr) {} 114 | 115 | ~Node() { 116 | if (data != nullptr) delete data; 117 | }; 118 | 119 | T* data; 120 | std::atomic next; 121 | }; 122 | 123 | Node* head_; 124 | std::atomic size_; 125 | static Reclaimer::HazardPointerList global_hp_list_; 126 | }; 127 | 128 | template 129 | Reclaimer::HazardPointerList LockFreeLinkedList::global_hp_list_; 130 | 131 | template 132 | class ListReclaimer : public Reclaimer { 133 | friend LockFreeLinkedList; 134 | 135 | private: 136 | ListReclaimer(HazardPointerList& hp_list) : Reclaimer(hp_list) {} 137 | ~ListReclaimer() override = default; 138 | 139 | static ListReclaimer& GetInstance() { 140 | thread_local static ListReclaimer reclaimer( 141 | LockFreeLinkedList::global_hp_list_); 142 | return reclaimer; 143 | } 144 | }; 145 | 146 | template 147 | template 148 | bool LockFreeLinkedList::Emplace(Args&&... args) { 149 | Node* new_node = new Node(std::forward(args)...); 150 | Node* prev; 151 | Node* cur; 152 | HazardPointer prev_hp, cur_hp; 153 | do { 154 | if (Search(*new_node->data, &prev, &cur, prev_hp, cur_hp)) { 155 | // List already contains *new_node->data. 156 | delete new_node; 157 | return false; 158 | } 159 | 160 | new_node->next.store(cur, std::memory_order_release); 161 | } while (!prev->next.compare_exchange_weak( 162 | cur, new_node, std::memory_order_release, std::memory_order_relaxed)); 163 | 164 | size_.fetch_add(1, std::memory_order_relaxed); 165 | return true; 166 | } 167 | 168 | template 169 | bool LockFreeLinkedList::Delete(const T& data) { 170 | Node* prev; 171 | Node* cur; 172 | Node* next; 173 | HazardPointer prev_hp, cur_hp; 174 | do { 175 | do { 176 | if (!Search(data, &prev, &cur, prev_hp, cur_hp)) { 177 | return false; 178 | } 179 | next = cur->next.load(std::memory_order_acquire); 180 | } while (is_marked_reference(next)); 181 | // Logically delete cur by marking cur->next. 182 | } while (!cur->next.compare_exchange_weak(next, get_marked_reference(next), 183 | std::memory_order_release, 184 | std::memory_order_relaxed)); 185 | 186 | if (prev->next.compare_exchange_strong(cur, next, std::memory_order_release, 187 | std::memory_order_relaxed)) { 188 | size_.fetch_sub(1, std::memory_order_relaxed); 189 | auto& reclaimer = ListReclaimer::GetInstance(); 190 | reclaimer.ReclaimLater(cur, LockFreeLinkedList::OnDeleteNode); 191 | reclaimer.ReclaimNoHazardPointer(); 192 | } else { 193 | prev_hp.UnMark(); 194 | cur_hp.UnMark(); 195 | Search(data, &prev, &cur, prev_hp, cur_hp); 196 | } 197 | 198 | return true; 199 | } 200 | 201 | // Find the first node which data is equals to the given data, if not found 202 | // the given data then return false. *cur_ptr point to that node, *prev_ptr is 203 | // the predecessor of that node. 204 | template 205 | bool LockFreeLinkedList::Search(const T& data, Node** prev_ptr, 206 | Node** cur_ptr, HazardPointer& prev_hp, 207 | HazardPointer& cur_hp) { 208 | auto& reclaimer = ListReclaimer::GetInstance(); 209 | try_again: 210 | Node* prev = head_; 211 | Node* cur = prev->next.load(std::memory_order_acquire); 212 | Node* next; 213 | while (true) { 214 | cur_hp.UnMark(); 215 | cur_hp = HazardPointer(&reclaimer, cur); 216 | // Make sure prev is the predecessor of cur, 217 | // so that cur is properly marked as hazard. 218 | if (prev->next.load(std::memory_order_acquire) != cur) goto try_again; 219 | 220 | if (nullptr == cur) { 221 | *prev_ptr = prev; 222 | *cur_ptr = cur; 223 | return false; 224 | }; 225 | 226 | next = cur->next.load(std::memory_order_acquire); 227 | if (is_marked_reference(next)) { 228 | if (!prev->next.compare_exchange_strong(cur, 229 | get_unmarked_reference(next))) 230 | goto try_again; 231 | 232 | reclaimer.ReclaimLater(cur, LockFreeLinkedList::OnDeleteNode); 233 | reclaimer.ReclaimNoHazardPointer(); 234 | size_.fetch_sub(1, std::memory_order_relaxed); 235 | cur = get_unmarked_reference(next); 236 | } else { 237 | const T& cur_data = *cur->data; 238 | // Make sure prev is the predecessor of cur, 239 | // so that cur_data is correct. 240 | if (prev->next.load(std::memory_order_acquire) != cur) goto try_again; 241 | 242 | // Can not get cur_data after above invocation, 243 | // because prev may not be the predecessor of cur at this point. 244 | if (GreaterOrEquals(cur_data, data)) { 245 | *prev_ptr = prev; 246 | *cur_ptr = cur; 247 | return Equals(cur_data, data); 248 | } 249 | 250 | // Swap cur_hp and prev_hp. 251 | HazardPointer tmp = std::move(cur_hp); 252 | cur_hp = std::move(prev_hp); 253 | prev_hp = std::move(tmp); 254 | 255 | prev = cur; 256 | cur = next; 257 | } 258 | }; 259 | 260 | assert(false); 261 | return false; 262 | } 263 | 264 | #endif // LOCKFREE_LINKEDLIST_H --------------------------------------------------------------------------------