generated from TheLartians/ModernCppStarter
-
-
Notifications
You must be signed in to change notification settings - Fork 42
Lock free, work stealing queue #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
DeveloperPaul123
wants to merge
13
commits into
main
Choose a base branch
from
feature/lock-free-deque
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 6 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
3e22869
feature: wip implementation of chase-lev deque
DeveloperPaul123 9e3d43b
chore: add brace init in example
DeveloperPaul123 12f7091
fix: memory ordering
DeveloperPaul123 103d208
Merge remote-tracking branch 'origin/master' into feature/lock-free-d…
DeveloperPaul123 07bcab8
fix: minor issues with work stealing deque
DeveloperPaul123 c161115
fix: usage of new work-stealing deque
DeveloperPaul123 7c5c8fd
fix: properly check that circular buffer size is a power of 2
DeveloperPaul123 b443661
fix: deprecation warnings when building unit tests
DeveloperPaul123 fd12d9c
wip: trying to fix issues with work stealing deque
DeveloperPaul123 1ac31fc
chore: don't warn about hardware interference size
DeveloperPaul123 1bedebb
chore: change strategy when using work stealing deque
DeveloperPaul123 3fdee7e
wip: trying to address a build issue
DeveloperPaul123 3b0b88f
chore: update CPM.cmake version
DeveloperPaul123 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| #pragma once | ||
|
|
||
| #include <atomic> | ||
| #include <cassert> | ||
| #include <cstddef> | ||
| #include <cstdint> | ||
| #include <memory> | ||
| #include <optional> | ||
| #include <type_traits> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| namespace dp { | ||
|
|
||
| #ifdef __cpp_lib_hardware_interference_size | ||
| using std::hardware_destructive_interference_size; | ||
| #else | ||
| // 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ... | ||
| inline constexpr std::size_t hardware_destructive_interference_size = | ||
| 2 * sizeof(std::max_align_t); | ||
| #endif | ||
|
|
||
| /** | ||
| * @brief Chase-Lev work stealing queue | ||
| * @details Support single producer, multiple consumer. The producer owns the back, consumers | ||
| * own the top. Consumers can also take from the top of the queue. The queue is "lock-free" in | ||
| * that it does not directly use mutexes or locks. | ||
| * | ||
| * This is an implementation of the deque described in "Correct and Efficient Work-Stealing for | ||
| * Weak Memory Models" and "Dynamic Circular Work-Stealing Deque" by Chase,Lev. | ||
| * | ||
| * This implementation is taken from the following implementations | ||
| * - https://github.com/ConorWilliams/ConcurrentDeque/blob/main/include/riften/deque.hpp | ||
| * - https://github.com/taskflow/work-stealing-queue/blob/master/wsq.hpp | ||
| * | ||
| * I've made some minor edits and changes based on new C++ 23 features and other personal coding | ||
| * style choices/preferences. | ||
| */ | ||
| template <typename T> | ||
| requires std::is_destructible_v<T> | ||
| class work_stealing_deque final { | ||
| /** | ||
| * @brief Simple circular array buffer that can regrow | ||
| */ | ||
| class circular_buffer final { | ||
| std::int64_t size_; | ||
| std::int64_t mask_; | ||
| std::unique_ptr<T[]> buffer_ = std::make_unique_for_overwrite<T[]>(size_); | ||
|
|
||
| public: | ||
| explicit circular_buffer(const std::int64_t size) : size_(size), mask_(size - 1) { | ||
| // size must be a power of 2 | ||
| assert((size % 2) == 0); | ||
| } | ||
|
|
||
| [[nodiscard]] std::int64_t capacity() const noexcept { return size_; } | ||
|
|
||
| void store(const std::size_t index, T&& value) noexcept | ||
| requires std::is_move_assignable_v<T> | ||
| { | ||
| buffer_[index & mask_] = std::move(value); | ||
| } | ||
|
|
||
| T&& load(const std::size_t index) noexcept { | ||
| if constexpr (std::is_move_constructible_v<T>) { | ||
| return std::move(buffer_[index & mask_]); | ||
| } else { | ||
| return buffer_[index & mask_]; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @brief Resize the internal buffer. Copies [start, end) to the new buffer. | ||
| * @param start The start index | ||
| * @param end The end index | ||
| */ | ||
| circular_buffer* resize(const std::size_t start, const std::size_t end) { | ||
| auto temp = new circular_buffer(size_ * 2); | ||
| for (std::size_t i = start; i != end; ++i) { | ||
| temp->store(i, load(i)); | ||
| } | ||
| return temp; | ||
| } | ||
| }; | ||
|
|
||
| constexpr static std::size_t default_count = 1024; | ||
| alignas(hardware_destructive_interference_size) std::atomic_int64_t top_; | ||
| alignas(hardware_destructive_interference_size) std::atomic_int64_t bottom_; | ||
| alignas(hardware_destructive_interference_size) std::atomic<circular_buffer*> buffer_; | ||
|
|
||
| std::vector<std::unique_ptr<circular_buffer>> garbage_{32}; | ||
|
|
||
| static constexpr std::memory_order relaxed = std::memory_order_relaxed; | ||
| static constexpr std::memory_order acquire = std::memory_order_acquire; | ||
| static constexpr std::memory_order consume = std::memory_order_consume; | ||
| static constexpr std::memory_order release = std::memory_order_release; | ||
| static constexpr std::memory_order seq_cst = std::memory_order_seq_cst; | ||
|
|
||
| public: | ||
| explicit work_stealing_deque(const std::size_t& capacity = default_count) | ||
| : top_(0), bottom_(0), buffer_(new circular_buffer(capacity)) {} | ||
|
|
||
| // queue is non-copyable | ||
| work_stealing_deque(work_stealing_deque&) = delete; | ||
| work_stealing_deque& operator=(work_stealing_deque&) = delete; | ||
|
|
||
| [[nodiscard]] std::size_t capacity() const { return buffer_.load(relaxed)->capacity(); } | ||
| [[nodiscard]] std::size_t size() const { | ||
| const auto bottom = bottom_.load(relaxed); | ||
| const auto top = top_.load(relaxed); | ||
| return static_cast<std::size_t>(bottom >= top ? bottom - top : 0); | ||
| } | ||
|
|
||
| [[nodiscard]] bool empty() const { return !size(); } | ||
|
|
||
| template <typename... Args> | ||
| void emplace(Args&&... args) { | ||
| // construct first in case it throws | ||
| T value(std::forward<Args>(args)...); | ||
| push_bottom(std::move(value)); | ||
| } | ||
|
|
||
| void push_bottom(T&& value) { | ||
| auto bottom = bottom_.load(relaxed); | ||
| auto top = top_.load(acquire); | ||
| auto* buffer = buffer_.load(relaxed); | ||
|
|
||
| // check if the buffer is full | ||
| if (buffer->capacity() - 1 < (bottom - top)) { | ||
| garbage_.emplace_back(std::exchange(buffer, buffer->resize(top, bottom))); | ||
| buffer_.store(buffer, relaxed); | ||
| } | ||
|
|
||
| buffer->store(bottom, std::forward<T>(value)); | ||
|
|
||
| // this synchronizes with other acquire fences | ||
| // memory operations about this line cannot be reordered | ||
| std::atomic_thread_fence(release); | ||
|
|
||
| bottom_.store(bottom + 1, relaxed); | ||
| } | ||
|
|
||
| std::optional<T> take_bottom() { | ||
| auto bottom = bottom_.load(relaxed) - 1; | ||
| auto* buffer = buffer_.load(relaxed); | ||
|
|
||
| // prevent stealing | ||
| bottom_.store(bottom, relaxed); | ||
|
|
||
| // this synchronizes with other release fences | ||
| // memory ops below this line cannot be reordered | ||
| std::atomic_thread_fence(seq_cst); | ||
|
|
||
| std::optional<T> item = std::nullopt; | ||
|
|
||
| auto top = top_.load(relaxed); | ||
| if (top <= bottom) { | ||
| // queue isn't empty | ||
| item = buffer->load(bottom); | ||
| if (top == bottom) { | ||
| // there is only 1 item left in the queue, we need the CAS to succeed | ||
| // since another thread may be trying to steal and could steal before we're able | ||
| // to take the bottom | ||
| if (!top_.compare_exchange_strong(top, top + 1, seq_cst, relaxed)) { | ||
| // failed race | ||
| bottom_.store(bottom + 1, relaxed); | ||
| item = std::nullopt; | ||
| } | ||
| bottom_.store(bottom + 1, relaxed); | ||
| } | ||
| } else { | ||
| bottom_.store(bottom + 1, relaxed); | ||
| } | ||
|
|
||
| return item; | ||
| } | ||
|
|
||
| /** | ||
| * @brief Steal from the top of the queue | ||
| * | ||
| * @return std::optional<T> | ||
| */ | ||
| std::optional<T> pop_top() { | ||
| auto top = top_.load(acquire); | ||
| // this synchronizes with other release fences | ||
| // memory ops below this line cannot be reordered with ops above this line | ||
| std::atomic_thread_fence(seq_cst); | ||
| auto bottom = bottom_.load(acquire); | ||
| std::optional<T> item; | ||
|
|
||
| if (top < bottom) { | ||
| // non-empty queue | ||
| auto* buffer = buffer_.load(consume); | ||
| item = buffer->load(top); | ||
|
|
||
| if (!top_.compare_exchange_strong(top, top + 1, seq_cst, relaxed)) { | ||
| // failed the race | ||
| item = std::nullopt; | ||
| } | ||
| } | ||
| // empty queue | ||
| return item; | ||
| } | ||
| }; | ||
| } // namespace dp | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.