From 2b584edbddc782b6436a8e28d4f8b85a11f528a6 Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 4 Oct 2018 18:30:23 -0400 Subject: add lock-free queue impl and event-driven queue --- include/salticidae/event.h | 93 ++++++++++++++++++++++++ include/salticidae/queue.h | 176 +++++++++++++++++++++++++++++++++++++++++++++ test/.gitignore | 1 + test/CMakeLists.txt | 3 + test/test_queue.cpp | 92 ++++++++++++++++++++++++ 5 files changed, 365 insertions(+) create mode 100644 include/salticidae/queue.h create mode 100644 test/test_queue.cpp diff --git a/include/salticidae/event.h b/include/salticidae/event.h index 5b41d9e..99fff2f 100644 --- a/include/salticidae/event.h +++ b/include/salticidae/event.h @@ -25,8 +25,14 @@ #ifndef _SALTICIDAE_EVENT_H #define _SALTICIDAE_EVENT_H +#include #include +#include +#ifdef SALTICIDAE_DEBUG_LOG +#include +#endif +#include "salticidae/queue.h" #include "salticidae/util.h" #include "salticidae/ref.h" @@ -50,6 +56,7 @@ class EventContext: public _event_context_ot { EventContext &operator=(const EventContext &) = default; EventContext &operator=(EventContext &&) = default; void dispatch() { event_base_dispatch(get()); } + void stop() { event_base_loopbreak(get()); } }; class Event { @@ -76,6 +83,7 @@ class Event { ev(event_new(eb.get(), fd, events, Event::_then, this)), callback(callback) {} + Event(const Event &) = delete; Event(Event &&other): eb(std::move(other.eb)), fd(other.fd), events(other.events), callback(std::move(other.callback)) { @@ -115,6 +123,91 @@ class Event { operator bool() const { return ev != nullptr; } }; +template +class MPSCQueueEventDriven: public MPMCQueue { + private: + std::atomic wait_sig; + int fd; + Event ev; + + public: + template + MPSCQueueEventDriven(const EventContext &ec, Func &&func, size_t capacity = 65536): + MPMCQueue(capacity), + wait_sig(true), + fd(eventfd(0, EFD_NONBLOCK)), + ev(Event(ec, fd, EV_READ | EV_PERSIST, + [this, func=std::forward(func)](int, short) { + uint64_t t; + read(fd, &t, 8); + SALTICIDAE_LOG_DEBUG("%x\n", std::this_thread::get_id()); + T elem; + while (MPMCQueue::try_dequeue(elem)) + func(std::move(elem)); + wait_sig.store(true, std::memory_order_relaxed); + })) { ev.add(); } + + ~MPSCQueueEventDriven() { close(fd); } + + template + bool enqueue(U &&e) { + static const uint64_t dummy = 1; + bool ret = MPMCQueue::enqueue(std::forward(e)); + if (wait_sig.exchange(false, std::memory_order_relaxed)) + { + SALTICIDAE_LOG_DEBUG("mpsc notify"); + write(fd, &dummy, 8); + } + return ret; + } +}; + +template +class MPMCQueueEventDriven: public MPMCQueue { + private: + std::atomic wait_sig; + std::vector, int>> evs; + + public: + MPMCQueueEventDriven(size_t capacity = 65536): + MPMCQueue(capacity), + wait_sig(true) {} + + template + void listen(const EventContext &ec, Func &&func) { + int fd = eventfd(0, EFD_NONBLOCK); + evs.emplace(evs.end(), std::make_pair(new Event(ec, fd, EV_READ | EV_PERSIST, + [this, func=std::forward(func)](int fd, short) { + uint64_t t; + read(fd, &t, 8); + SALTICIDAE_LOG_DEBUG("%x\n", std::this_thread::get_id()); + T elem; + while (MPMCQueue::try_dequeue(elem)) + func(std::move(elem)); + wait_sig.store(true, std::memory_order_relaxed); + }), fd)); + evs.rbegin()->first->add(); + } + + ~MPMCQueueEventDriven() { + for (const auto &p: evs) + close(p.second); + } + + template + bool enqueue(U &&e) { + static const uint64_t dummy = 1; + bool ret = MPMCQueue::enqueue(std::forward(e)); + if (wait_sig.exchange(false, std::memory_order_relaxed)) + { + SALTICIDAE_LOG_DEBUG("mpmc notify"); + for (const auto &p: evs) + write(p.second, &dummy, 8); + } + return ret; + } +}; + } #endif diff --git a/include/salticidae/queue.h b/include/salticidae/queue.h new file mode 100644 index 0000000..88b3fc3 --- /dev/null +++ b/include/salticidae/queue.h @@ -0,0 +1,176 @@ +#ifndef _SALTICIDAE_QUEUE_H +#define _SALTICIDAE_QUEUE_H + +#include +#include +#include + +namespace salticidae { + +static size_t const cacheline_size = 64; + +class FreeList { + public: + struct Node { + std::atomic next; + std::atomic addback; + std::atomic refcnt; + Node(): next(nullptr), addback(false), refcnt(1) {} + }; + + private: + alignas(cacheline_size) std::atomic top; + + public: + FreeList(): top(nullptr) {} + FreeList(const FreeList &) = delete; + FreeList(FreeList &&) = delete; + + void release_ref(Node *u) { + if (u->refcnt.fetch_sub(1, std::memory_order_relaxed) != 1) return; + u->addback.store(false, std::memory_order_relaxed); + //if (!u->addback.exchange(false, std::memory_order_relaxed)) return; + for (;;) + { + auto t = top.load(std::memory_order_acquire); + // repair the next pointer before CAS, otherwise u->next == nullptr + // could lead to skipping elements + u->next.store(t, std::memory_order_consume); + // the replacement is ok even if ABA happens + if (top.compare_exchange_weak(t, u, std::memory_order_consume)) + { + u->refcnt.store(1, std::memory_order_relaxed); + break; + } + } + } + + bool push(Node *u) { + assert(u->addback.load() == false); + // attempt to push it + u->addback.store(true, std::memory_order_release); + release_ref(u); + return true; + } + + bool pop(Node *&r) { + bool loop = true; + while (loop) + { + auto u = top.load(std::memory_order_acquire); + /* the list is now empty */ + if (u == nullptr) return false; + auto t = u->refcnt.load(std::memory_order_acquire); + /* let's wait for another round if u is a ghost (already popped) */ + if (!t) continue; + /* otherwise t > 0, so with CAS, the invariant that zero refcnt can + * never be increased is guaranteed */ + if (u->refcnt.compare_exchange_weak(t, t + 1, std::memory_order_consume)) + { + /* here, nobody is able to change v->next (because v->next is + * only changed when pushed) even when ABA happens */ + auto v = u; + auto nv = u->next.load(std::memory_order_acquire); + if (top.compare_exchange_weak(v, nv, std::memory_order_consume)) + { + /* manage to pop the head */ + r = u; + loop = false; + /* do not need to try cas_push here because the current + * thread is the only one who can push u back */ + } + /* release the refcnt and execute the delayed push call if + * necessary */ + release_ref(u); + } + } + return true; + } +}; + +template +class MPMCQueue { + struct Block: public FreeList::Node { + T elem; + std::atomic next; + }; + + FreeList blks; + + alignas(cacheline_size) std::atomic head; + alignas(cacheline_size) std::atomic tail; + + template + void _enqueue(Block *nblk, U &&e) { + new (&(nblk->elem)) T(std::forward(e)); + nblk->next.store(nullptr, std::memory_order_release); + auto prev = tail.exchange(nblk, std::memory_order_relaxed); + prev->next.store(nblk, std::memory_order_relaxed); + } + + public: + MPMCQueue(const MPMCQueue &) = delete; + MPMCQueue(MPMCQueue &&) = delete; + + MPMCQueue(size_t capacity = 65536): head(new Block()), tail(head.load()) { + head.load()->next = nullptr; + while (capacity--) + blks.push(new Block()); + } + + ~MPMCQueue() { + for (FreeList::Node *ptr; blks.pop(ptr); ) delete ptr; + for (Block *ptr = head.load(), *nptr; ptr; ptr = nptr) + { + nptr = ptr->next; + delete ptr; + } + } + + template + bool enqueue(U &&e) { + FreeList::Node * _nblk; + if (!blks.pop(_nblk)) _nblk = new Block(); + _enqueue(static_cast(_nblk), std::forward(e)); + return true; + } + + template + bool try_enqueue(U &&e) { + FreeList::Node * _nblk; + if (!blks.pop(_nblk)) return false; + _enqueue(static_cast(_nblk), std::forward(e)); + return true; + } + + bool try_dequeue(T &e) { + for (;;) + { + auto h = head.load(std::memory_order_acquire); + auto t = h->refcnt.load(std::memory_order_relaxed); + if (!t) continue; + if (h->refcnt.compare_exchange_weak(t, t + 1, std::memory_order_consume)) + { + auto nh = h->next.load(std::memory_order_relaxed); + if (nh == nullptr) + { + blks.release_ref(h); + return false; + } + e = std::move(nh->elem); + auto hh = h; + if (head.compare_exchange_weak(hh, nh, std::memory_order_consume)) + { + blks.release_ref(h); + blks.push(h); + return true; + } + blks.release_ref(h); + } + } + } +}; + +} + +#endif diff --git a/test/.gitignore b/test/.gitignore index 2cd44f2..592f084 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,5 +1,6 @@ test_msg test_bits test_network +test_queue bench_network Makefile diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9689df3..44c984d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,5 +29,8 @@ target_link_libraries(test_bits salticidae_static) add_executable(test_network test_network.cpp) target_link_libraries(test_network salticidae_static) +add_executable(test_queue test_queue.cpp) +target_link_libraries(test_queue salticidae_static pthread) + add_executable(bench_network bench_network.cpp) target_link_libraries(bench_network salticidae_static pthread) diff --git a/test/test_queue.cpp b/test/test_queue.cpp new file mode 100644 index 0000000..bed88c4 --- /dev/null +++ b/test/test_queue.cpp @@ -0,0 +1,92 @@ +#include +#include +#include + +#include "salticidae/event.h" + +void test_mpsc(int nproducers = 16, int nops = 100000) { + size_t total = nproducers * nops; + salticidae::EventContext ec; + std::atomic collected(0); + salticidae::MPSCQueueEventDriven q(ec, [&collected](int x) { + printf("%d\n", x); + collected.fetch_add(1); + }); + std::vector producers; + std::thread consumer([&collected, total, &ec]() { + salticidae::Event timer(ec, -1, EV_TIMEOUT | EV_PERSIST, + [&ec, &collected, total](int, short) { + if (collected.load() == total) ec.stop(); + }); + timer.add_with_timeout(1); + ec.dispatch(); + }); + for (int i = 0; i < nproducers; i++) + { + producers.emplace(producers.end(), std::thread([&q, nops, i, nproducers]() { + int x = i; + for (int j = 0; j < nops; j++) + { + //usleep(rand() / double(RAND_MAX) * 100); + q.enqueue(x); + x += nproducers; + } + })); + } + for (auto &t: producers) t.join(); + fprintf(stderr, "producers terminate\n"); + consumer.join(); + fprintf(stderr, "consumers terminate\n"); +} + +void test_mpmc(int nproducers = 16, int nconsumers = 4, int nops = 100000) { + size_t total = nproducers * nops; + salticidae::MPMCQueueEventDriven q; + std::vector producers; + std::vector consumers; + std::vector ecs; + std::atomic collected(0); + ecs.resize(nconsumers); + for (int i = 0; i < nconsumers; i++) + { + q.listen(ecs[i], [&collected](int x) { + //usleep(10); + printf("%d\n", x); + collected.fetch_add(1); + }); + } + for (int i = 0; i < nconsumers; i++) + { + consumers.emplace(consumers.end(), std::thread( + [&collected, total, &ec = ecs[i]]() { + salticidae::Event timer(ec, -1, EV_TIMEOUT | EV_PERSIST, + [&ec, &collected, total](int, short) { + if (collected.load() == total) ec.stop(); + }); + timer.add_with_timeout(1); + ec.dispatch(); + })); + } + for (int i = 0; i < nproducers; i++) + { + producers.emplace(producers.end(), std::thread([&q, nops, i, nproducers]() { + int x = i; + for (int j = 0; j < nops; j++) + { + //usleep(rand() / double(RAND_MAX) * 100); + q.enqueue(x); + x += nproducers; + } + })); + } + for (auto &t: producers) t.join(); + fprintf(stderr, "producers terminate\n"); + for (auto &t: consumers) t.join(); + fprintf(stderr, "consumers terminate\n"); +} + +int main() { + //test_mpsc(); + test_mpmc(); + return 0; +} -- cgit v1.2.3