commit b075f4d1a8516ee672bd0ad9be84e823d286a19a Author: Artur Mukhamadiev Date: Thu Jun 5 01:27:10 2025 +0800 [lfq][test] init commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..34fe704 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Chromium \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3df67ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +compile_commands.json +.cache diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f58d605 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.20) + +project(lfq) + +find_package(GTest REQUIRED) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_executable(${PROJECT_NAME} test.cc) + +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PRIVATE gtest::gtest) + +target_compile_options(${PROJECT_NAME} PRIVATE + -O2 + -fsanitize=thread +) + +target_link_options(${PROJECT_NAME} PRIVATE + -fsanitize=thread +) \ No newline at end of file diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json new file mode 100644 index 0000000..66dabec --- /dev/null +++ b/CMakeUserPresets.json @@ -0,0 +1,10 @@ +{ + "version": 4, + "vendor": { + "conan": {} + }, + "include": [ + "build/Release/generators/CMakePresets.json", + "build/Debug/generators/CMakePresets.json" + ] +} \ No newline at end of file diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 0000000..7045101 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,10 @@ +[requires] +gtest/1.16.0 +glog/0.7.1 +benchmark/1.9.1 + +[generators] +CMakeDeps +CMakeToolchain +[layout] +cmake_layout \ No newline at end of file diff --git a/include/queue.hh b/include/queue.hh new file mode 100644 index 0000000..72cdba4 --- /dev/null +++ b/include/queue.hh @@ -0,0 +1,115 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +namespace lfq { + +template +class LockFreeQueue { + static_assert(!(Size & (Size - 1)), "Size must be power of two"); + + struct node { + node() = default; + node(size_t pos, T obj) : pos(pos), obj(obj) {} + node(node& oth) : pos(oth.pos.load()), obj(oth.obj) {} + node(node&& oth) : pos(oth.pos.load()), obj(std::move(oth.obj)) {} + node& operator=(node& oth) { + pos = oth.pos.load(); + obj = oth.obj; + return *this; + } + node& operator=(node&& oth) { + pos = oth.pos.load(); + obj = std::move(oth.obj); + return *this; + } + + std::atomic pos{std::numeric_limits::max()}; + T obj; + }; + + public: + LockFreeQueue() : enqueue_pos(0), dequeue_pos(0) {} + + void push_back(const T& element); + void pull_front(T& element); + size_t apprx_available() const; + bool is_full() const; + bool is_empty() const; + + private: + static constexpr size_t mask{Size - 1}; + std::atomic enqueue_pos; + std::atomic dequeue_pos; + std::array elements; +}; + +/** + * @brief lfq implementation of pull operation. + * result will be written to reference argument + * it will block the thread until success + */ +template +void LockFreeQueue::pull_front(T& element) { + node* n_obj; + size_t fix_pos{0}; + bool res{false}; + while (!res) { + while (is_empty()) { + std::this_thread::yield(); + } + fix_pos = dequeue_pos.load(); + n_obj = &elements[fix_pos & mask]; + if (n_obj->pos != fix_pos) { + // someone installed the value already retry + std::this_thread::yield(); + continue; + } + res = dequeue_pos.compare_exchange_weak(fix_pos, fix_pos + 1); + } + element = n_obj->obj; + n_obj->pos = std::numeric_limits::max(); +} + +/** + * @brief lock free implementation for push back operation + * @details trying my best to make it work :D + */ +template +void LockFreeQueue::push_back(const T& element) { + size_t fix_pos{0}; + bool res{false}; + node obj; + while (!res) { + while (is_full()) { + std::this_thread::yield(); + } + fix_pos = enqueue_pos.load(); + obj = elements[fix_pos & mask]; + res = enqueue_pos.compare_exchange_weak(fix_pos, fix_pos + 1); + } + elements[fix_pos & mask] = node{fix_pos, element}; +} + +template +bool LockFreeQueue::is_full() const { + assert(enqueue_pos.load() >= dequeue_pos.load()); + return enqueue_pos.load() - dequeue_pos.load() >= Size; +} + +template +bool LockFreeQueue::is_empty() const { + return enqueue_pos.load() == dequeue_pos.load(); +} + +template +size_t LockFreeQueue::apprx_available() const { + assert(enqueue_pos.load() >= dequeue_pos.load()); + return enqueue_pos.load() - dequeue_pos.load(); +} + +} // namespace lfq \ No newline at end of file diff --git a/main.cc b/main.cc new file mode 100644 index 0000000..e8eab27 --- /dev/null +++ b/main.cc @@ -0,0 +1,8 @@ +#include +#include "queue.hh" + +int main() { + lfq::LockFreeQueue queue; + std::cout << "Hello!\n"; + return 0; +} \ No newline at end of file diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..e69de29 diff --git a/subprojects/gtest.wrap b/subprojects/gtest.wrap new file mode 100644 index 0000000..1ef791e --- /dev/null +++ b/subprojects/gtest.wrap @@ -0,0 +1,16 @@ +[wrap-file] +directory = googletest-1.17.0 +source_url = https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz +source_filename = gtest-1.17.0.tar.gz +source_hash = 65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c +patch_filename = gtest_1.17.0-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.17.0-2/get_patch +patch_hash = c6ff59f36c8ee48bcd6d968f08a5a08c2c4216a2327079c0ae2323b2e062971e +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.17.0-2/gtest-1.17.0.tar.gz +wrapdb_version = 1.17.0-2 + +[provide] +gtest = gtest_dep +gtest_main = gtest_main_dep +gmock = gmock_dep +gmock_main = gmock_main_dep diff --git a/test.cc b/test.cc new file mode 100644 index 0000000..550041d --- /dev/null +++ b/test.cc @@ -0,0 +1,72 @@ +#include +#include +#include +// #include +#include "queue.hh" + +struct Env { + size_t num_to_repeat{1000}; + size_t time{0}; +}; + +template +void producer(lfq::LockFreeQueue& queue, T el, Env env) { + for (int i = 0; i < env.num_to_repeat; ++i) { + queue.push_back({el}); + sleep(env.time); + } +} + +template +void consumer(lfq::LockFreeQueue& queue, Env env) { + T element; + for (int i = 0; i < env.num_to_repeat; ++i) { + queue.pull_front(element); + sleep(env.time); + } +} + +TEST(LFQ, spsc) { + std::cout << "Single producer single consumer\n"; + lfq::LockFreeQueue queue; + size_t element = 10; + Env env{.num_to_repeat = 10, .time = 0}; + EXPECT_NO_THROW(producer(queue, element, env)); + EXPECT_NO_THROW(consumer(queue, env)); + ASSERT_EQ(queue.apprx_available(), 0); +} + +TEST(LFQ, spmc) { + std::cout << "Single producer multi consumer\n"; + lfq::LockFreeQueue queue; + size_t element = 10; + Env env_consumer{.num_to_repeat = 1000, .time = 0}; + Env env_producer{.num_to_repeat = 10000, .time = 0}; + + std::future pobj = + std::async(std::launch::async, [env_producer, &queue] { + EXPECT_NO_THROW(producer(queue, size_t(10), env_producer)); + }); + + std::vector> consumers; + for (int i = 0; i < 10; ++i) { + consumers.emplace_back( + std::async(std::launch::async, [env_consumer, &queue] { + EXPECT_NO_THROW(consumer(queue, env_consumer)); + })); + } + + for (auto& cobj : consumers) { + EXPECT_NO_THROW(cobj.wait()); + } + ASSERT_EQ(queue.apprx_available(), 0); +} + +TEST(LFQ, mpsc) {} + +TEST(LFQ, mpmc) {} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file