diff --git a/.github/workflows/cmake.yaml b/.github/workflows/cmake.yaml index a6a9ee3..e9c0d41 100644 --- a/.github/workflows/cmake.yaml +++ b/.github/workflows/cmake.yaml @@ -11,4 +11,6 @@ jobs: spinlock: uses: ./.github/workflows/spinlock.yaml mutex: - uses: ./.github/workflows/mutex.yaml \ No newline at end of file + uses: ./.github/workflows/mutex.yaml + queue_spsc: + uses: ./.github/workflows/queue_spsc.yaml \ No newline at end of file diff --git a/.github/workflows/mutex.yaml b/.github/workflows/mutex.yaml index 526b549..51822e9 100644 --- a/.github/workflows/mutex.yaml +++ b/.github/workflows/mutex.yaml @@ -1,7 +1,9 @@ name: Cmake on: - workflow_call: + push: + branches: + - master env: BUILD_TYPE_DEBUG: Debug diff --git a/.github/workflows/queue_spsc.yaml b/.github/workflows/queue_spsc.yaml new file mode 100644 index 0000000..3cb07d9 --- /dev/null +++ b/.github/workflows/queue_spsc.yaml @@ -0,0 +1,111 @@ +name: Cmake + +on: [push] + +env: + BUILD_TYPE_DEBUG: Debug + BUILD_TYPE_RELEASE: Release + + CLANG_15: clang++-15 + +jobs: + ubuntu-clang-15-debug-address-leak-undefined-sanitize: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Install Dependencies + uses: actions/cache@v2 + with: + path: ~/.conan/data + key: ${{ runner.os }}-conan-${{ hashFiles('conanfile.txt') }}-queue-spsc-address + - run: | + export BUILD_TYPE=${{env.BUILD_TYPE_DEBUG}} + ./tools/install_deps.sh + - run: | + mkdir -p ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -DCMAKE_CXX_COMPILER=/bin/${{env.CLANG_15}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE_DEBUG}} \ + -DCHECK_SANITIZE=ON -DENABLE_SANITIZER_ADDRESS=True -DENABLE_SANITIZER_LEAK=True -DENABLE_SANITIZER_UNDEFINED_BEHAVIOR=True + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_DEBUG}} --target queue_spsc -- -j 2 + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_DEBUG}} --target unit_test_queue_spsc -- -j 2 + - run: ${{github.workspace}}/build/bin/unit_test_queue_spsc + --gtest_shuffle + --gtest_color=yes + + ubuntu-clang-15-debug-memorysanitize: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Install Dependencies + uses: actions/cache@v2 + with: + path: ~/.conan/data + key: ${{ runner.os }}-conan-${{ hashFiles('conanfile.txt') }}-queue-spsc-address + - run: | + export BUILD_TYPE=${{env.BUILD_TYPE_DEBUG}} + ./tools/install_deps.sh + - run: | + mkdir -p ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -DCMAKE_CXX_COMPILER=/bin/${{env.CLANG_15}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE_DEBUG}} \ + -DCHECK_SANITIZE=ON -DENABLE_SANITIZER_MEMORY=True + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_DEBUG}} --target queue_spsc -- -j 2 + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_DEBUG}} --target unit_test_queue_spsc -- -j 2 + - run: ${{github.workspace}}/build/bin/unit_test_queue_spsc + --gtest_shuffle + --gtest_color=yes + + ubuntu-clang-15-debug-thread-sanitize: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Install Dependencies + uses: actions/cache@v2 + with: + path: ~/.conan/data + key: ${{ runner.os }}-conan-${{ hashFiles('conanfile.txt') }}-queue-spsc-thread + - run: | + export BUILD_TYPE=${{env.BUILD_TYPE_DEBUG}} + ./tools/install_deps.sh + - run: | + mkdir -p ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -DCMAKE_CXX_COMPILER=/bin/${{env.CLANG_15}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE_DEBUG}} \ + -DCHECK_SANITIZE=ON -DENABLE_SANITIZER_THREAD=True + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_DEBUG}} --target queue_spsc -- -j 2 + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_DEBUG}} --target unit_test_queue_spsc -- -j 2 + - run: ${{github.workspace}}/build/bin/unit_test_queue_spsc + --gtest_shuffle + --gtest_color=yes + + ubuntu-clang-15-release: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Install Dependencies + uses: actions/cache@v2 + with: + path: ~/.conan/data + key: ${{ runner.os }}-conan-${{ hashFiles('conanfile.txt') }}-queue-spsc-release + - run: | + export BUILD_TYPE=${{env.BUILD_TYPE_RELEASE}} + ./tools/install_deps.sh + - run: | + mkdir -p ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -DCMAKE_CXX_COMPILER=/bin/${{env.CLANG_15}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE_RELEASE}} + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_RELEASE}} --target queue_spsc -- -j 2 + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_RELEASE}} --target unit_test_queue_spsc -- -j 2 +# cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE_RELEASE}} --target benchmark_stack_counting -- -j 2 + - run: ${{github.workspace}}/build/bin/unit_test_queue_spsc + --gtest_shuffle + --gtest_color=yes +# - run: ${{github.workspace}}/build/bin/benchmark_sync diff --git a/.github/workflows/spinlock.yaml b/.github/workflows/spinlock.yaml index c612b77..7d6793a 100644 --- a/.github/workflows/spinlock.yaml +++ b/.github/workflows/spinlock.yaml @@ -1,7 +1,9 @@ name: Cmake on: - workflow_call: + push: + branches: + - master env: BUILD_TYPE_DEBUG: Debug diff --git a/.github/workflows/stack_counting.yaml b/.github/workflows/stack_counting.yaml index ce95ad3..5d8e862 100644 --- a/.github/workflows/stack_counting.yaml +++ b/.github/workflows/stack_counting.yaml @@ -1,7 +1,9 @@ name: Cmake on: - workflow_call: + push: + branches: + - master env: BUILD_TYPE_DEBUG: Debug diff --git a/.github/workflows/stack_hazard_ptr.yaml b/.github/workflows/stack_hazard_ptr.yaml index 13f73e8..9237dd8 100644 --- a/.github/workflows/stack_hazard_ptr.yaml +++ b/.github/workflows/stack_hazard_ptr.yaml @@ -1,7 +1,9 @@ name: Cmake on: - workflow_call: + push: + branches: + - master env: BUILD_TYPE_DEBUG: Debug diff --git a/lockfree/CMakeLists.txt b/lockfree/CMakeLists.txt index a4c631b..89fa72c 100644 --- a/lockfree/CMakeLists.txt +++ b/lockfree/CMakeLists.txt @@ -1,4 +1,5 @@ cmake_minimum_required(VERSION 3.25) add_subdirectory(stack/counting) -add_subdirectory(stack/hazard) \ No newline at end of file +add_subdirectory(stack/hazard) +add_subdirectory(queue/spsc) diff --git a/lockfree/queue/spsc/CMakeLists.txt b/lockfree/queue/spsc/CMakeLists.txt new file mode 100644 index 0000000..ba904b4 --- /dev/null +++ b/lockfree/queue/spsc/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.25) + +include_directories(.) +add_library(queue_spsc + queue.h +) + +target_link_libraries(queue_spsc + atomic + project_sanitizers + project_warnings +) + +target_include_directories(queue_spsc PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") + +set_target_properties(queue_spsc PROPERTIES LINKER_LANGUAGE CXX) + +add_subdirectory(test) \ No newline at end of file diff --git a/lockfree/queue/spsc/queue.h b/lockfree/queue/spsc/queue.h new file mode 100644 index 0000000..a2a0d52 --- /dev/null +++ b/lockfree/queue/spsc/queue.h @@ -0,0 +1,64 @@ +// +// Created by focus on 9/13/23. +// + +#include +#include + + +namespace sync_cpp { + +template +class SPSCQueue final { + struct Node final { + std::shared_ptr data{}; + Node* next{nullptr}; + }; + + std::atomic head_; + std::atomic tail_; + + Node* pop_head() { + Node* const old_head = head_.load(); + if (old_head == tail_.load()) { + return nullptr; + } + head_.store(old_head->next); + return old_head; + } + +public: + SPSCQueue() : head_(new Node), tail_(head_.load()) + {} + + SPSCQueue(const SPSCQueue&) = delete; + SPSCQueue(SPSCQueue&&) noexcept = delete; + ~SPSCQueue() { + while (auto* const old_head = head_.load()) { + head_ = old_head->next; + delete old_head; + } + } + + void push(T new_value) { + auto new_data = std::make_shared(std::move(new_value)); + Node* p = new Node; + Node* const old_tail = tail_.load(); + old_tail->data.swap(new_data); + old_tail->next = p; + tail_.store(p); + } + + std::shared_ptr pop() { + Node* old_head = pop_head(); + if (!old_head) { + return std::shared_ptr(); + } + + std::shared_ptr res(old_head->data); + delete old_head; + return res; + } +}; + +} // namespace sync_cpp diff --git a/lockfree/queue/spsc/test/CMakeLists.txt b/lockfree/queue/spsc/test/CMakeLists.txt new file mode 100644 index 0000000..2826795 --- /dev/null +++ b/lockfree/queue/spsc/test/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.25) + +include_directories(${CONAN_INCLUDE_DIRS_GTEST}) +include_directories(${CONAN_INCLUDE_DIRS_BENCHMARK}) + +set(TEST_APP_NAME unit_test_queue_spsc) + +add_executable(${TEST_APP_NAME} + main.cpp + unit-tests.cpp +) + +target_link_libraries(${TEST_APP_NAME} + queue_spsc + ${CONAN_LIBS_GTEST} + project_sanitizers + project_warnings +) + +enable_testing() \ No newline at end of file diff --git a/lockfree/queue/spsc/test/main.cpp b/lockfree/queue/spsc/test/main.cpp new file mode 100644 index 0000000..478d0a4 --- /dev/null +++ b/lockfree/queue/spsc/test/main.cpp @@ -0,0 +1,11 @@ +// +// Created by focus on 9/13/23. +// + +#include "gtest/gtest.h" + +int +main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/lockfree/queue/spsc/test/unit-tests.cpp b/lockfree/queue/spsc/test/unit-tests.cpp new file mode 100644 index 0000000..7b833b1 --- /dev/null +++ b/lockfree/queue/spsc/test/unit-tests.cpp @@ -0,0 +1,66 @@ +// +// Created by focus on 9/13/23. +// + +#include "gtest/gtest.h" + +#include + +#include + +class TestSPSCQueue : public ::testing::Test { +public: +}; + +TEST_F(TestSPSCQueue, simple_push_pop) { + sync_cpp::SPSCQueue q; + + q.push(117); + auto item = q.pop(); + ASSERT_TRUE(item); + ASSERT_EQ(*item, 117); + + auto empty = q.pop(); + ASSERT_FALSE(empty); +} + +TEST_F(TestSPSCQueue, only_pop) { + sync_cpp::SPSCQueue q; + + auto empty = q.pop(); + ASSERT_FALSE(empty); + + empty = q.pop(); + ASSERT_FALSE(empty); +} + +TEST_F(TestSPSCQueue, 2_threads) { + sync_cpp::SPSCQueue q; + + auto th1 = std::jthread([&] { + for (size_t i=0; i<20'000; i++) { + q.push(i); + } + }); + + std::vector result; + result.reserve(20'000); + size_t counter = 0; + while(true) { + auto data = q.pop(); + if (!data) { + continue; + } + counter++; + result.push_back(*data); + if (counter == 20'000) { + break; + } + } + + std::sort(result.begin(), result.end()); + + for(size_t i=0; i<20'000; i++) { + ASSERT_EQ(result[i], i); + } +} \ No newline at end of file diff --git a/lockfree/stack/hazard/hp.h b/lockfree/stack/hazard/hp.h new file mode 100644 index 0000000..a4e8bf3 --- /dev/null +++ b/lockfree/stack/hazard/hp.h @@ -0,0 +1,28 @@ +// +// Created by focus on 9/12/23. +// + +#pragma once + +#include +#include +#include +#include + +namespace sync_cpp { + +static constexpr size_t MaxHazardPointers = 100; + +struct HazardPtr { + std::atomic id; + std::atomic ptr; +}; + +HazardPtr hazard_ptrs[MaxHazardPointers]; + +template +void do_delete(void *p) { + delete static_cast(p); +} + +} // namespace sync_cpp \ No newline at end of file diff --git a/lockfree/stack/hazard/hp_owner.h b/lockfree/stack/hazard/hp_owner.h index 324e5ca..cfa4f9e 100644 --- a/lockfree/stack/hazard/hp_owner.h +++ b/lockfree/stack/hazard/hp_owner.h @@ -5,30 +5,20 @@ #include #include -namespace sync_cpp { - -static constexpr size_t MaxHazardPointers = 100; -struct HazardPtr -{ - std::atomic id; - std::atomic ptr; -}; - -HazardPtr hazard_ptrs[MaxHazardPointers]; +#include "hp.h" -template -void do_delete(void* p) { - delete static_cast(p); -} +namespace sync_cpp { class hp_owner { + HazardPtr* hp; + public: hp_owner() : hp(nullptr) { - for(size_t i=0; iptr.store(nullptr); hp->id.store(std::this_thread::get_id()); } - - - private: - HazardPtr* hp; }; diff --git a/lockfree/stack/hazard/reclaim.h b/lockfree/stack/hazard/reclaim.h index f0f49b6..8ace9bc 100644 --- a/lockfree/stack/hazard/reclaim.h +++ b/lockfree/stack/hazard/reclaim.h @@ -2,6 +2,7 @@ #include #include +#include #include "hp_owner.h" @@ -14,7 +15,7 @@ struct Reclaim { data_to_reclaim* next; template - data_to_reclaim(T* p): + explicit data_to_reclaim(T* p): data(p), deleter(&do_delete), next(nullptr) @@ -25,43 +26,40 @@ struct Reclaim { } }; - std::atomic nodes_to_reclame_; + std::atomic nodes_to_reclaim_; - void add_to_reclame_list(data_to_reclaim* node) { - node->next = nodes_to_reclame_.load(); - while(!nodes_to_reclame_.compare_exchange_weak(node->next, node)) {} + void add_to_reclaim_list(data_to_reclaim* node) { + node->next = nodes_to_reclaim_.load(); + while(!nodes_to_reclaim_.compare_exchange_weak(node->next, node)) {} } template void reclaim_later(T* data) { - add_to_reclame_list(new data_to_reclaim(data)); + add_to_reclaim_list(new data_to_reclaim(data)); } void delete_nodes_with_no_hazard() { - data_to_reclaim* current = nodes_to_reclame_.exchange(nullptr); + data_to_reclaim* current = nodes_to_reclaim_.exchange(nullptr); while (current) { data_to_reclaim* next = current->next; if (!OutstandingHazardPtrsFor(current->data)) { delete current; } else { - add_to_reclame_list(current); + add_to_reclaim_list(current); } current = next; } } - bool OutstandingHazardPtrsFor(void* p) { - for(size_t i=0; i& GetHazardPtrForCurrentThread() { + static std::atomic& GetHazardPtrForCurrentThread() { thread_local static hp_owner hazard; return hazard.get_pointer(); } diff --git a/lockfree/stack/hazard/stack.h b/lockfree/stack/hazard/stack.h index 579ad95..1e55157 100644 --- a/lockfree/stack/hazard/stack.h +++ b/lockfree/stack/hazard/stack.h @@ -17,31 +17,39 @@ class LockFreeStackHazard final : private Reclaim { Node* next; }; + std::atomic head_; + + Node* DropHead() { + std::atomic& hp = GetHazardPtrForCurrentThread(); + Node* old_head = head_.load(); + do { + Node* temp; + do { + temp = old_head; + hp.store(old_head); + old_head = head_.load(); + } while (old_head != temp); + // hp sets to head + + } while (old_head && !head_.compare_exchange_strong(old_head, old_head->next)); + + // clean hp + hp.store(nullptr); + + return old_head; + } + public: void Push(T data) { Node* node = new Node{ - std::make_shared(std::move(data)), - nullptr + .data=std::make_shared(std::move(data)), + .next=nullptr }; while(!head_.compare_exchange_weak(node->next, node)) {} } std::shared_ptr TryPop() { - std::atomic& hp = GetHazardPtrForCurrentThread(); - Node* old_head = head_.load(); - do { - Node* temp; - do { - temp = old_head; - hp.store(old_head); - old_head = head_.load(); - } while (old_head != temp); - // hp sets to head - - } while (old_head && !head_.compare_exchange_strong(old_head, old_head->next)); - - // clean hp - hp.store(nullptr); + Node* old_head = DropHead(); std::shared_ptr res; if (old_head) { @@ -59,9 +67,6 @@ class LockFreeStackHazard final : private Reclaim { ~LockFreeStackHazard() { while(TryPop()) {} } - - private: - std::atomic head_; }; } // namespace sync_cpp \ No newline at end of file diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 8fbfc65..0000000 --- a/main.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include - -int main() { - std::cout << "Hello, World!" << std::endl; - return 0; -}