diff --git a/.github/workflows/sanitizer.yml b/.github/workflows/sanitizer.yml new file mode 100644 index 0000000..0825cf4 --- /dev/null +++ b/.github/workflows/sanitizer.yml @@ -0,0 +1,101 @@ +name: Sanitize + +on: + push: + branches: + - master + tags: + - v**.** + pull_request: + branches: + - master + +jobs: + sanitize: + strategy: + fail-fast: false + matrix: + # sanitize: [address, memory, thread] + sanitize: [address] + include: + - sanitize: address + sanitize_flag: ENABLE_LLVM_ADDRESS_SANITIZER + # - sanitize: memory + # sanitize_flag: ENABLE_LLVM_MEMORY_SANITIZER + # - sanitize: thread + # sanitize_flag: ENABLE_LLVM_THREAD_SANITIZER + + env: + os: "ubuntu-latest" + build_type: "Debug" + cxx_standard: "20" + c_compiler: "clang" + cxx_compiler: "clang++" + conan_preset: "conan-debug" + + name: ${{ matrix.sanitize }} + + runs-on: "ubuntu-latest" + + steps: + - uses: actions/checkout@v4 + - uses: seanmiddleditch/gha-setup-ninja@v4 + - uses: seanmiddleditch/gha-setup-vsdevenv@master + - uses: KyleMayes/install-llvm-action@v1 + with: + version: "17.0" + directory: ${{ runner.temp }}/llvm + + - name: Install GCC + shell: bash + run: | + sudo add-apt-repository ppa:ubuntu-toolchain-r/test + sudo apt update + sudo apt install gcc-13 g++-13 + sudo update-alternatives --remove-all gcc || true + sudo update-alternatives --remove-all g++ || true + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 10 --slave /usr/bin/g++ g++ /usr/bin/g++-13 + + - name: Install conan + shell: bash + env: + CC: "${{ env.c_compiler != 'cl' && env.c_compiler || '' }}" + CXX: "${{ env.cxx_compiler != 'cl' && env.cxx_compiler || '' }}" + run: | + pip install conan + conan profile detect --name ci --force + python $GITHUB_WORKSPACE/support/update-conan-profile.py $(conan profile path ci) ${{env.build_type}} ${{env.c_compiler}} ${{env.cxx_compiler}} ${{env.cxx_standard}} + + - name: Cache conan packages + id: cache-conan + uses: actions/cache@v3 + with: + path: ~/.conan2/p + key: conan-cache-packages-${{ env.os }}-${{ env.c_compiler }}-${{ env.build_type }}-${{ env.cxx_standard }} + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake + shell: bash + working-directory: ${{runner.workspace}}/build + env: + CC: ${{env.c_compiler}} + CXX: ${{env.cxx_compiler}} + run: | + conan install $GITHUB_WORKSPACE --output-folder=. --build="*" -pr ci -pr:b ci -s build_type=${{ env.build_type }} + conan cache clean + cmake $GITHUB_WORKSPACE --preset ${{ env.conan_preset }} -D${{ matrix.sanitize_flag }}:BOOL=ON + + - name: Build + working-directory: ${{runner.workspace}}/build + shell: bash + run: | + cmake --build ./build/${{ env.build_type }} + cmake -E make_directory ${{runner.workspace}}/installation/SEDManager + cmake --install ./build/${{ env.build_type }} --prefix '${{runner.workspace}}/installation/SEDManager' + + - name: Test + working-directory: ${{runner.workspace}}/build + shell: bash + run: ./build/${{ env.build_type }}/bin/test diff --git a/.gitignore b/.gitignore index 21a9a50..44d73a5 100644 --- a/.gitignore +++ b/.gitignore @@ -943,7 +943,7 @@ CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts -Testing +#Testing Makefile cmake_install.cmake install_manifest.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 249d641..370d12f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,19 +19,23 @@ if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") add_compile_options("-fprofile-instr-generate" "-fcoverage-mapping" "-mllvm" "-enable-name-compression=false") add_link_options("-fprofile-instr-generate" "-fcoverage-mapping") endif() + file(REAL_PATH "${CMAKE_SOURCE_DIR}/sanitize_ignorelist.txt" sanitize_ignorelist) if (ENABLE_LLVM_ADDRESS_SANITIZER) message("Using address sanitizer") add_compile_options("-fsanitize=address") + add_compile_options("-fsanitize-ignorelist=${sanitize_ignorelist}") add_link_options("-fsanitize=address") endif() if (ENABLE_LLVM_MEMORY_SANITIZER) message("Using memory sanitizer") add_compile_options("-fsanitize=memory") + add_compile_options("-fsanitize-ignorelist=${sanitize_ignorelist}") add_link_options("-fsanitize=memory") endif() if (ENABLE_LLVM_THREAD_SANITIZER) message("Using thread sanitizer") add_compile_options("-fsanitize=thread") + add_compile_options("-fsanitize-ignorelist=${sanitize_ignorelist}") add_link_options("-fsanitize=thread") endif() endif() diff --git a/include/asyncpp/awaitable.hpp b/include/asyncpp/awaitable.hpp deleted file mode 100644 index aa07d8d..0000000 --- a/include/asyncpp/awaitable.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include -#include - - -namespace asyncpp { - -template -struct basic_awaitable { - virtual ~basic_awaitable() = default; - virtual void on_ready() noexcept = 0; -}; - -} // namespace asyncpp \ No newline at end of file diff --git a/include/asyncpp/container/atomic_collection.hpp b/include/asyncpp/container/atomic_collection.hpp index 4d4de77..146ab05 100644 --- a/include/asyncpp/container/atomic_collection.hpp +++ b/include/asyncpp/container/atomic_collection.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../testing/suspension_point.hpp" #include #include @@ -10,47 +11,41 @@ namespace asyncpp { template class atomic_collection { public: - atomic_collection() = default; + atomic_collection() noexcept = default; Element* push(Element* element) noexcept { Element* first = nullptr; do { element->*next = first; - } while (first != CLOSED && !m_first.compare_exchange_weak(first, element)); + } while (first != CLOSED && !INTERLEAVED(m_first.compare_exchange_weak(first, element))); return first; } Element* detach() noexcept { - return m_first.exchange(nullptr); + return INTERLEAVED(m_first.exchange(nullptr)); } Element* close() noexcept { - return m_first.exchange(CLOSED); + return INTERLEAVED(m_first.exchange(CLOSED)); } bool empty() const noexcept { - return m_first.load(std::memory_order_relaxed) == nullptr || closed(); + const auto item = m_first.load(std::memory_order_relaxed); + return item == nullptr || closed(item); } bool closed() const noexcept { - return m_first.load(std::memory_order_relaxed) == CLOSED; + return closed(m_first.load(std::memory_order_relaxed)); } static bool closed(Element* element) { return element == CLOSED; } - Element* first() { + Element* first() const noexcept { return m_first.load(std::memory_order_relaxed); } - const Element* first() const { - return m_first.load(std::memory_order_relaxed); - } - -protected: - atomic_collection(Element* first) : m_first(first) {} - private: std::atomic m_first = nullptr; static inline Element* const CLOSED = reinterpret_cast(std::numeric_limits::max()); diff --git a/include/asyncpp/container/atomic_deque.hpp b/include/asyncpp/container/atomic_deque.hpp new file mode 100644 index 0000000..198a3cb --- /dev/null +++ b/include/asyncpp/container/atomic_deque.hpp @@ -0,0 +1,129 @@ +#pragma once + +#include "../threading/spinlock.hpp" + +#include +#include + + +namespace asyncpp { + +template +class deque { +public: + Element* push_front(Element* element) noexcept { + element->*prev = nullptr; + element->*next = m_front; + if (m_front) { + m_front->*prev = element; + } + else { + m_back = element; + } + return std::exchange(m_front, element); + } + + Element* push_back(Element* element) noexcept { + element->*prev = m_back; + element->*next = nullptr; + if (m_back) { + m_back->*next = element; + } + else { + m_front = element; + } + return std::exchange(m_back, element); + } + + Element* pop_front() noexcept { + if (m_front) { + const auto element = std::exchange(m_front, m_front->*next); + if (m_front) { + m_front->*prev = nullptr; + } + else { + m_back = nullptr; + } + element->*next = nullptr; + return element; + } + return nullptr; + } + + Element* pop_back() noexcept { + if (m_back) { + const auto element = std::exchange(m_back, m_back->*prev); + if (m_back) { + m_back->*next = nullptr; + } + else { + m_front = nullptr; + } + element->*prev = nullptr; + return element; + } + return nullptr; + } + + Element* front() const noexcept { + return m_front; + } + + Element* back() const noexcept { + return m_back; + } + + bool empty() const noexcept { + return m_back == nullptr; + } + +private: + Element* m_front = nullptr; + Element* m_back = nullptr; +}; + + +template +class atomic_deque { +public: + Element* push_front(Element* element) noexcept { + std::lock_guard lk(m_mutex); + return m_container.push_front(element); + } + + Element* push_back(Element* element) noexcept { + std::lock_guard lk(m_mutex); + return m_container.push_back(element); + } + + Element* pop_front() noexcept { + std::lock_guard lk(m_mutex); + return m_container.pop_front(); + } + + Element* pop_back() noexcept { + std::lock_guard lk(m_mutex); + return m_container.pop_back(); + } + + Element* front() const noexcept { + std::lock_guard lk(m_mutex); + return m_container.front(); + } + + Element* back() const noexcept { + std::lock_guard lk(m_mutex); + return m_container.back(); + } + + bool empty() const noexcept { + std::lock_guard lk(m_mutex); + return m_container.empty(); + } + +private: + deque m_container; + mutable spinlock m_mutex; +}; + +} // namespace asyncpp \ No newline at end of file diff --git a/include/asyncpp/container/atomic_item.hpp b/include/asyncpp/container/atomic_item.hpp new file mode 100644 index 0000000..67c7639 --- /dev/null +++ b/include/asyncpp/container/atomic_item.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "../testing/suspension_point.hpp" + +#include +#include + + +namespace asyncpp { + +template +class atomic_item { +public: + atomic_item() noexcept = default; + + Element* set(Element* element) noexcept { + Element* expected = nullptr; + INTERLEAVED(m_item.compare_exchange_strong(expected, element)); + return expected; + } + + Element* close() noexcept { + return INTERLEAVED(m_item.exchange(CLOSED)); + } + + bool empty() const noexcept { + const auto item = m_item.load(std::memory_order_relaxed); + return item == nullptr || closed(item); + } + + bool closed() const noexcept { + return closed(m_item.load(std::memory_order_relaxed)); + } + + static bool closed(Element* element) { + return element == CLOSED; + } + + Element* item() const noexcept { + return m_item.load(std::memory_order_relaxed); + } + +private: + std::atomic m_item = nullptr; + static inline Element* const CLOSED = reinterpret_cast(std::numeric_limits::max()); +}; + + +} // namespace asyncpp \ No newline at end of file diff --git a/include/asyncpp/container/atomic_queue.hpp b/include/asyncpp/container/atomic_queue.hpp deleted file mode 100644 index 8a7b03b..0000000 --- a/include/asyncpp/container/atomic_queue.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include "../threading/spinlock.hpp" - -#include -#include - - -namespace asyncpp { - -template -class atomic_queue { -public: - Element* push(Element* element) noexcept { - std::lock_guard lk(m_mtx); - const auto prev_front = m_front.load(std::memory_order_relaxed); - element->*prev = prev_front; - element->*next = nullptr; - m_front.store(element, std::memory_order_relaxed); - if (prev_front == nullptr) { - m_back.store(element, std::memory_order_relaxed); - } - else { - prev_front->*next = element; - } - return prev_front; - } - - Element* pop() noexcept { - std::lock_guard lk(m_mtx); - const auto prev_back = m_back.load(std::memory_order_relaxed); - if (prev_back != nullptr) { - const auto new_back = prev_back->*next; - m_back.store(new_back, std::memory_order_relaxed); - if (new_back == nullptr) { - m_front.store(nullptr, std::memory_order_relaxed); - } - else { - new_back->*prev = nullptr; - } - } - return prev_back; - } - - Element* front() { - return m_front.load(std::memory_order_relaxed); - } - - Element* back() { - return m_back.load(std::memory_order_relaxed); - } - - bool empty() const noexcept { - return m_back.load(std::memory_order_relaxed) == nullptr; - } - -private: - std::atomic m_front; - std::atomic m_back; - mutable spinlock m_mtx; -}; - -} // namespace asyncpp \ No newline at end of file diff --git a/include/asyncpp/container/atomic_stack.hpp b/include/asyncpp/container/atomic_stack.hpp index a793124..448e027 100644 --- a/include/asyncpp/container/atomic_stack.hpp +++ b/include/asyncpp/container/atomic_stack.hpp @@ -3,37 +3,64 @@ #include "../threading/spinlock.hpp" #include +#include namespace asyncpp { + +template +class stack { +public: + Element* push(Element* element) noexcept { + element->*next = m_top; + return std::exchange(m_top, element); + } + + Element* pop() noexcept { + const auto new_top = m_top ? m_top->*next : nullptr; + return std::exchange(m_top, new_top); + } + + Element* top() const noexcept { + return m_top; + } + + bool empty() const noexcept { + return m_top == nullptr; + } + +private: + Element* m_top = nullptr; +}; + + template class atomic_stack { public: Element* push(Element* element) noexcept { - std::lock_guard lk(m_mtx); - const auto prev_first = m_first.load(std::memory_order_relaxed); - element->*next = prev_first; - m_first = element; - return prev_first; + std::lock_guard lk(m_mutex); + return m_container.push(element); } Element* pop() noexcept { - std::lock_guard lk(m_mtx); - const auto prev_first = m_first.load(std::memory_order_relaxed); - if (prev_first != nullptr) { - m_first = prev_first->*next; - } - return prev_first; + std::lock_guard lk(m_mutex); + return m_container.pop(); + } + + Element* top() const noexcept { + std::lock_guard lk(m_mutex); + return m_container.top(); } bool empty() const noexcept { - return m_first.load(std::memory_order_relaxed) == nullptr; + std::lock_guard lk(m_mutex); + return m_container.empty(); } private: - std::atomic m_first = nullptr; - mutable spinlock m_mtx; + stack m_container; + mutable spinlock m_mutex; }; } // namespace asyncpp \ No newline at end of file diff --git a/include/asyncpp/event.hpp b/include/asyncpp/event.hpp index 2c01b4d..c332dea 100644 --- a/include/asyncpp/event.hpp +++ b/include/asyncpp/event.hpp @@ -1,121 +1,204 @@ #pragma once #include "container/atomic_collection.hpp" -#include "interleaving/sequence_point.hpp" +#include "container/atomic_item.hpp" #include "promise.hpp" #include -#include -#include +#include namespace asyncpp { -namespace impl_event { - - template - class promise { - public: - struct awaitable : basic_awaitable { - awaitable* m_next = nullptr; - - awaitable(promise* owner) noexcept : m_owner(owner) {} +template +class basic_event { +public: + struct awaitable { + basic_event* m_owner = nullptr; + resumable_promise* m_enclosing = nullptr; + awaitable* m_next = nullptr; + + bool await_ready() const { + assert(m_owner); + return m_owner->m_awaiter.closed(); + } - bool await_ready() const noexcept { - return m_owner->ready(); + template Promise> + bool await_suspend(std::coroutine_handle promise) { + assert(m_owner); + m_enclosing = &promise.promise(); + const auto status = m_owner->m_awaiter.set(this); + if (status != nullptr && !m_owner->m_awaiter.closed(status)) { + m_owner->m_awaiter.set(status); + throw std::invalid_argument("event already awaited"); } + return !m_owner->m_awaiter.closed(status); + } - template Promise> - bool await_suspend(std::coroutine_handle enclosing) noexcept { - m_enclosing = &enclosing.promise(); - const bool ready = m_owner->await(this); - return !ready; - } + T await_resume() { + assert(m_owner); + assert(m_owner->m_result.has_value()); + return static_cast(m_owner->m_result.move_or_throw()); + } + }; - auto await_resume() -> typename task_result::reference { - return m_owner->get_result().get_or_throw(); - } +public: + basic_event() = default; + basic_event(basic_event&&) = delete; + basic_event& operator=(basic_event&&) = delete; + ~basic_event() { + assert(m_awaiter.empty()); + } - void on_ready() noexcept final { - m_enclosing->resume(); - } + void set_exception(std::exception_ptr ex) { + set(task_result(std::move(ex))); + } - private: - resumable_promise* m_enclosing = nullptr; - promise* m_owner = nullptr; - }; - - public: - promise() = default; - promise(promise&&) = delete; - promise(const promise&) = delete; - promise& operator=(promise&&) = delete; - promise& operator=(const promise&) = delete; - - void set(task_result result) noexcept { - m_result = std::move(result); - finalize(); + void set(task_result result) { + if (m_result.has_value()) { + throw std::invalid_argument("event already set"); } + m_result = std::move(result); + resume_one(); + } - awaitable await() noexcept { - return { this }; - } + bool ready() const noexcept { + return m_awaiter.closed(); + } - bool ready() const noexcept { - return INTERLEAVED(m_awaiters.closed()); - } + awaitable operator co_await() { + return awaitable{ this }; + } + + task_result& _debug_get_result() { + return m_result; + } - private: - bool await(awaitable* awaiter) noexcept { - const auto previous = INTERLEAVED(m_awaiters.push(awaiter)); - return m_awaiters.closed(previous); +protected: + void resume_one() { + auto item = m_awaiter.close(); + assert(!m_awaiter.closed(item)); + if (item != nullptr) { + assert(item->m_enclosing != nullptr); + item->m_enclosing->resume(); } + } - void finalize() noexcept { - auto awaiter = INTERLEAVED(m_awaiters.close()); - assert(!m_awaiters.closed(awaiter) && "cannot set event twice"); - while (awaiter != nullptr) { - const auto next = awaiter->m_next; - awaiter->on_ready(); - awaiter = next; - } +protected: + atomic_item m_awaiter; + task_result m_result; +}; + + +template +class event : public basic_event { +public: + void set_value(T value) { + this->set(task_result(std::forward(value))); + } +}; + + +template <> +class event : public basic_event { +public: + void set_value() { + set(task_result(nullptr)); + } +}; + + +template +class basic_broadcast_event { +public: + struct awaitable { + basic_broadcast_event* m_owner = nullptr; + resumable_promise* m_enclosing = nullptr; + awaitable* m_next = nullptr; + + bool await_ready() const { + assert(m_owner); + return m_owner->m_awaiters.closed(); } - auto& get_result() noexcept { - return this->m_result; + template Promise> + bool await_suspend(std::coroutine_handle promise) { + assert(m_owner); + m_enclosing = &promise.promise(); + const auto status = m_owner->m_awaiters.push(this); + return !m_owner->m_awaiters.closed(status); } - private: - task_result m_result; - atomic_collection m_awaiters; + auto await_resume() -> std::conditional_t, void, std::add_lvalue_reference_t> { + assert(m_owner); + assert(m_owner->m_result.has_value()); + return m_owner->m_result.get_or_throw(); + } }; -} // namespace impl_event +public: + basic_broadcast_event() = default; + basic_broadcast_event(basic_broadcast_event&&) = delete; + basic_broadcast_event& operator=(basic_broadcast_event&&) = delete; + ~basic_broadcast_event() { + assert(m_awaiters.empty()); + } + void set_exception(std::exception_ptr ex) { + set(task_result(std::move(ex))); + } -template -class event { -public: - event() = default; - event(const event&) = delete; - event(event&&) = delete; - event& operator=(const event&) = delete; - event& operator=(event&&) = delete; + void set(task_result result) { + if (m_result.has_value()) { + throw std::invalid_argument("event already set"); + } + m_result = std::move(result); + resume_all(); + } - auto operator co_await() noexcept { - return m_promise.await(); + bool ready() const noexcept { + return m_awaiters.closed(); } - void set_value(T value) { - m_promise.set(std::move(value)); + awaitable operator co_await() { + return awaitable{ this }; } - void set_exception(std::exception_ptr ex) { - m_promise.set(std::move(ex)); + task_result& _debug_get_result() { + return m_result; + } + +protected: + void resume_all() { + auto first = m_awaiters.close(); + while (first != nullptr) { + assert(first->m_enclosing != nullptr); + first->m_enclosing->resume(); + first = first->m_next; + } + } + +protected: + atomic_collection m_awaiters; + task_result m_result; +}; + + +template +class broadcast_event : public basic_broadcast_event { +public: + void set_value(T value) { + this->set(task_result(std::forward(value))); } +}; + -private: - impl_event::promise m_promise; +template <> +class broadcast_event : public basic_broadcast_event { +public: + void set_value() { + set(task_result(nullptr)); + } }; } // namespace asyncpp \ No newline at end of file diff --git a/include/asyncpp/interleaving/runner.hpp b/include/asyncpp/interleaving/runner.hpp deleted file mode 100644 index 9455b97..0000000 --- a/include/asyncpp/interleaving/runner.hpp +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include "../generator.hpp" -#include "sequencer.hpp" - -#include -#include -#include -#include -#include -#include -#include - - -namespace asyncpp::interleaving { - - -struct deadlock_error : std::runtime_error { - deadlock_error() : std::runtime_error("deadlock") {} -}; - - -using interleaving = std::vector>; - - -struct interleaving_printer { - const interleaving& il; - bool detail = false; -}; - - -std::ostream& operator<<(std::ostream& os, const interleaving_printer& il); - - -class filter { -public: - filter() : filter(".*") {} - explicit filter(std::string_view file_regex) : m_files(file_regex.begin(), file_regex.end()) {} - - bool operator()(const sequence_point& point) const; - -private: - std::regex m_files; -}; - - -generator run_all(std::function fixture, - std::vector> threads, - std::vector names = {}, - filter filter_ = {}); - - -template - requires std::convertible_to -generator run_all(std::function fixture, - std::vector> threads, - std::vector names = {}, - filter filter_ = {}) { - std::function wrapped_init = [fixture = std::move(fixture)]() -> std::any { - if constexpr (!std::is_void_v) { - return std::any(fixture()); - } - return {}; - }; - - std::vector> wrapped_threads; - std::ranges::transform(threads, std::back_inserter(wrapped_threads), [](auto& thread) { - return std::function([thread = std::move(thread)](std::any& fixture) { - return thread(std::any_cast(fixture)); - }); - }); - - return run_all(std::move(wrapped_init), std::move(wrapped_threads), std::move(names), filter_); -} - - -inline generator run_all(std::vector> threads, - std::vector names = {}, - filter filter_ = {}) { - std::vector> wrapped_threads; - std::ranges::transform(threads, std::back_inserter(wrapped_threads), [](auto& thread) { - return std::function([thread = std::move(thread)](std::any&) { - return thread(); - }); - }); - return run_all([] { return std::any(); }, std::move(wrapped_threads), std::move(names), filter_); -} - -} // namespace asyncpp::interleaving diff --git a/include/asyncpp/interleaving/sequence_point.hpp b/include/asyncpp/interleaving/sequence_point.hpp deleted file mode 100644 index 35f6660..0000000 --- a/include/asyncpp/interleaving/sequence_point.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include -#include - - -namespace asyncpp::interleaving { - - -struct sequence_point { - bool acquire = false; - std::string_view name = {}; - std::string_view function = {}; - std::string_view file = {}; - uint64_t line = 0; -}; - - -namespace impl_sp { - void wait(sequence_point& sp); -} - - -#if defined(ASYNCPP_BUILD_TESTS) && ASYNCPP_BUILD_TESTS - #define SEQUENCE_POINT(NAME) \ - { \ - static ::asyncpp::interleaving::sequence_point sp = { \ - false, \ - NAME, \ - __func__, \ - __FILE__, \ - __LINE__, \ - }; \ - ::asyncpp::interleaving::impl_sp::wait(sp); \ - } \ - void() - - - #define SEQUENCE_POINT_ACQUIRE(NAME) \ - { \ - static ::asyncpp::interleaving::sequence_point sp = { \ - true, \ - NAME, \ - __func__, \ - __FILE__, \ - __LINE__, \ - }; \ - ::asyncpp::interleaving::impl_sp::wait(sp); \ - } \ - void() - - - #define INTERLEAVED(EXPR) \ - [&](std::string_view func) -> decltype(auto) { \ - static ::asyncpp::interleaving::sequence_point sp = { \ - false, \ - #EXPR, \ - func, \ - __FILE__, \ - __LINE__, \ - }; \ - ::asyncpp::interleaving::impl_sp::wait(sp); \ - return EXPR; \ - }(__func__) - - - #define INTERLEAVED_ACQUIRE(EXPR) \ - [&](std::string_view func) -> decltype(auto) { \ - static ::asyncpp::interleaving::sequence_point sp = { \ - true, \ - #EXPR, \ - func, \ - __FILE__, \ - __LINE__, \ - }; \ - ::asyncpp::interleaving::impl_sp::wait(sp); \ - return EXPR; \ - }(__func__) -#else - #define SEQUENCE_POINT(NAME) void() - #define SEQUENCE_POINT_ACQUIRE(NAME) void() - #define INTERLEAVED(EXPR) EXPR - #define INTERLEAVED_ACQUIRE(EXPR) EXPR -#endif - -} // namespace asyncpp::interleaving diff --git a/include/asyncpp/interleaving/sequencer.hpp b/include/asyncpp/interleaving/sequencer.hpp deleted file mode 100644 index bec9b81..0000000 --- a/include/asyncpp/interleaving/sequencer.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include -#include -#include - - -namespace asyncpp::interleaving { - -struct sequence_point; - - -class sequencer { -public: - struct state { - std::strong_ordering operator<=>(const state&) const noexcept = default; - enum { - waiting, - running, - acquiring, - } activity; - sequence_point* location = nullptr; - }; - - static inline sequence_point* const RUNNING = reinterpret_cast(std::numeric_limits::max() - 2); - static inline sequence_point* const ACQUIRING = reinterpret_cast(std::numeric_limits::max() - 1); - - sequencer(std::string name = {}); - - auto get_name() const -> std::string_view; - auto get_state() const -> state; - void wait(sequence_point& location); - bool allow(); - -private: - std::atomic m_location = RUNNING; - std::string m_name; -}; - -} // namespace asyncpp::interleaving \ No newline at end of file diff --git a/include/asyncpp/interleaving/state_tree.hpp b/include/asyncpp/interleaving/state_tree.hpp deleted file mode 100644 index 299c958..0000000 --- a/include/asyncpp/interleaving/state_tree.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "sequencer.hpp" - -#include -#include -#include - - -namespace asyncpp::interleaving { - - -struct state { - std::strong_ordering operator<=>(const state&) const noexcept = default; - std::map, sequencer::state> m_sequencer_states; -}; - - -class state_tree { - using branch_key = std::pair, state>; - -public: - state_tree(state state); - - auto branch(const std::shared_ptr& allowed_thread, const state& resulting_state) -> std::shared_ptr; - auto get_state() const -> const state&; - auto get_branches() const -> const std::map>&; - void mark_complete(); - bool is_marked_complete() const; - void prune(); - -private: - std::map> m_branches; - state m_state; - bool m_complete = false; -}; - -} // namespace asyncpp::interleaving \ No newline at end of file diff --git a/include/asyncpp/join.hpp b/include/asyncpp/join.hpp index 32bcdd6..8c691a7 100644 --- a/include/asyncpp/join.hpp +++ b/include/asyncpp/join.hpp @@ -1,8 +1,8 @@ #pragma once #include "concepts.hpp" -#include "interleaving/sequence_point.hpp" #include "promise.hpp" +#include "testing/suspension_point.hpp" #include #include diff --git a/include/asyncpp/lock.hpp b/include/asyncpp/lock.hpp index 6885238..471f8ce 100644 --- a/include/asyncpp/lock.hpp +++ b/include/asyncpp/lock.hpp @@ -48,7 +48,7 @@ class locked_mutex_shared { template class unique_lock { - using mutex_awaitable = std::invoke_result_t; + using mutex_awaitable = std::invoke_result_t; struct awaitable { unique_lock* m_lock; mutex_awaitable m_awaitable; @@ -99,7 +99,7 @@ class unique_lock { auto operator co_await() noexcept { assert(!owns_lock()); - return awaitable(this, m_mtx->unique()); + return awaitable(this, m_mtx->exclusive()); } void unlock() noexcept { diff --git a/include/asyncpp/memory/rc_ptr.hpp b/include/asyncpp/memory/rc_ptr.hpp new file mode 100644 index 0000000..8a4f2dd --- /dev/null +++ b/include/asyncpp/memory/rc_ptr.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include + + +namespace asyncpp { + +class rc_from_this { + template + friend class rc_ptr; + + std::atomic_size_t m_rc = 0; +}; + + +template +struct rc_default_delete { + void operator()(T* object) const { + object->destroy(); + } +}; + + +template > +class rc_ptr { +public: + rc_ptr() noexcept(std::is_nothrow_constructible_v) + : m_ptr(nullptr) {} + + explicit rc_ptr(T* ptr, Deleter deleter = {}) noexcept(std::is_nothrow_move_constructible_v) + : m_ptr(ptr), + m_deleter(std::move(deleter)) { + increment(); + } + + rc_ptr(rc_ptr&& other) noexcept(std::is_nothrow_move_constructible_v) + : m_ptr(std::exchange(other.m_ptr, nullptr)), + m_deleter(std::move(other.m_deleter)) {} + + rc_ptr(const rc_ptr& other) noexcept(std::is_nothrow_copy_constructible_v) + : m_ptr(other.m_ptr), + m_deleter(other.m_deleter) { + increment(); + } + + rc_ptr& operator=(rc_ptr&& other) noexcept(std::is_nothrow_move_constructible_v) { + decrement(); + m_ptr = std::exchange(other.m_ptr, nullptr); + m_deleter = std::move(other.m_deleter); + return *this; + } + + rc_ptr& operator=(const rc_ptr& other) { + if (this != &other) { + decrement(); + m_ptr = other.m_ptr; + m_deleter = other.m_deleter; + increment(); + } + return *this; + } + + ~rc_ptr() { + decrement(); + } + + void reset(T* ptr = nullptr) { + decrement(); + m_ptr = ptr; + increment(); + } + + T* get() const noexcept { + return m_ptr; + } + + T& operator*() const noexcept { + assert(m_ptr); + return *m_ptr; + } + + T* operator->() const noexcept { + assert(m_ptr); + return m_ptr; + } + + size_t use_count() const noexcept { + if (m_ptr) { + return m_ptr->rc_from_this::m_rc.load(std::memory_order_relaxed); + } + return 0; + } + + bool unique() const noexcept { + return use_count() == 1; + } + + explicit operator bool() const noexcept { + return m_ptr != nullptr; + } + + auto operator<=>(const rc_ptr& other) const noexcept { + return m_ptr <=> other.m_ptr; + } + + auto operator==(const rc_ptr& other) const noexcept { + return m_ptr == other.m_ptr; + } + +private: + void increment() const noexcept { + if (m_ptr) { + m_ptr->rc_from_this::m_rc.fetch_add(1, std::memory_order_relaxed); + } + } + + void decrement() const { + if (m_ptr) { + const auto count = m_ptr->rc_from_this::m_rc.fetch_sub(1, std::memory_order_relaxed) - 1; + if (count == 0) { + m_deleter(m_ptr); + } + } + } + +private: + T* m_ptr = nullptr; + Deleter m_deleter; +}; + +} // namespace asyncpp \ No newline at end of file diff --git a/include/asyncpp/mutex.hpp b/include/asyncpp/mutex.hpp index e9146cc..0cd77de 100644 --- a/include/asyncpp/mutex.hpp +++ b/include/asyncpp/mutex.hpp @@ -1,34 +1,31 @@ #pragma once -#include "container/atomic_queue.hpp" +#include "container/atomic_deque.hpp" #include "lock.hpp" #include "promise.hpp" #include "threading/spinlock.hpp" #include -#include namespace asyncpp { class mutex { struct awaitable { + mutex* m_owner = nullptr; + resumable_promise* m_enclosing = nullptr; awaitable* m_next = nullptr; awaitable* m_prev = nullptr; - awaitable(mutex* mtx) : m_mtx(mtx) {} - bool await_ready() noexcept; + bool await_ready() const noexcept; + template Promise> bool await_suspend(std::coroutine_handle enclosing) noexcept; - locked_mutex await_resume() noexcept; - void on_ready() noexcept; - private: - mutex* m_mtx; - resumable_promise* m_enclosing = nullptr; + locked_mutex await_resume() noexcept; }; - bool lock_enqueue(awaitable* waiting); + bool add_awaiting(awaitable* waiting); public: mutex() = default; @@ -39,21 +36,28 @@ class mutex { ~mutex(); bool try_lock() noexcept; - awaitable unique() noexcept; + awaitable exclusive() noexcept; awaitable operator co_await() noexcept; void unlock(); + void _debug_clear() noexcept; + bool _debug_is_locked() noexcept; + private: - atomic_queue m_queue; - bool m_locked = false; + // Front: the next awaiting coroutine to acquire, back: last coroutine to acquire. + deque m_queue; + // Used to protect the queue data structure. spinlock m_spinlock; + // At the front, signifies mutex is locked. + awaitable m_sentinel; }; template Promise> bool mutex::awaitable::await_suspend(std::coroutine_handle enclosing) noexcept { + assert(m_owner); m_enclosing = &enclosing.promise(); - const bool ready = m_mtx->lock_enqueue(this); + const bool ready = m_owner->add_awaiting(this); return !ready; } diff --git a/include/asyncpp/promise.hpp b/include/asyncpp/promise.hpp index 590b33d..31d0667 100644 --- a/include/asyncpp/promise.hpp +++ b/include/asyncpp/promise.hpp @@ -1,10 +1,10 @@ #pragma once -#include "awaitable.hpp" - #include #include +#include #include +#include namespace asyncpp { @@ -21,10 +21,8 @@ struct task_result { std::optional> m_result; task_result() = default; - - task_result(value_type value) : m_result(std::move(value)) {} - - task_result(std::exception_ptr value) : m_result(std::move(value)) {} + explicit task_result(value_type value) : m_result(std::move(value)) {} + explicit task_result(std::exception_ptr value) : m_result(std::move(value)) {} task_result& operator=(value_type value) { m_result = std::move(value); @@ -51,6 +49,16 @@ struct task_result { } return static_cast(std::get(value)); } + + value_type move_or_throw() { + auto& value = m_result.value(); // Throws if empty. + if (std::holds_alternative(value)) { + std::rethrow_exception(std::get(value)); + } + return std::move(std::get(value)); + } + + auto operator<=>(const task_result&) const noexcept = default; }; diff --git a/include/asyncpp/shared_mutex.hpp b/include/asyncpp/shared_mutex.hpp index b906a1b..d0dc498 100644 --- a/include/asyncpp/shared_mutex.hpp +++ b/include/asyncpp/shared_mutex.hpp @@ -1,6 +1,6 @@ #pragma once -#include "container/atomic_queue.hpp" +#include "container/atomic_deque.hpp" #include "lock.hpp" #include "promise.hpp" #include "threading/spinlock.hpp" @@ -12,49 +12,41 @@ namespace asyncpp { class shared_mutex { + enum class awaitable_type { + exclusive, + shared, + unknown, + }; + struct basic_awaitable { + shared_mutex* m_owner = nullptr; + awaitable_type m_type = awaitable_type::unknown; basic_awaitable* m_next = nullptr; basic_awaitable* m_prev = nullptr; + resumable_promise* m_enclosing = nullptr; - basic_awaitable(shared_mutex* mtx) : m_mtx(mtx) {} - virtual ~basic_awaitable() = default; - virtual void on_ready() noexcept = 0; - virtual bool is_shared() const noexcept = 0; - - protected: - shared_mutex* m_mtx; - }; - - struct awaitable : basic_awaitable { - using basic_awaitable::basic_awaitable; - - bool await_ready() noexcept; template Promise> bool await_suspend(std::coroutine_handle enclosing) noexcept; - locked_mutex await_resume() noexcept; - void on_ready() noexcept final; - bool is_shared() const noexcept final; + }; - private: - resumable_promise* m_enclosing = nullptr; + struct exclusive_awaitable : basic_awaitable { + explicit exclusive_awaitable(shared_mutex* owner = nullptr) + : basic_awaitable(owner, awaitable_type::exclusive) {} + + bool await_ready() const noexcept; + locked_mutex await_resume() const noexcept; }; struct shared_awaitable : basic_awaitable { - using basic_awaitable::basic_awaitable; + explicit shared_awaitable(shared_mutex* owner = nullptr) + : basic_awaitable(owner, awaitable_type::shared) {} - bool await_ready() noexcept; - template Promise> - bool await_suspend(std::coroutine_handle enclosing) noexcept; - locked_mutex_shared await_resume() noexcept; - void on_ready() noexcept final; - bool is_shared() const noexcept final; - - private: - resumable_promise* m_enclosing = nullptr; + bool await_ready() const noexcept; + locked_mutex_shared await_resume() const noexcept; }; - bool lock_enqueue(awaitable* waiting); - bool lock_enqueue_shared(shared_awaitable* waiting); + bool add_awaiting(basic_awaitable* waiting); + void continue_waiting(std::unique_lock& lk); public: shared_mutex() = default; @@ -66,33 +58,29 @@ class shared_mutex { bool try_lock() noexcept; bool try_lock_shared() noexcept; - awaitable unique() noexcept; + exclusive_awaitable exclusive() noexcept; shared_awaitable shared() noexcept; void unlock(); void unlock_shared(); + void _debug_clear() noexcept; + bool _debug_is_exclusive_locked() const noexcept; + size_t _debug_is_shared_locked() const noexcept; private: - using queue_t = atomic_queue; - queue_t m_queue; - intptr_t m_locked = 0; - intptr_t m_unique_waiting = false; + deque m_queue; spinlock m_spinlock; + exclusive_awaitable m_exclusive_sentinel; + shared_awaitable m_shared_sentinel; + size_t m_shared_count = 0; }; template Promise> -bool shared_mutex::awaitable::await_suspend(std::coroutine_handle enclosing) noexcept { - m_enclosing = &enclosing.promise(); - const bool ready = m_mtx->lock_enqueue(this); - return !ready; -} - - -template Promise> -bool shared_mutex::shared_awaitable::await_suspend(std::coroutine_handle enclosing) noexcept { +bool shared_mutex::basic_awaitable::await_suspend(std::coroutine_handle enclosing) noexcept { m_enclosing = &enclosing.promise(); - const bool ready = m_mtx->lock_enqueue_shared(this); + assert(m_owner); + const bool ready = m_owner->add_awaiting(this); return !ready; } diff --git a/include/asyncpp/shared_task.hpp b/include/asyncpp/shared_task.hpp index 91ab58f..0ac8210 100644 --- a/include/asyncpp/shared_task.hpp +++ b/include/asyncpp/shared_task.hpp @@ -1,10 +1,10 @@ #pragma once -#include "awaitable.hpp" -#include "container/atomic_collection.hpp" -#include "interleaving/sequence_point.hpp" +#include "event.hpp" +#include "memory/rc_ptr.hpp" #include "promise.hpp" #include "scheduler.hpp" +#include "testing/suspension_point.hpp" #include #include @@ -20,65 +20,26 @@ class shared_task; namespace impl_shared_task { template - struct chained_awaitable : basic_awaitable { - chained_awaitable* m_next; - }; - - template - struct promise : result_promise, resumable_promise, schedulable_promise, impl::leak_checked_promise { + struct promise : result_promise, resumable_promise, schedulable_promise, impl::leak_checked_promise, rc_from_this { struct final_awaitable { constexpr bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle handle) noexcept { auto& owner = handle.promise(); - - auto awaiting = INTERLEAVED(owner.m_awaiting.close()); - while (awaiting != nullptr) { - awaiting->on_ready(); - awaiting = awaiting->m_next; - } - - owner.release(); + owner.m_event.set(owner.m_result); + auto self = std::move(owner.m_self); // owner.m_self.reset() call method on owner after it's been deleted. + self.reset(); } constexpr void await_resume() const noexcept {} }; auto get_return_object() { - return shared_task(this); + return shared_task(rc_ptr(this)); } auto initial_suspend() noexcept { return std::suspend_always{}; } - void start() noexcept { - const bool has_started = INTERLEAVED(m_started.test_and_set()); - if (!has_started) { - acquire(); - resume(); - } - } - - bool await(chained_awaitable* awaiter) { - start(); - const auto previous = INTERLEAVED(m_awaiting.push(awaiter)); - return m_awaiting.closed(previous); - } - - void acquire() { - INTERLEAVED(m_references.fetch_add(1, std::memory_order_release)); - } - - void release() { - const auto references = INTERLEAVED(m_references.fetch_sub(1, std::memory_order_acquire)); - if (references == 1) { - handle().destroy(); - } - } - - auto& get_result() noexcept { - return this->m_result; - } - auto final_suspend() noexcept { return final_awaitable{}; } @@ -91,43 +52,49 @@ namespace impl_shared_task { return m_scheduler ? m_scheduler->schedule(*this) : handle().resume(); } + void start() noexcept { + if (!INTERLEAVED(m_started.test_and_set(std::memory_order_relaxed))) { + m_self.reset(this); + resume(); + } + } + + static auto await(rc_ptr pr); + bool ready() const { - return INTERLEAVED(m_awaiting.closed()); + return m_event.ready(); + } + + void destroy() { + handle().destroy(); } private: - std::atomic_size_t m_references = 0; std::atomic_flag m_started; - atomic_collection, &chained_awaitable::m_next> m_awaiting; + broadcast_event m_event; + rc_ptr m_self; }; template - struct awaitable : chained_awaitable { - promise* m_awaited = nullptr; - resumable_promise* m_enclosing = nullptr; + struct awaitable : broadcast_event::awaitable { + using base = typename broadcast_event::awaitable; - awaitable(promise* awaited) : m_awaited(awaited) {} + rc_ptr> m_awaited = nullptr; - constexpr bool await_ready() const noexcept { - return m_awaited->ready(); - } - - template Promise> - bool await_suspend(std::coroutine_handle enclosing) noexcept { - m_enclosing = &enclosing.promise(); - const bool ready = m_awaited->await(this); - return !ready; + awaitable(base base, rc_ptr> awaited) : broadcast_event::awaitable(std::move(base)), m_awaited(awaited) { + assert(m_awaited); } + }; - auto await_resume() -> typename task_result::reference { - return m_awaited->get_result().get_or_throw(); - } - void on_ready() noexcept final { - m_enclosing->resume(); - } - }; + template + auto promise::await(rc_ptr pr) { + assert(pr); + pr->start(); + auto base = pr->m_event.operator co_await(); + return awaitable{ std::move(base), std::move(pr) }; + } } // namespace impl_shared_task @@ -138,65 +105,36 @@ class shared_task { using promise_type = impl_shared_task::promise; shared_task() = default; - - shared_task(const shared_task& rhs) noexcept : shared_task(rhs.m_promise) {} - - shared_task& operator=(const shared_task& rhs) noexcept { - if (m_promise) { - m_promise->release(); - } - m_promise = rhs.m_promise; - m_promise->acquire(); - return *this; - } - - shared_task(shared_task&& rhs) noexcept : m_promise(std::exchange(rhs.m_promise, nullptr)) {} - - shared_task& operator=(shared_task&& rhs) noexcept { - if (m_promise) { - m_promise->release(); - } - m_promise = std::exchange(rhs.m_promise, nullptr); - return *this; - } - - shared_task(promise_type* promise) : m_promise(promise) { - m_promise->acquire(); - } - - ~shared_task() { - if (m_promise) { - m_promise->release(); - } - } + shared_task(rc_ptr promise) : m_promise(std::move(promise)) {} bool valid() const { - return m_promise != nullptr; + return !!m_promise; } - void launch() { - assert(valid()); - m_promise->start(); - } - - bool ready() { + bool ready() const { assert(valid()); return m_promise->ready(); } - auto operator co_await() const { + void launch() { assert(valid()); - return impl_shared_task::awaitable(m_promise); + m_promise->start(); } void bind(scheduler& scheduler) { + assert(valid()); if (m_promise) { m_promise->m_scheduler = &scheduler; } } + auto operator co_await() { + assert(valid()); + return promise_type::await(m_promise); + } + private: - promise_type* m_promise; + rc_ptr m_promise; }; diff --git a/include/asyncpp/sleep.hpp b/include/asyncpp/sleep.hpp index b216e11..b4818f4 100644 --- a/include/asyncpp/sleep.hpp +++ b/include/asyncpp/sleep.hpp @@ -1,6 +1,5 @@ #pragma once -#include "awaitable.hpp" #include "promise.hpp" #include @@ -14,21 +13,20 @@ namespace impl_sleep { using clock_type = std::chrono::steady_clock; - struct awaitable : basic_awaitable { + struct awaitable { + resumable_promise* m_enclosing = nullptr; + explicit awaitable(clock_type::time_point time) noexcept; bool await_ready() const noexcept; template Promise> void await_suspend(std::coroutine_handle enclosing) noexcept; void await_resume() const noexcept; - void on_ready() noexcept override; auto get_time() const noexcept -> clock_type::time_point; private: void enqueue() noexcept; - clock_type::time_point m_time; - resumable_promise* m_enclosing = nullptr; }; template Promise> diff --git a/include/asyncpp/stream.hpp b/include/asyncpp/stream.hpp index 56dc54e..214830f 100644 --- a/include/asyncpp/stream.hpp +++ b/include/asyncpp/stream.hpp @@ -1,14 +1,12 @@ #pragma once -#include "awaitable.hpp" -#include "interleaving/sequence_point.hpp" +#include "event.hpp" +#include "memory/rc_ptr.hpp" #include "promise.hpp" #include "scheduler.hpp" -#include #include #include -#include #include #include @@ -23,50 +21,83 @@ class stream; namespace impl_stream { template - struct promise : resumable_promise, schedulable_promise, impl::leak_checked_promise { + using wrapper_type = typename task_result::wrapper_type; + + template + using reference_type = typename task_result::reference; + + + template + struct [[nodiscard]] item { + item(std::optional> result) : m_result(std::move(result)) {} + + explicit operator bool() const noexcept { + return !!m_result; + } + + T& operator*() noexcept { + assert(m_result); + return m_result.value(); + } + + std::remove_reference_t* operator->() noexcept { + assert(m_result); + return std::addressof(static_cast(m_result.value())); + } + + const T& operator*() const noexcept { + assert(m_result); + return m_result.get(); + } + + const std::remove_reference_t* operator->() const noexcept { + assert(m_result); + return std::addressof(static_cast(m_result.value())); + } + + private: + std::optional> m_result; + }; + + + template + struct promise : resumable_promise, schedulable_promise, impl::leak_checked_promise, rc_from_this { struct yield_awaitable { - constexpr bool await_ready() const noexcept { - return false; - } + constexpr bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle handle) const noexcept { auto& owner = handle.promise(); - const auto state = INTERLEAVED(owner.m_state.exchange(READY)); - assert(state != STOPPED && state != READY); - if (state != RUNNING) { - state->on_ready(); - } + assert(owner.m_event); + assert(owner.m_result.has_value()); + owner.m_event->set(std::move(owner.m_result)); } constexpr void await_resume() const noexcept {} }; auto get_return_object() noexcept { - return stream(this); + return stream(rc_ptr(this)); } - constexpr auto initial_suspend() const noexcept { - return std::suspend_always{}; + constexpr std::suspend_always initial_suspend() const noexcept { + return {}; } - bool await(basic_awaitable* awaiting) { - INTERLEAVED(m_state.store(RUNNING)); - resume(); - const auto state = INTERLEAVED(m_state.exchange(awaiting)); - assert(state == RUNNING || state == READY); - return state == READY; + yield_awaitable final_suspend() const noexcept { + return {}; } - auto get_result() { - return std::move(m_result); + yield_awaitable yield_value(T value) noexcept { + m_result = std::optional(wrapper_type(std::forward(value))); + return {}; } - auto final_suspend() const noexcept { - return yield_awaitable{}; + void unhandled_exception() noexcept { + m_result = std::current_exception(); } - void release() noexcept { - handle().destroy(); + void return_void() noexcept { + m_result = std::nullopt; } auto handle() -> std::coroutine_handle<> override { @@ -77,65 +108,57 @@ namespace impl_stream { return m_scheduler ? m_scheduler->schedule(*this) : handle().resume(); } - void unhandled_exception() noexcept { - m_result = std::current_exception(); + void destroy() noexcept { + handle().destroy(); } - auto yield_value(T value) noexcept { - m_result = std::forward(value); - return yield_awaitable{}; - } + auto await() noexcept; - void return_void() noexcept { - m_result.clear(); + bool has_event() const noexcept { + return !!m_event; } private: - static inline const auto STOPPED = reinterpret_cast*>(std::numeric_limits::max() - 14); - static inline const auto RUNNING = reinterpret_cast*>(std::numeric_limits::max() - 13); - static inline const auto READY = reinterpret_cast*>(std::numeric_limits::max() - 12); - std::atomic*> m_state = STOPPED; - task_result m_result; + std::optional>>> m_event; + task_result>> m_result; }; template - struct awaitable : basic_awaitable { - task_result m_result; - promise* m_awaited = nullptr; - resumable_promise* m_enclosing = nullptr; + struct awaitable { + using base = typename event>>::awaitable; + + base m_base; + rc_ptr> m_awaited = nullptr; - awaitable(promise* awaited) : m_awaited(awaited) {} + awaitable(base base, rc_ptr> awaited) : m_base(base), m_awaited(awaited) {} bool await_ready() noexcept { - return false; + assert(m_awaited->has_event()); + return m_base.await_ready(); } template Promise> bool await_suspend(std::coroutine_handle enclosing) { - m_enclosing = &enclosing.promise(); - const bool ready = m_awaited->await(this); - return !ready; - } - - auto await_resume() -> std::optional::wrapper_type> { - // Result was ready in await suspend or result was nullopt. - if (!m_result.has_value()) { - m_result = m_awaited->get_result(); - } - // Ensure result was truly nullopt. - if (!m_result.has_value()) { - return std::nullopt; - } - return m_result.get_or_throw(); + assert(m_awaited->has_event()); + return m_base.await_suspend(enclosing); } - void on_ready() noexcept final { - m_result = m_awaited->get_result(); - m_enclosing->resume(); + item await_resume() { + assert(m_awaited->has_event()); + return { m_base.await_resume() }; } }; + + template + auto promise::await() noexcept { + m_event.emplace(); + auto aw = awaitable(m_event->operator co_await(), rc_ptr(this)); + resume(); + return aw; + } + } // namespace impl_stream @@ -147,38 +170,20 @@ class [[nodiscard]] stream { stream() = default; stream(const stream&) = delete; stream& operator=(const stream&) = delete; - stream(stream&& rhs) noexcept : m_promise(std::exchange(rhs.m_promise, nullptr)) {} - stream& operator=(stream&& rhs) noexcept { - release(); - m_promise = std::exchange(rhs.m_promise, nullptr); - return *this; - } - stream(promise_type* promise) : m_promise(promise) {} - ~stream() { - release(); - } + stream(stream&& rhs) noexcept = default; + stream& operator=(stream&& rhs) noexcept = default; + stream(rc_ptr promise) : m_promise(std::move(promise)) {} auto operator co_await() const { - return impl_stream::awaitable(m_promise); + return m_promise->await(); } - operator bool() const { - return good(); - } - - bool good() const { - return m_promise != nullptr; - } - -private: - void release() { - if (m_promise) { - m_promise->release(); - } + bool valid() const { + return !!m_promise; } private: - promise_type* m_promise = nullptr; + rc_ptr m_promise; }; diff --git a/include/asyncpp/task.hpp b/include/asyncpp/task.hpp index c9f6a26..9782d2f 100644 --- a/include/asyncpp/task.hpp +++ b/include/asyncpp/task.hpp @@ -1,14 +1,14 @@ #pragma once -#include "awaitable.hpp" -#include "interleaving/sequence_point.hpp" +#include "event.hpp" +#include "memory/rc_ptr.hpp" #include "promise.hpp" #include "scheduler.hpp" +#include "testing/suspension_point.hpp" #include #include #include -#include #include @@ -22,121 +22,80 @@ class task; namespace impl_task { template - struct promise : result_promise, resumable_promise, schedulable_promise, impl::leak_checked_promise { + struct promise : result_promise, resumable_promise, schedulable_promise, impl::leak_checked_promise, rc_from_this { struct final_awaitable { constexpr bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle handle) const noexcept { auto& owner = handle.promise(); - const auto state = INTERLEAVED(owner.m_state.exchange(READY)); - assert(state != CREATED && state != READY); // May be RUNNING || AWAITED - if (state != RUNNING) { - state->on_ready(); - } - owner.release(); + owner.m_event.set(owner.m_result); + auto self = std::move(owner.m_self); // owner.m_self.reset() call method on owner after it's been deleted. + self.reset(); } constexpr void await_resume() const noexcept {} }; auto get_return_object() { - return task(this); + return task(rc_ptr(this)); } constexpr auto initial_suspend() noexcept { - INTERLEAVED(m_released.test_and_set()); return std::suspend_always{}; } - void start() noexcept { - auto created = CREATED; - const bool success = INTERLEAVED(m_state.compare_exchange_strong(created, RUNNING)); - if (success) { - INTERLEAVED(m_released.clear()); - resume(); - } - } - - bool await(basic_awaitable* awaiter) noexcept { - start(); - auto state = INTERLEAVED(m_state.exchange(awaiter)); - assert(state == CREATED || state == RUNNING || state == READY); - if (state == READY) { - INTERLEAVED(m_state.store(READY)); - } - return state == READY; - } - - void release() noexcept { - const auto released = INTERLEAVED(m_released.test_and_set()); - if (released) { - handle().destroy(); - } - } - auto final_suspend() noexcept { return final_awaitable{}; } - task_result get_result() noexcept { - return std::move(this->m_result); - } - auto handle() -> std::coroutine_handle<> final { return std::coroutine_handle::from_promise(*this); } void resume() final { - [[maybe_unused]] const auto state = INTERLEAVED(m_state.load()); - assert(state != READY); return m_scheduler ? m_scheduler->schedule(*this) : handle().resume(); } + void start() noexcept { + if (!INTERLEAVED(m_started.test_and_set(std::memory_order_relaxed))) { + m_self.reset(this); + resume(); + } + } + + static auto await(rc_ptr pr); + bool ready() const { - return INTERLEAVED(m_state.load()) == READY; + return m_event.ready(); + } + + void destroy() { + handle().destroy(); } private: - static inline const auto CREATED = reinterpret_cast*>(std::numeric_limits::max() - 14); - static inline const auto RUNNING = reinterpret_cast*>(std::numeric_limits::max() - 13); - static inline const auto READY = reinterpret_cast*>(std::numeric_limits::max() - 12); - std::atomic*> m_state = CREATED; - std::atomic_flag m_released; + std::atomic_flag m_started; + event m_event; + rc_ptr m_self; }; - template - struct awaitable : basic_awaitable { - promise* m_awaited = nullptr; - resumable_promise* m_enclosing = nullptr; - - awaitable(promise* awaited) : m_awaited(awaited) {} - - constexpr bool await_ready() const noexcept { - return m_awaited->ready(); - } - - template Promise> - bool await_suspend(std::coroutine_handle enclosing) noexcept { - m_enclosing = &enclosing.promise(); - const bool ready = m_awaited->await(this); - return !ready; - } + struct awaitable : event::awaitable { + using base = typename event::awaitable; - T await_resume() { - auto result = m_awaited->get_result(); - m_awaited->release(); - if constexpr (!std::is_void_v) { - return std::forward(result.get_or_throw()); - } - else { - return result.get_or_throw(); - } - } + rc_ptr> m_awaited = nullptr; - void on_ready() noexcept final { - m_enclosing->resume(); + awaitable(base base, rc_ptr> awaited) : event::awaitable(std::move(base)), m_awaited(awaited) { + assert(m_awaited); } }; + template + auto promise::await(rc_ptr pr) { + assert(pr); + pr->start(); + auto base = pr->m_event.operator co_await(); + return awaitable{ std::move(base), std::move(pr) }; + } + } // namespace impl_task @@ -148,22 +107,12 @@ class [[nodiscard]] task { task() = default; task(const task& rhs) = delete; task& operator=(const task& rhs) = delete; - task(task&& rhs) noexcept : m_promise(std::exchange(rhs.m_promise, nullptr)) {} - task& operator=(task&& rhs) noexcept { - release(); - m_promise = std::exchange(rhs.m_promise, nullptr); - return *this; - } - task(promise_type* promise) : m_promise(promise) {} - ~task() { release(); } + task(task&& rhs) noexcept = default; + task& operator=(task&& rhs) noexcept = default; + task(rc_ptr promise) : m_promise(std::move(promise)) {} bool valid() const { - return m_promise != nullptr; - } - - void launch() { - assert(valid()); - m_promise->start(); + return !!m_promise; } bool ready() const { @@ -171,26 +120,25 @@ class [[nodiscard]] task { return m_promise->ready(); } - auto operator co_await() { + void launch() { assert(valid()); - return impl_task::awaitable(std::exchange(m_promise, nullptr)); + m_promise->start(); } void bind(scheduler& scheduler) { + assert(valid()); if (m_promise) { m_promise->m_scheduler = &scheduler; } } -private: - void release() { - if (m_promise) { - std::exchange(m_promise, nullptr)->release(); - } + auto operator co_await() { + assert(valid()); + return promise_type::await(std::move(m_promise)); } private: - promise_type* m_promise = nullptr; + rc_ptr m_promise; }; diff --git a/include/asyncpp/testing/interleaver.hpp b/include/asyncpp/testing/interleaver.hpp new file mode 100644 index 0000000..32d20d8 --- /dev/null +++ b/include/asyncpp/testing/interleaver.hpp @@ -0,0 +1,208 @@ +#pragma once + +#include "suspension_point.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace asyncpp::testing { + + +struct thread_state { +private: + enum class code : size_t { + running = size_t(-1), + blocked = size_t(-2), + completed = size_t(-3), + }; + +public: + auto operator<=>(const thread_state& rhs) const = default; + + const suspension_point* get_suspension_point() const { + return is_suspended() ? reinterpret_cast(value) : nullptr; + } + + bool is_suspended() const { + return value != code::running && value != code::blocked && value != code::completed; + } + + bool is_stable() const { + return value != code::running; + } + + static thread_state suspended(const suspension_point& sp) { + return thread_state{ static_cast(reinterpret_cast(&sp)) }; + } + + static constinit const thread_state running; + static constinit const thread_state blocked; + static constinit const thread_state completed; + + code value = code::running; +}; + + +class thread { +public: + thread() = default; + thread(thread&&) = delete; + + template + thread(Func func, Args&&... args) { + const auto wrapper = [this, func = std::move(func)](std::stop_token, Args_&&... args_) { + initialize_this_thread(); + INTERLEAVED("initial_point"); + func(std::forward(args_)...); + m_content->state.store(thread_state::completed); + }; + m_content->thread = std::jthread(wrapper, std::forward(args)...); + } + + void resume(); + static void suspend_this_thread(const suspension_point& sp); + thread_state get_state() const; + +private: + void initialize_this_thread(); + void suspend(const suspension_point& sp); + +private: + struct content { + std::jthread thread; + std::atomic state = thread_state::running; + }; + std::unique_ptr m_content = std::make_unique(); + static thread_local thread* current_thread; +}; + + +template +struct thread_function { + std::string name; + void (Scenario::*func)(); +}; + + +template Str, class Scenario> +thread_function(const Str&, void (Scenario::*)()) -> thread_function; + + +struct swarm_state { + std::vector thread_states; + + auto operator<=>(const swarm_state&) const = default; +}; + + +class tree { +public: + struct transition_node; + + struct stable_node : std::enable_shared_from_this { + stable_node() = default; + stable_node(std::map> swarm_states, + std::weak_ptr predecessor) + : swarm_states(std::move(swarm_states)), predecessor(std::move(predecessor)) {} + std::map> swarm_states; + std::weak_ptr predecessor; + }; + + struct transition_node : std::enable_shared_from_this { + transition_node() = default; + transition_node(std::map> successors, + std::weak_ptr predecessor) + : successors(std::move(successors)), predecessor(std::move(predecessor)) {} + std::map> successors; + std::set completed; + std::weak_ptr predecessor; + }; + +public: + stable_node& root(); + transition_node& next(stable_node& node, const swarm_state& state); + stable_node& next(transition_node& node, int resumed); + transition_node& previous(stable_node& node); + stable_node& previous(transition_node& node); + std::string dump() const; + +private: + std::shared_ptr m_root = std::make_shared(); +}; + + +struct path { + std::vector> steps; + + std::string dump() const; +}; + + +struct validated_scenario { + ~validated_scenario() = default; + virtual void validate(const path& p) = 0; +}; + + +template +auto launch_threads(const std::vector>& thread_funcs) + -> std::pair>, std::shared_ptr> { + const auto scenario = std::make_shared(); + std::vector> threads; + for (const auto& thread_func : thread_funcs) { + threads.push_back(std::make_unique([scenario, func = thread_func.func] { (scenario.get()->*func)(); })); + } + return { std::move(threads), std::move(scenario) }; +} + +std::vector stabilize(std::span> threads); +std::vector get_states(std::span> threads); +bool is_stable(const std::vector& v); +bool is_unblocked(const std::vector& states); +int select_resumed(const swarm_state& state, const tree::transition_node& node, const std::vector>* hit_counts = nullptr); +bool is_transitively_complete(const tree& tree, const tree::stable_node& node); +void mark_complete(tree& tree, tree::stable_node& node); +path run_next_interleaving(tree& tree, std::span> swarm); + + +template +class interleaver { +public: + explicit interleaver(std::vector> thread_funcs) + : m_thread_funcs(std::move(thread_funcs)) {} + + void run() { + tree tree; + do { + auto [swarm, scenario] = launch_threads(m_thread_funcs); + const auto path_ = run_next_interleaving(tree, swarm); + if constexpr (std::convertible_to) { + scenario->validate(path_); + } + } while (!is_transitively_complete(tree, tree.root())); + } + +private: + std::vector> m_thread_funcs; +}; + + +} // namespace asyncpp::testing + + +#define INTERLEAVED_RUN(SCENARIO, ...) asyncpp::testing::interleaver({ __VA_ARGS__ }).run() +#define THREAD(NAME, METHOD) asyncpp::testing::thread_function(NAME, METHOD) \ No newline at end of file diff --git a/include/asyncpp/testing/suspension_point.hpp b/include/asyncpp/testing/suspension_point.hpp new file mode 100644 index 0000000..9f1e857 --- /dev/null +++ b/include/asyncpp/testing/suspension_point.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include + + +namespace asyncpp::testing { + + +struct suspension_point { + bool acquire = false; + std::string_view name = {}; + std::string_view function = {}; + std::string_view file = {}; + uint64_t line = 0; +}; + + +namespace impl_suspension { + void wait(suspension_point& sp); +} + + +#if defined(ASYNCPP_BUILD_TESTS) && ASYNCPP_BUILD_TESTS + #define SEQUENCE_POINT(NAME) \ + { \ + static ::asyncpp::testing::suspension_point sp = { \ + false, \ + NAME, \ + __func__, \ + __FILE__, \ + __LINE__, \ + }; \ + ::asyncpp::testing::impl_suspension::wait(sp); \ + } \ + void() + + + #define SEQUENCE_POINT_ACQUIRE(NAME) \ + { \ + static ::asyncpp::testing::suspension_point sp = { \ + true, \ + NAME, \ + __func__, \ + __FILE__, \ + __LINE__, \ + }; \ + ::asyncpp::testing::impl_suspension::wait(sp); \ + } \ + void() + + + #define INTERLEAVED(EXPR) \ + [&](std::string_view func) -> decltype(auto) { \ + static ::asyncpp::testing::suspension_point sp = { \ + false, \ + #EXPR, \ + func, \ + __FILE__, \ + __LINE__, \ + }; \ + ::asyncpp::testing::impl_suspension::wait(sp); \ + return EXPR; \ + }(__func__) + + + #define INTERLEAVED_ACQUIRE(EXPR) \ + [&](std::string_view func) -> decltype(auto) { \ + static ::asyncpp::testing::suspension_point sp = { \ + true, \ + #EXPR, \ + func, \ + __FILE__, \ + __LINE__, \ + }; \ + ::asyncpp::testing::impl_suspension::wait(sp); \ + return EXPR; \ + }(__func__) +#else + #define SEQUENCE_POINT(NAME) void() + #define SEQUENCE_POINT_ACQUIRE(NAME) void() + #define INTERLEAVED(EXPR) EXPR + #define INTERLEAVED_ACQUIRE(EXPR) EXPR +#endif + +} // namespace asyncpp::testing diff --git a/include/asyncpp/thread_pool.hpp b/include/asyncpp/thread_pool.hpp index 2b9d1de..f116cbc 100644 --- a/include/asyncpp/thread_pool.hpp +++ b/include/asyncpp/thread_pool.hpp @@ -1,11 +1,12 @@ #pragma once -#include "scheduler.hpp" #include "container/atomic_stack.hpp" +#include "scheduler.hpp" #include #include +#include #include #include @@ -14,43 +15,48 @@ namespace asyncpp { class thread_pool : public scheduler { +public: struct worker { - worker* m_next; - - public: - void add_work_item(schedulable_promise& promise); - schedulable_promise* get_work_item(); - bool has_work() const; - bool is_stopped() const; - void stop(); - void wake() const; - void wait() const; - - private: - atomic_stack m_work_items; - mutable std::condition_variable m_wake_cv; - mutable std::mutex m_wake_mutex; - std::atomic_flag m_terminated; + worker* m_next = nullptr; + + std::jthread thread; + atomic_stack worklist; }; -public: - thread_pool(size_t num_threads = std::thread::hardware_concurrency()); + thread_pool(size_t num_threads = 1); thread_pool(thread_pool&&) = delete; - thread_pool& operator=(thread_pool&&) = delete; - ~thread_pool() noexcept; + thread_pool operator=(thread_pool&&) = delete; + ~thread_pool(); - void schedule(schedulable_promise& promise) noexcept override; + void schedule(schedulable_promise& promise) override; -private: - void worker_function(std::shared_ptr thread) noexcept; -private: - std::vector m_os_threads; - std::vector> m_workers; - atomic_stack m_free_workers; + static void schedule(schedulable_promise& item, + atomic_stack& global_worklist, + std::condition_variable& global_notification, + std::mutex& global_mutex, + std::atomic_size_t& num_waiting, + worker* local = nullptr); + + static schedulable_promise* steal(std::span workers); - static thread_local std::shared_ptr local_worker; - static thread_local thread_pool* local_owner; + static void execute(worker& local, + atomic_stack& global_worklist, + std::condition_variable& global_notification, + std::mutex& global_mutex, + std::atomic_flag& terminate, + std::atomic_size_t& num_waiting, + std::span workers); + +private: + std::condition_variable m_global_notification; + std::mutex m_global_mutex; + atomic_stack m_global_worklist; + std::vector m_workers; + std::atomic_flag m_terminate; + std::atomic_size_t m_num_waiting = 0; + + inline static thread_local worker* local = nullptr; }; diff --git a/sanitize_ignorelist.txt b/sanitize_ignorelist.txt new file mode 100644 index 0000000..5f8e641 --- /dev/null +++ b/sanitize_ignorelist.txt @@ -0,0 +1 @@ +mainfile:*/catch2/* \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 98ee271..5cd90ee 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,9 +6,7 @@ target_sources(asyncpp mutex.cpp shared_mutex.cpp sleep.cpp - interleaving/runner.cpp - interleaving/sequencer.cpp - interleaving/state_tree.cpp + testing/interleaver.cpp ) target_link_libraries(asyncpp asyncpp-headers) \ No newline at end of file diff --git a/src/interleaving/runner.cpp b/src/interleaving/runner.cpp deleted file mode 100644 index ef56cfd..0000000 --- a/src/interleaving/runner.cpp +++ /dev/null @@ -1,303 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -namespace asyncpp::interleaving { - - -std::ostream& operator<<(std::ostream& os, const sequence_point& st); -std::ostream& operator<<(std::ostream& os, const sequencer::state& st); - -namespace impl_sp { - - - struct timeout_error : std::runtime_error { - timeout_error() : std::runtime_error("timeout") {} - }; - - - sequence_point initial_point{ .acquire = false, .name = "", .file = __FILE__, .line = __LINE__ }; - sequence_point final_point{ .acquire = false, .name = "", .file = __FILE__, .line = __LINE__ }; - thread_local std::shared_ptr local_sequencer; - thread_local filter local_filter; - - - interleaving* g_current_interleaving = nullptr; - - - void signal_handler(int sig) { - if (g_current_interleaving) { - std::cout << interleaving_printer{ *g_current_interleaving, true } << std::endl; - g_current_interleaving = nullptr; - } - std::quick_exit(-1); - } - - - template - void wait_while(Func func, std::chrono::milliseconds timeout) { - const auto start = std::chrono::high_resolution_clock::now(); - while (func()) { - const auto now = std::chrono::high_resolution_clock::now(); - if (now - start > timeout) { - throw timeout_error(); - } - } - } - - - void wait(sequence_point& sp) { - if (local_sequencer) { - if (local_filter(sp)) { - local_sequencer->wait(sp); - } - } - } - - - static state get_current_states(std::span> sequencers) { - state state_; - for (const auto& seq : sequencers) { - const auto seq_state = seq->get_state(); - state_.m_sequencer_states[seq] = seq_state; - } - return state_; - } - - - static state sync_sequencers(std::span> sequencers, std::chrono::milliseconds timeout) { - state state_; - const auto verify_synced = [&state_, &sequencers] { - state_ = get_current_states(sequencers); - const auto synced = std::ranges::all_of(state_.m_sequencer_states, [](const auto& seq_state) { - return seq_state.second.activity == sequencer::state::acquiring || seq_state.second.activity == sequencer::state::waiting; - }); - return synced; - }; - wait_while([&] { return !verify_synced(); }, timeout); - return state_; - } - - - auto select_allowed_sequencer(const state& state, std::span> excluded = {}) -> std::shared_ptr { - const auto is_waiting = [](const sequencer::state& seq_state) { - return seq_state.activity == sequencer::state::waiting; - }; - const auto is_finished = [](const sequencer::state& seq_state) { - return seq_state.location == &final_point; - }; - - std::vector> choices; - for (const auto& [seq, seq_state] : state.m_sequencer_states) { - if (is_waiting(seq_state) && !is_finished(seq_state) && std::ranges::find(excluded, seq) == excluded.end()) { - choices.push_back(seq); - } - } - std::ranges::sort(choices, [](const auto& lhs, const auto& rhs) { return lhs->get_name() < rhs->get_name(); }); - - return choices.empty() ? nullptr : choices.front(); - } - - - auto get_excluded_sequencers(const state_tree& tree) -> std::vector> { - const auto is_completed = [&tree](const auto& seq) { - for (const auto& [key, branch] : tree.get_branches()) { - if (key.first == seq && branch->is_marked_complete()) { - return true; - } - } - return false; - }; - - std::vector> excluded; - for (const auto& [seq, seq_state] : tree.get_state().m_sequencer_states) { - if (is_completed(seq)) { - excluded.push_back(seq); - } - } - return excluded; - } - - - auto is_complete(const state_tree& tree) -> bool { - return select_allowed_sequencer(tree.get_state(), get_excluded_sequencers(tree)) == nullptr; - } - - - void task_thread_func(std::shared_ptr seq, std::function func, filter filter_) { - local_filter = std::move(filter_); - local_sequencer = seq; - seq->wait(initial_point); - func(); - seq->wait(final_point); - } - - - interleaving control_thread_func(std::span> sequencers, std::shared_ptr tree) { - using namespace std::chrono_literals; - sync_sequencers(sequencers, 200ms); - - std::vector path = { tree }; - interleaving interleaving; - - g_current_interleaving = &interleaving; - const auto prev_handler = std::signal(SIGABRT, &signal_handler); - - std::shared_ptr selected; - do { - try { - selected = nullptr; - const auto excluded = get_excluded_sequencers(*path.back()); - state selection_state; - const auto select = [&] { - selection_state = get_current_states(sequencers); - const bool final = std::ranges::all_of(selection_state.m_sequencer_states | std::views::values, [](auto& v) { return v.location == &final_point; }); - if (final) { - return false; - } - selected = select_allowed_sequencer(selection_state, excluded); - return selected == nullptr; - }; - try { - wait_while(select, 200ms); - } - catch (timeout_error&) { - throw deadlock_error(); - } - if (selected) { - interleaving.emplace_back(selected->get_name(), selection_state.m_sequencer_states.at(selected)); - selected->allow(); - const auto synced_state = sync_sequencers(sequencers, 200ms); - const auto branch = path.back()->branch(selected, synced_state); - path.push_back(branch); - } - } - catch (timeout_error&) { - std::cerr << interleaving_printer{ interleaving, true }; - std::cerr << "\nTIMED OUT"; - std::terminate(); - } - catch (deadlock_error&) { - std::cerr << interleaving_printer{ interleaving, true }; - std::cerr << "\nDEADLOCK"; - std::terminate(); - } - } while (selected); - for (const auto& branch : std::views::reverse(path)) { - if (is_complete(*branch)) { - branch->mark_complete(); - branch->prune(); - } - } - - sync_sequencers(sequencers, 200ms); - for (const auto& exec_thread : sequencers) { - exec_thread->allow(); - } - - std::signal(SIGABRT, prev_handler); - g_current_interleaving = nullptr; - - return interleaving; - } - -} // namespace impl_sp - - -generator run_all(std::function fixture, std::vector> threads, std::vector names, filter filter_) { - using namespace impl_sp; - - std::vector> sequencers; - for (size_t i = 0; i < threads.size(); ++i) { - std::string name = i < names.size() ? std::string(names[i]) : std::format("${}", i); - const auto seq = std::make_shared(std::move(name)); - sequencers.push_back(seq); - } - - std::map, sequencer::state> initial_state; - for (const auto& exec_thread : sequencers) { - initial_state[exec_thread] = { sequencer::state::waiting, &initial_point }; - } - const auto root = std::make_shared(state{ initial_state }); - - do { - interleaving interleaving_; - { - std::vector os_threads; - auto fixture_val = fixture(); - for (size_t i = 0; i < threads.size(); ++i) { - os_threads.emplace_back([exec_thread = sequencers[i], &tsk = threads[i], &fixture_val, &filter_] { - task_thread_func( - exec_thread, [&] { tsk(fixture_val); }, filter_); - }); - } - interleaving_ = control_thread_func(sequencers, root); - } - co_yield interleaving_; - } while (!root->is_marked_complete()); -} - - -std::ostream& operator<<(std::ostream& os, const sequence_point& st) { - os << st.function << "\n"; - os << " " << st.name; - if (st.acquire) { - os << " [ACQUIRE]"; - } - os << "\n"; - if (!st.file.empty()) { - os << " " << st.file << ":" << st.line; - } - return os; -} - - -std::ostream& operator<<(std::ostream& os, const sequencer::state& st) { - switch (st.activity) { - case sequencer::state::waiting: return os << *st.location; - case sequencer::state::running: return os << "running"; - case sequencer::state::acquiring: return os << "acquiring"; - default: std::terminate(); - } -} - - -std::ostream& operator<<(std::ostream& os, const interleaving_printer& il) { - if (il.detail) { - size_t idx = 1; - for (auto& node : il.il) { - os << idx++ << ". " << node.first << ": " << node.second << std::endl; - } - } - else { - for (auto& node : il.il) { - os << "{" << node.first << ": "; - switch (node.second.activity) { - case sequencer::state::waiting: os << node.second.location->name; break; - case sequencer::state::running: os << "running"; break; - case sequencer::state::acquiring: os << "acquiring"; break; - default: std::terminate(); - } - os << "} -> "; - } - os << "X"; - } - return os; -} - - -bool filter::operator()(const sequence_point& point) const { - return std::regex_search(point.file.begin(), point.file.end(), m_files); -} - - -} // namespace asyncpp::interleaving \ No newline at end of file diff --git a/src/interleaving/sequencer.cpp b/src/interleaving/sequencer.cpp deleted file mode 100644 index 85bd1f1..0000000 --- a/src/interleaving/sequencer.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include - -#include - - -namespace asyncpp::interleaving { - -sequencer::sequencer(std::string name) : m_name(name) {} - -void sequencer::wait(sequence_point& location) { - m_location.store(&location); - do { - } while (m_location.load() == &location); -} - - -bool sequencer::allow() { - const auto current_state = m_location.load(); - assert(current_state != ACQUIRING && current_state != RUNNING); - const auto next_state = current_state->acquire ? ACQUIRING : RUNNING; - const auto prev_state = m_location.exchange(next_state); - return prev_state != ACQUIRING && prev_state != RUNNING; -} - - -auto sequencer::get_state() const -> state { - const auto current_state = m_location.load(); - if (current_state == RUNNING) { - return { .activity = state::running }; - } - if (current_state == ACQUIRING) { - return { .activity = state::acquiring }; - } - return { .activity = state::waiting, .location = current_state }; -} - - -std::string_view sequencer::get_name() const { - return m_name; -} - -} // namespace asyncpp::interleaving \ No newline at end of file diff --git a/src/interleaving/state_tree.cpp b/src/interleaving/state_tree.cpp deleted file mode 100644 index 3681856..0000000 --- a/src/interleaving/state_tree.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include - - -namespace asyncpp::interleaving { - - -state_tree::state_tree(state state) - : m_state(std::move(state)) {} - - -std::shared_ptr state_tree::branch(const std::shared_ptr& allowed_thread, - const state& resulting_state) { - branch_key key(allowed_thread, resulting_state); - auto it = m_branches.find(key); - if (it == m_branches.end()) { - const auto br = std::make_shared(resulting_state); - it = m_branches.insert_or_assign(key, br).first; - } - return it->second; -} - - -const state& state_tree::get_state() const { - return m_state; -} - - -auto state_tree::get_branches() const -> const std::map>& { - return m_branches; -} - - -void state_tree::mark_complete() { - m_complete = true; -} - - -bool state_tree::is_marked_complete() const { - return m_complete; -} - - -void state_tree::prune() { - m_branches = {}; -} - - -} // namespace asyncpp::interleaving \ No newline at end of file diff --git a/src/mutex.cpp b/src/mutex.cpp index dfdeb8a..012841b 100644 --- a/src/mutex.cpp +++ b/src/mutex.cpp @@ -5,68 +5,83 @@ namespace asyncpp { -bool mutex::awaitable::await_ready() noexcept { - return m_mtx->try_lock(); +bool mutex::awaitable::await_ready() const noexcept { + assert(m_owner); + return m_owner->try_lock(); } locked_mutex mutex::awaitable::await_resume() noexcept { - return { m_mtx }; -} - - -void mutex::awaitable::on_ready() noexcept { - assert(m_enclosing); - m_enclosing->resume(); + assert(m_owner); + return { m_owner }; } mutex::~mutex() { std::lock_guard lk(m_spinlock); - if (m_locked) { + // Mutex must be unlocked before it's destroyed. + if (!m_queue.empty()) { std::terminate(); } } bool mutex::try_lock() noexcept { std::lock_guard lk(m_spinlock); - return std::exchange(m_locked, true) == false; + if (m_queue.empty()) { + m_queue.push_back(&m_sentinel); + return true; + } + return false; } -mutex::awaitable mutex::unique() noexcept { +mutex::awaitable mutex::exclusive() noexcept { return { this }; } mutex::awaitable mutex::operator co_await() noexcept { - return unique(); + return exclusive(); } -bool mutex::lock_enqueue(awaitable* waiting) { +bool mutex::add_awaiting(awaitable* waiting) { std::lock_guard lk(m_spinlock); - const bool acquired = std::exchange(m_locked, true) == false; - if (acquired) { + const auto previous = m_queue.push_back(waiting); + // We've just acquired the lock. + if (previous == nullptr) { + m_queue.push_back(&m_sentinel); + m_queue.pop_front(); return true; } - m_queue.push(waiting); + // We haven't acquired the lock. return false; } void mutex::unlock() { std::unique_lock lk(m_spinlock); - assert(m_locked); - m_locked = false; - const auto next = m_queue.pop(); - if (next) { - m_locked = true; - } - lk.unlock(); - if (next) { - next->on_ready(); + assert(!m_queue.empty()); // Sentinel must be in the queue. + const auto sentinel = m_queue.pop_front(); + assert(sentinel == &m_sentinel); + const auto next = m_queue.pop_front(); + if (next != nullptr) { + m_queue.push_front(&m_sentinel); + lk.unlock(); + assert(next->m_enclosing); + next->m_enclosing->resume(); } } + +void mutex::_debug_clear() noexcept { + m_queue.~deque(); + new (&m_queue) decltype(m_queue); +} + + +bool mutex::_debug_is_locked() noexcept { + return m_queue.front() == &m_sentinel; +} + } // namespace asyncpp \ No newline at end of file diff --git a/src/shared_mutex.cpp b/src/shared_mutex.cpp index 23cad19..4386c13 100644 --- a/src/shared_mutex.cpp +++ b/src/shared_mutex.cpp @@ -5,59 +5,42 @@ namespace asyncpp { -bool shared_mutex::awaitable::await_ready() noexcept { - return m_mtx->try_lock(); +bool shared_mutex::exclusive_awaitable::await_ready() const noexcept { + assert(m_owner); + return m_owner->try_lock(); } -locked_mutex shared_mutex::awaitable::await_resume() noexcept { - return { m_mtx }; +bool shared_mutex::shared_awaitable::await_ready() const noexcept { + assert(m_owner); + return m_owner->try_lock_shared(); } -void shared_mutex::awaitable::on_ready() noexcept { - assert(m_enclosing); - m_enclosing->resume(); +locked_mutex shared_mutex::exclusive_awaitable::await_resume() const noexcept { + assert(m_owner); + return { m_owner }; } -bool shared_mutex::awaitable::is_shared() const noexcept { - return false; -} - - -bool shared_mutex::shared_awaitable::await_ready() noexcept { - return m_mtx->try_lock_shared(); -} - - -locked_mutex_shared shared_mutex::shared_awaitable::await_resume() noexcept { - return { m_mtx }; -} - - -void shared_mutex::shared_awaitable::on_ready() noexcept { - assert(m_enclosing); - m_enclosing->resume(); -} - - -bool shared_mutex::shared_awaitable::is_shared() const noexcept { - return true; +locked_mutex_shared shared_mutex::shared_awaitable::await_resume() const noexcept { + assert(m_owner); + return { m_owner }; } shared_mutex::~shared_mutex() { std::lock_guard lk(m_spinlock); - if (m_locked != 0) { + // Mutex must be released before destroying. + if (!m_queue.empty()) { std::terminate(); } } bool shared_mutex::try_lock() noexcept { std::lock_guard lk(m_spinlock); - if (m_locked == 0) { - --m_locked; + if (m_queue.empty()) { + m_queue.push_back(&m_exclusive_sentinel); return true; } return false; @@ -66,82 +49,118 @@ bool shared_mutex::try_lock() noexcept { bool shared_mutex::try_lock_shared() noexcept { std::lock_guard lk(m_spinlock); - if (m_locked >= 0 && m_unique_waiting == 0) { - ++m_locked; + if (m_queue.empty()) { + m_queue.push_back(&m_shared_sentinel); + ++m_shared_count; + return true; + } + if (m_queue.back() == &m_shared_sentinel) { + ++m_shared_count; return true; } return false; } -shared_mutex::awaitable shared_mutex::unique() noexcept { - return { this }; +shared_mutex::exclusive_awaitable shared_mutex::exclusive() noexcept { + return exclusive_awaitable{ this }; } shared_mutex::shared_awaitable shared_mutex::shared() noexcept { - return { this }; + return shared_awaitable{ this }; } -bool shared_mutex::lock_enqueue(awaitable* waiting) { +bool shared_mutex::add_awaiting(basic_awaitable* waiting) { std::lock_guard lk(m_spinlock); - if (m_locked == 0) { - --m_locked; - return true; + const auto previous = m_queue.push_back(waiting); + if (waiting->m_type == awaitable_type::exclusive) { + // We've just acquire the exclusive lock. + if (previous == nullptr) { + m_queue.push_back(&m_exclusive_sentinel); + m_queue.pop_front(); + return true; + } + } + else if (waiting->m_type == awaitable_type::shared) { + // We've just acquire the exclusive lock. + if (previous == nullptr) { + m_queue.push_back(&m_shared_sentinel); + m_queue.pop_front(); + ++m_shared_count; + return true; + } + // We've just acquired the shared lock. + if (previous == &m_shared_sentinel) { + m_queue.pop_front(); // Pop old sentinel. + m_queue.pop_front(); // Pop just added awaitable. + m_queue.push_back(&m_shared_sentinel); + ++m_shared_count; + return true; + } + } + else { + assert(false && "improperly initialized awaiter"); } - m_queue.push(waiting); - ++m_unique_waiting; return false; } -bool shared_mutex::lock_enqueue_shared(shared_awaitable* waiting) { - std::lock_guard lk(m_spinlock); - if (m_locked >= 0 && m_unique_waiting == 0) { - ++m_locked; - return true; +void shared_mutex::continue_waiting(std::unique_lock& lk) { + decltype(m_queue) unblocked; + + const auto type = m_queue.front() ? m_queue.front()->m_type : awaitable_type::unknown; + if (type == awaitable_type::exclusive) { + assert(!m_queue.empty()); // Must not be empty as the front is exclusive. + unblocked.push_back(m_queue.pop_front()); + } + else if (type == awaitable_type::shared) { + while (m_queue.front() && m_queue.front()->m_type == awaitable_type::shared) { + unblocked.push_back(m_queue.pop_front()); + } + } + + lk.unlock(); + while (const auto waiting = unblocked.pop_front()) { + assert(waiting->m_enclosing); + waiting->m_enclosing->resume(); } - m_queue.push(waiting); - return false; } void shared_mutex::unlock() { std::unique_lock lk(m_spinlock); - assert(m_locked == -1); - ++m_locked; - queue_t next_list; - basic_awaitable* next; - do { - next = m_queue.pop(); - if (next) { - m_locked += next->is_shared() ? +1 : -1; - m_unique_waiting -= intptr_t(!next->is_shared()); - next_list.push(next); - } - } while (next && next->is_shared() && !m_queue.empty() && m_queue.back()->is_shared() && m_locked >= 0); - lk.unlock(); - while ((next = next_list.pop()) != nullptr) { - next->on_ready(); - } + assert(m_queue.front() == &m_exclusive_sentinel); + m_queue.pop_front(); + continue_waiting(lk); } void shared_mutex::unlock_shared() { std::unique_lock lk(m_spinlock); - assert(m_locked > 0); - --m_locked; - if (m_locked == 0) { - const auto next = m_queue.pop(); - if (next) { - assert(!next->is_shared()); // Shared ones would have been continued immediately. - --m_locked; - --m_unique_waiting; - lk.unlock(); - next->on_ready(); - } + assert(m_queue.front() == &m_shared_sentinel); + if (--m_shared_count == 0) { + m_queue.pop_front(); + continue_waiting(lk); } } + +void shared_mutex::_debug_clear() noexcept { + m_queue.~deque(); + new (&m_queue) decltype(m_queue); + m_shared_count = 0; +} + + +bool shared_mutex::_debug_is_exclusive_locked() const noexcept { + return m_queue.front() == &m_exclusive_sentinel; +} + + +size_t shared_mutex::_debug_is_shared_locked() const noexcept { + return m_queue.front() == &m_shared_sentinel ? m_shared_count : 0; +} + } // namespace asyncpp diff --git a/src/sleep.cpp b/src/sleep.cpp index 1183423..ad647b1 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -55,7 +55,7 @@ namespace impl_sleep { if (next->get_time() <= clock_type::now()) { m_queue.pop(); lk.unlock(); - next->on_ready(); + next->m_enclosing->resume(); } } } @@ -83,10 +83,6 @@ namespace impl_sleep { return; } - void awaitable::on_ready() noexcept { - m_enclosing->resume(); - } - void awaitable::enqueue() noexcept { sleep_scheduler::get().enqueue(this); } diff --git a/src/testing/interleaver.cpp b/src/testing/interleaver.cpp new file mode 100644 index 0000000..4982372 --- /dev/null +++ b/src/testing/interleaver.cpp @@ -0,0 +1,336 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace asyncpp::testing { + + +constinit const thread_state thread_state::running = thread_state{ code::running }; +constinit const thread_state thread_state::blocked = thread_state{ code::blocked }; +constinit const thread_state thread_state::completed = thread_state{ code::completed }; + +static_assert(std::atomic::is_always_lock_free); + +thread_local thread* thread::current_thread = nullptr; + + +namespace impl_suspension { + void wait(suspension_point& sp) { + thread::suspend_this_thread(sp); + } +} // namespace impl_suspension + + +void thread::resume() { + const auto current_state = m_content->state.load(); + const auto current_suspension_point = current_state.get_suspension_point(); + assert(current_suspension_point && "thread must be suspended at a suspension point"); + const auto next_state = current_suspension_point->acquire ? thread_state::blocked : thread_state::running; + m_content->state.store(next_state); +} + + +void thread::suspend_this_thread(const suspension_point& sp) { + if (current_thread) { + current_thread->suspend(sp); + } +} + + +thread_state thread::get_state() const { + return m_content->state.load(); +} + + +void thread::suspend(const suspension_point& sp) { + const auto prev_state = m_content->state.exchange(thread_state::suspended(sp)); + assert(prev_state == thread_state::running || prev_state == thread_state::blocked); + while (m_content->state.load().is_suspended()) { + // Wait. + } +} + + +tree::stable_node& tree::root() { + return *m_root; +} + + +tree::transition_node& tree::next(stable_node& node, const swarm_state& state) { + const auto it = node.swarm_states.find(state); + if (it != node.swarm_states.end()) { + return *it->second; + } + const auto successor = std::make_shared(std::map>{}, node.weak_from_this()); + return *node.swarm_states.insert_or_assign(state, successor).first->second; +} + + +tree::stable_node& tree::next(transition_node& node, int resumed) { + const auto it = node.successors.find(resumed); + if (it != node.successors.end()) { + return *it->second; + } + const auto successor = std::make_shared(std::map>{}, node.weak_from_this()); + return *node.successors.insert_or_assign(resumed, successor).first->second; +} + + +tree::transition_node& tree::previous(stable_node& node) { + const auto ptr = node.predecessor.lock(); + assert(ptr); + return *ptr; +} + + +tree::stable_node& tree::previous(transition_node& node) { + const auto ptr = node.predecessor.lock(); + assert(ptr); + return *ptr; +} + + +std::string dump(const swarm_state& state) { + std::stringstream ss; + for (const auto& th : state.thread_states) { + if (th == thread_state::completed) { + ss << "completed"; + } + else if (th == thread_state::blocked) { + ss << "blocked"; + } + else if (th == thread_state::running) { + ss << "running"; + } + else { + ss << th.get_suspension_point()->function << "::" << th.get_suspension_point()->name; + } + ss << "\n"; + } + return ss.str(); +} + + +std::string tree::dump() const { + std::stringstream ss; + + std::stack worklist; + worklist.push(m_root.get()); + + ss << "digraph G {\n"; + + while (!worklist.empty()) { + const auto node = worklist.top(); + worklist.pop(); + + size_t state_idx = 0; + for (const auto& [swarm, transition] : node->swarm_states) { + const auto node_name = std::format("_{}_{}", (void*)node, state_idx++); + const auto transition_name = std::format("_{}", (void*)transition.get()); + + auto state = asyncpp::testing::dump(swarm); + state = std::regex_replace(state, std::regex("\n"), "\\n"); + state = std::regex_replace(state, std::regex("\""), "\\\""); + ss << std::format("{} [label=\"{}\"];\n", node_name, state); + ss << node_name << " -> " << transition_name << ";\n"; + + std::stringstream complete; + for (const auto v : transition->completed) { + complete << v << " "; + } + ss << std::format("{} [label=\"complete: [{}]\"];\n", transition_name, complete.str()); + + if (const auto predecessor = node->predecessor.lock()) { + const auto resumed = std::ranges::find_if(predecessor->successors, [&](const auto& s) { return s.second.get() == node; })->first; + const auto predecessor_name = std::format("_{}", (void*)predecessor.get()); + ss << predecessor_name << " -> " << node_name << " [label=" << resumed << "];\n"; + } + + for (const auto& [resumed, next] : transition->successors) { + worklist.emplace(next.get()); + } + } + } + + ss << "}"; + + return ss.str(); +} + + +std::vector stabilize(std::span> threads) { + using namespace std::chrono_literals; + + std::vector states; + const auto start = std::chrono::high_resolution_clock::now(); + do { + const auto elapsed = std::chrono::high_resolution_clock::now() - start; + if (elapsed > 200ms) { + throw std::logic_error("deadlock - still running"); + } + states = get_states(threads); + } while (!is_stable(states)); + + while (!is_unblocked(states)) { + const auto elapsed = std::chrono::high_resolution_clock::now() - start; + if (elapsed > 200ms) { + throw std::logic_error("deadlock - all blocked"); + } + states = get_states(threads); + } + + return states; +} + + +std::vector get_states(std::span> threads) { + std::vector states; + std::ranges::transform(threads, std::back_inserter(states), [](const auto& th) { + return th->get_state(); + }); + return states; +} + + +bool is_stable(const std::vector& states) { + return std::ranges::all_of(states, [](const auto& state) { return state.is_stable(); }); +} + + +bool is_unblocked(const std::vector& states) { + const auto num_blocked = std::ranges::count_if(states, [](const auto& state) { return state == thread_state::blocked; }); + const auto num_suspended = std::ranges::count_if(states, [](const auto& state) { return state.is_suspended(); }); + if (num_blocked != 0) { + return num_suspended != 0; + } + return true; +} + + +int select_resumed(const swarm_state& state, const tree::transition_node& node, const std::vector>* hit_counts) { + std::vector all(state.thread_states.size()); + std::iota(all.begin(), all.end(), 0); + + auto viable = all | std::views::filter([&](int thread_idx) { return state.thread_states[thread_idx].is_suspended(); }); + auto undiscovered = viable | std::views::filter([&](int thread_idx) { return !node.successors.contains(thread_idx); }); + auto incomplete = viable | std::views::filter([&](int thread_idx) { return !node.completed.contains(thread_idx); }); + auto unrepeated = incomplete | std::views::filter([&](int thread_idx) { + if (!hit_counts) { + return true; + } + return !(*hit_counts)[thread_idx].contains(state.thread_states[thread_idx].get_suspension_point()) + || (*hit_counts)[thread_idx].at(state.thread_states[thread_idx].get_suspension_point()) < 3; + }); + + if (!undiscovered.empty()) { + return undiscovered.front(); + } + if (!incomplete.empty()) { + return incomplete.front(); + } + if (!viable.empty()) { + return viable.front(); + } + throw std::logic_error("no thread can be resumed"); +} + + +bool is_transitively_complete(const tree& tree, const tree::stable_node& node) { + std::vector completed_paths; + for (const auto& [swarm, transition] : node.swarm_states) { + completed_paths.resize(swarm.thread_states.size(), false); + for (size_t resumed = 0; resumed < swarm.thread_states.size(); ++resumed) { + const auto& ts = swarm.thread_states[resumed]; + if (ts == thread_state::completed || ts == thread_state::blocked) { + completed_paths[resumed] = true; + } + } + for (const int resumed : transition->completed) { + completed_paths[resumed] = true; + } + } + return std::ranges::all_of(completed_paths, [](auto v) { return v; }); +} + + +void mark_complete(tree& tree, tree::stable_node& node) { + if (&node == &tree.root()) { + return; + } + auto& transition = tree.previous(node); + const auto resumed_it = std::ranges::find_if(transition.successors, [&](auto& s) { return s.second.get() == &node; }); + assert(resumed_it != transition.successors.end()); + const int resumed = resumed_it->first; + transition.completed.insert(resumed); + + auto& prev_node = tree.previous(transition); + if (is_transitively_complete(tree, prev_node)) { + return mark_complete(tree, prev_node); // Stack overflow risk: tail recursion should optimize out to loop. + } +} + + +path run_next_interleaving(tree& tree, std::span> swarm) { + std::vector> hit_counts(swarm.size()); + path path; + auto current_node = &tree.root(); + + do { + try { + const auto state = swarm_state(stabilize(swarm)); + path.steps.push_back({ state, -1 }); + if (std::ranges::all_of(state.thread_states, [](const auto& ts) { return ts == thread_state::completed; })) { + break; + } + for (size_t thread_idx = 0; thread_idx < swarm.size(); ++thread_idx) { + const auto& ts = state.thread_states[thread_idx]; + if (const auto sp = ts.get_suspension_point(); sp != nullptr) { + auto it = hit_counts[thread_idx].find(sp); + if (it == hit_counts[thread_idx].end()) { + it = hit_counts[thread_idx].insert_or_assign(sp, 0).first; + } + ++it->second; + } + } + auto& transition_node = tree.next(*current_node, state); + const auto resumed = select_resumed(state, transition_node, &hit_counts); + path.steps.back().second = resumed; + swarm[resumed]->resume(); + current_node = &tree.next(transition_node, resumed); + } + catch (std::exception&) { + const auto locked_states = get_states(swarm); + path.steps.push_back({ swarm_state(locked_states), -1 }); + std::cerr << path.dump() << std::endl; + std::terminate(); + } + } while (true); + + mark_complete(tree, *current_node); + return path; +} + + +std::string path::dump() const { + std::stringstream ss; + for (const auto& step : steps) { + ss << ::asyncpp::testing::dump(step.first) << std::endl; + ss << " -> " << step.second << std::endl; + } + return ss.str(); +} + + +void thread::initialize_this_thread() { + current_thread = this; +} + +} // namespace asyncpp::testing \ No newline at end of file diff --git a/src/thread_pool.cpp b/src/thread_pool.cpp index 9d4bf9c..59d07a4 100644 --- a/src/thread_pool.cpp +++ b/src/thread_pool.cpp @@ -1,110 +1,96 @@ +#include #include -#include -#include - namespace asyncpp { -thread_local std::shared_ptr thread_pool::local_worker = nullptr; -thread_local thread_pool* thread_pool::local_owner = nullptr; +thread_pool::thread_pool(size_t num_threads) + : m_workers(num_threads) { + for (auto& w : m_workers) { + w.thread = std::jthread([this, &w] { + local = &w; + execute(w, m_global_worklist, m_global_notification, m_global_mutex, m_terminate, m_num_waiting, m_workers); + }); + } +} -thread_pool::thread_pool(size_t num_threads) { - std::ranges::generate_n(std::back_inserter(m_workers), num_threads, [this] { - return std::make_shared(); - }); - std::ranges::transform(m_workers, std::back_inserter(m_os_threads), [this](const auto& w) { - return std::jthread([this, &w] { worker_function(w); }); - }); +thread_pool::~thread_pool() { + std::lock_guard lk(m_global_mutex); + m_terminate.test_and_set(); + m_global_notification.notify_all(); } -thread_pool::~thread_pool() noexcept { - std::ranges::for_each(m_workers, [](auto& w) { - w->stop(); - w->wake(); - }); +void thread_pool::schedule(schedulable_promise& promise) { + schedule(promise, m_global_worklist, m_global_notification, m_global_mutex, m_num_waiting, local); } -void thread_pool::schedule(schedulable_promise& promise) noexcept { - if (m_free_workers.empty() && local_owner == this) { - local_worker->add_work_item(promise); - } - else if (const auto free_worker = m_free_workers.pop()) { - free_worker->add_work_item(promise); - free_worker->wake(); +void thread_pool::schedule(schedulable_promise& item, + atomic_stack& global_worklist, + std::condition_variable& global_notification, + std::mutex& global_mutex, + std::atomic_size_t& num_waiting, + worker* local) { + if (local) { + const auto prev_item = INTERLEAVED(local->worklist.push(&item)); + if (prev_item != nullptr) { + if (num_waiting.load(std::memory_order_relaxed) > 0) { + global_notification.notify_one(); + } + } } else { - const auto& worker = m_workers[0]; - const bool has_work = worker->has_work(); - worker->add_work_item(promise); - if (has_work) { - worker->wake(); - } + std::unique_lock lk(global_mutex, std::defer_lock); + INTERLEAVED_ACQUIRE(lk.lock()); + INTERLEAVED(global_worklist.push(&item)); + INTERLEAVED(global_notification.notify_one()); } } -void thread_pool::worker_function(std::shared_ptr w) noexcept { - local_worker = w; - local_owner = this; +schedulable_promise* thread_pool::steal(std::span workers) { + for (auto& w : workers) { + if (const auto item = INTERLEAVED(w.worklist.pop())) { + return item; + } + } + return nullptr; +} - schedulable_promise* promise; - do { - promise = w->get_work_item(); - for (auto it = m_workers.begin(); it != m_workers.end() && !promise; ++it) { - promise = (*it)->get_work_item(); +void thread_pool::execute(worker& local, + atomic_stack& global_worklist, + std::condition_variable& global_notification, + std::mutex& global_mutex, + std::atomic_flag& terminate, + std::atomic_size_t& num_waiting, + std::span workers) { + do { + if (const auto item = INTERLEAVED(local.worklist.pop())) { + item->handle().resume(); + continue; } - - if (promise) { - promise->handle().resume(); + else if (const auto item = INTERLEAVED(global_worklist.pop())) { + local.worklist.push(item); + continue; + } + else if (const auto item = steal(workers)) { + local.worklist.push(item); + continue; } else { - m_free_workers.push(w.get()); - w->wait(); + std::unique_lock lk(global_mutex, std::defer_lock); + INTERLEAVED_ACQUIRE(lk.lock()); + if (!INTERLEAVED(terminate.test()) && INTERLEAVED(global_worklist.empty())) { + num_waiting.fetch_add(1, std::memory_order_relaxed); + INTERLEAVED_ACQUIRE(global_notification.wait(lk)); + num_waiting.fetch_sub(1, std::memory_order_relaxed); + } } - } while (promise || !w->is_stopped()); -} - - -void thread_pool::worker::add_work_item(schedulable_promise& promise) { - m_work_items.push(&promise); -} - - -schedulable_promise* thread_pool::worker::get_work_item() { - return m_work_items.pop(); -} - - -bool thread_pool::worker::has_work() const { - return !m_work_items.empty(); -} - - -bool thread_pool::worker::is_stopped() const { - return m_terminated.test(); -} - - -void thread_pool::worker::stop() { - m_terminated.test_and_set(); -} - - -void thread_pool::worker::wake() const { - std::lock_guard lk(m_wake_mutex); - m_wake_cv.notify_one(); -} - - -void thread_pool::worker::wait() const { - std::unique_lock lk(m_wake_mutex); - m_wake_cv.wait(lk, [this] { return has_work() || is_stopped(); }); + } while (!INTERLEAVED(terminate.test())); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 03ebe94..7e6b8c0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,20 +3,21 @@ add_executable(test) target_sources(test PRIVATE container/test_atomic_collection.cpp - container/test_atomic_queue.cpp + container/test_atomic_item.cpp container/test_atomic_stack.cpp - interleaving/test_runner.cpp + container/test_atomic_deque.cpp + memory/test_rc_ptr.cpp main.cpp test_generator.cpp test_join.cpp test_mutex.cpp - test_shared_mutex.cpp - test_shared_task.cpp + test_shared_mutex.cpp test_stream.cpp test_task.cpp test_thread_pool.cpp test_event.cpp test_sleep.cpp + testing/test_interleaver.cpp ) diff --git a/test/container/test_atomic_collection.cpp b/test/container/test_atomic_collection.cpp index d55738c..212a956 100644 --- a/test/container/test_atomic_collection.cpp +++ b/test/container/test_atomic_collection.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -14,17 +15,22 @@ struct collection_element { using collection_t = atomic_collection; -TEST_CASE("Atomic collection: empty on creation", "[Atomic collection]") { +TEST_CASE("Atomic collection: empty", "[Atomic collection]") { collection_t collection; REQUIRE(collection.empty()); + REQUIRE(collection.first() == nullptr); REQUIRE(!collection.closed()); } -TEST_CASE("Atomic collection: push once", "[Atomic collection]") { +TEST_CASE("Atomic collection: push", "[Atomic collection]") { collection_t collection; collection_element e1{ 1 }; + collection_element e2{ 2 }; REQUIRE(nullptr == collection.push(&e1)); + REQUIRE(&e1 == collection.first()); + REQUIRE(&e1 == collection.push(&e2)); + REQUIRE(&e2 == collection.first()); REQUIRE(!collection.empty()); REQUIRE(!collection.closed()); } @@ -34,15 +40,18 @@ TEST_CASE("Atomic collection: detach", "[Atomic collection]") { collection_t collection; collection_element e1{ 1 }; collection_element e2{ 2 }; - REQUIRE(nullptr == collection.push(&e1)); - REQUIRE(&e1 == collection.push(&e2)); + + collection.push(&e1); + collection.push(&e2); + const collection_element* detached = collection.detach(); REQUIRE(collection.empty()); + REQUIRE(collection.first() == nullptr); + REQUIRE(!collection.closed()); + REQUIRE(detached == &e2); REQUIRE(detached->next == &e1); REQUIRE(detached->next->next == nullptr); - REQUIRE(nullptr == collection.push(&e1)); - REQUIRE(!collection.empty()); } @@ -50,15 +59,124 @@ TEST_CASE("Atomic collection: close", "[Atomic collection]") { collection_t collection; collection_element e1{ 1 }; collection_element e2{ 2 }; - REQUIRE(nullptr == collection.push(&e1)); - REQUIRE(&e1 == collection.push(&e2)); + + collection.push(&e1); + collection.push(&e2); + const collection_element* detached = collection.close(); REQUIRE(collection.empty()); REQUIRE(collection.closed()); + REQUIRE(detached == &e2); REQUIRE(detached->next == &e1); REQUIRE(detached->next->next == nullptr); - REQUIRE(collection.closed(collection.push(&e1))); - REQUIRE(collection.empty()); +} + + +TEST_CASE("Atomic collection: push to closed", "[Atomic collection]") { + collection_t collection; + collection_element e1{ 1 }; + collection.close(); + REQUIRE(collection_t::closed(collection.push(&e1))); REQUIRE(collection.closed()); +} + + +TEST_CASE("Atomic collection: push-push interleave", "[Atomic collection]") { + struct scenario : testing::validated_scenario { + collection_t collection; + collection_element e1{ 1 }; + collection_element e2{ 2 }; + + void thread_1() { + collection.push(&e1); + } + + void thread_2() { + collection.push(&e2); + } + + void validate(const testing::path& p) override { + INFO(p.dump()); + size_t size = 0; + collection_element* first = collection.first(); + bool e1_found = false; + bool e2_found = false; + while (first) { + if (first == &e1) { + e1_found = true; + } + if (first == &e2) { + e2_found = true; + } + ++size; + first = first->next; + } + REQUIRE(size == 2); + REQUIRE(e1_found); + REQUIRE(e2_found); + } + }; + + INTERLEAVED_RUN(scenario, THREAD("t1", &scenario::thread_1), THREAD("t2", &scenario::thread_2)); +} + + +TEST_CASE("Atomic collection: push-detach interleave", "[Atomic collection]") { + struct scenario : testing::validated_scenario { + collection_t collection; + collection_element e1{ 1 }; + collection_element* volatile detached = nullptr; + + void thread_1() { + collection.push(&e1); + } + + void thread_2() { + detached = collection.detach(); + } + + void validate(const testing::path& p) override { + INFO(p.dump()); + if (detached == nullptr) { + REQUIRE(collection.first() == &e1); + } + else { + REQUIRE(collection.empty()); + REQUIRE(detached == &e1); + } + } + }; + + INTERLEAVED_RUN(scenario, THREAD("t1", &scenario::thread_1), THREAD("t2", &scenario::thread_2)); +} + + +TEST_CASE("Atomic collection: push-close interleave", "[Atomic collection]") { + struct scenario : testing::validated_scenario { + collection_t collection; + collection_element e1{ 1 }; + collection_element* volatile closed = nullptr; + + void thread_1() { + collection.push(&e1); + } + + void thread_2() { + closed = collection.close(); + } + + void validate(const testing::path& p) override { + INFO(p.dump()); + if (closed == nullptr) { + REQUIRE(collection.closed()); + } + else { + REQUIRE(collection.closed()); + REQUIRE(closed == &e1); + } + } + }; + + INTERLEAVED_RUN(scenario, THREAD("t1", &scenario::thread_1), THREAD("t2", &scenario::thread_2)); } \ No newline at end of file diff --git a/test/container/test_atomic_deque.cpp b/test/container/test_atomic_deque.cpp new file mode 100644 index 0000000..22ce61d --- /dev/null +++ b/test/container/test_atomic_deque.cpp @@ -0,0 +1,107 @@ +#include + +#include + + +using namespace asyncpp; + + +struct element { + element* next; + element* prev; +}; + + +using deque_t = atomic_deque; + + +TEST_CASE("Atomic deque - empty", "[Atomic deque]") { + deque_t c; + REQUIRE(c.front() == nullptr); + REQUIRE(c.back() == nullptr); + REQUIRE(c.empty()); +} + + +TEST_CASE("Atomic deque - push_front", "[Atomic deque]") { + deque_t c; + element e1, e2; + + SECTION("single item") { + c.push_front(&e1); + REQUIRE(c.front() == &e1); + REQUIRE(c.back() == &e1); + } + SECTION("multiple items") { + c.push_front(&e1); + c.push_front(&e2); + REQUIRE(c.front() == &e2); + REQUIRE(c.back() == &e1); + } +} + + +TEST_CASE("Atomic deque - push_back", "[Atomic deque]") { + deque_t c; + element e1, e2; + + SECTION("single item") { + c.push_back(&e1); + REQUIRE(c.front() == &e1); + REQUIRE(c.back() == &e1); + } + SECTION("multiple items") { + c.push_back(&e1); + c.push_back(&e2); + REQUIRE(c.front() == &e1); + REQUIRE(c.back() == &e2); + } +} + + +TEST_CASE("Atomic deque - pop_front", "[Atomic deque]") { + deque_t c; + element e1, e2, e3; + + c.push_front(&e1); + c.push_front(&e2); + c.push_front(&e3); + + REQUIRE(c.pop_front() == &e3); + REQUIRE(c.front() == &e2); + REQUIRE(c.back() == &e1); + + REQUIRE(c.pop_front() == &e2); + REQUIRE(c.front() == &e1); + REQUIRE(c.back() == &e1); + + REQUIRE(c.pop_front() == &e1); + REQUIRE(c.front() == nullptr); + REQUIRE(c.back() == nullptr); + + REQUIRE(c.pop_front() == nullptr); +} + + +TEST_CASE("Atomic deque - pop_back", "[Atomic deque]") { + deque_t c; + element e1, e2, e3; + + c.push_back(&e1); + c.push_back(&e2); + c.push_back(&e3); + + REQUIRE(c.pop_back() == &e3); + REQUIRE(c.back() == &e2); + REQUIRE(c.front() == &e1); + + REQUIRE(c.pop_back() == &e2); + REQUIRE(c.back() == &e1); + REQUIRE(c.front() == &e1); + + REQUIRE(c.pop_back() == &e1); + REQUIRE(c.front() == nullptr); + REQUIRE(c.back() == nullptr); + + REQUIRE(c.pop_back() == nullptr); +} diff --git a/test/container/test_atomic_item.cpp b/test/container/test_atomic_item.cpp new file mode 100644 index 0000000..5fcdd1a --- /dev/null +++ b/test/container/test_atomic_item.cpp @@ -0,0 +1,117 @@ +#include +#include + +#include + + +using namespace asyncpp; + + +struct element { + int id = 0; +}; + +using item_t = atomic_item; + + +TEST_CASE("Atomic item: empty", "[Atomic item]") { + item_t item; + REQUIRE(item.empty()); + REQUIRE(item.item() == nullptr); + REQUIRE(!item.closed()); +} + + +TEST_CASE("Atomic item: set", "[Atomic item]") { + item_t item; + element e{ 1 }; + item.set(&e); + REQUIRE(item.item() == &e); + REQUIRE(!item.empty()); + REQUIRE(!item.closed()); +} + + +TEST_CASE("Atomic item: set twice", "[Atomic item]") { + item_t item; + element e1{ 1 }; + element e2{ 2 }; + item.set(&e1); + REQUIRE(&e1 == item.set(&e2)); + REQUIRE(item.item() == &e1); + REQUIRE(!item.empty()); + REQUIRE(!item.closed()); +} + + +TEST_CASE("Atomic item: set closed", "[Atomic item]") { + item_t item; + item.close(); + element e1{ 1 }; + REQUIRE(item_t::closed(item.set(&e1))); + REQUIRE(item.closed()); +} + + +TEST_CASE("Atomic item: close", "[Atomic item]") { + item_t item; + element e1{ 1 }; + item.set(&e1); + REQUIRE(item.close() == &e1); + REQUIRE(item.empty()); + REQUIRE(item.closed()); +} + + +TEST_CASE("Atomic item: push-push interleave", "[Atomic item]") { + struct scenario : testing::validated_scenario { + item_t item; + element e1{ 1 }; + element e2{ 2 }; + + void thread_1() { + item.set(&e1); + } + + void thread_2() { + item.set(&e2); + } + + void validate(const testing::path& p) override { + INFO(p.dump()); + REQUIRE((item.item() == &e1 || item.item() == &e2)); + } + }; + + INTERLEAVED_RUN(scenario, THREAD("t1", &scenario::thread_1), THREAD("t2", &scenario::thread_2)); +} + + +TEST_CASE("Atomic item: push-close interleave", "[Atomic item]") { + struct scenario : testing::validated_scenario { + item_t item; + element e{ 1 }; + element* volatile closed = nullptr; + + void thread_1() { + item.set(&e); + } + + void thread_2() { + closed = item.close(); + } + + void validate(const testing::path& p) override { + INFO(p.dump()); + if (closed == nullptr) { + REQUIRE(item.closed()); + } + else { + REQUIRE(item.closed()); + REQUIRE(closed == &e); + } + } + }; + + INTERLEAVED_RUN(scenario, THREAD("t1", &scenario::thread_1), THREAD("t2", &scenario::thread_2)); +} \ No newline at end of file diff --git a/test/container/test_atomic_queue.cpp b/test/container/test_atomic_queue.cpp deleted file mode 100644 index 3a822d2..0000000 --- a/test/container/test_atomic_queue.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include - -#include - - -using namespace asyncpp; - - -struct queue_element { - int id = 0; - queue_element* next = nullptr; - queue_element* prev = nullptr; -}; - -using queue_t = atomic_queue; - - -TEST_CASE("Atomic queue: all", "[Atomic queue]") { - queue_element e0{ .id = 0 }; - queue_element e1{ .id = 1 }; - queue_element e2{ .id = 2 }; - queue_element e3{ .id = 3 }; - queue_t queue; - REQUIRE(queue.empty()); - REQUIRE(queue.pop() == nullptr); - REQUIRE(queue.push(&e0) == nullptr); - REQUIRE(queue.push(&e1) == &e0); - REQUIRE(queue.push(&e2) == &e1); - REQUIRE(queue.push(&e3) == &e2); - REQUIRE(!queue.empty()); - REQUIRE(queue.pop() == &e0); - REQUIRE(queue.pop() == &e1); - REQUIRE(queue.pop() == &e2); - REQUIRE(queue.pop() == &e3); - REQUIRE(queue.empty()); - REQUIRE(queue.pop() == nullptr); -} \ No newline at end of file diff --git a/test/container/test_atomic_stack.cpp b/test/container/test_atomic_stack.cpp index fe954fa..16067c8 100644 --- a/test/container/test_atomic_stack.cpp +++ b/test/container/test_atomic_stack.cpp @@ -6,31 +6,41 @@ using namespace asyncpp; -struct queue_element { - int id = 0; - queue_element* next = nullptr; +struct element { + element* next; + element* prev; }; -using queue_t = atomic_stack; - - -TEST_CASE("Atomic stack: all", "[Atomic stack]") { - queue_element e0{ .id = 0 }; - queue_element e1{ .id = 1 }; - queue_element e2{ .id = 2 }; - queue_element e3{ .id = 3 }; - queue_t stack; - REQUIRE(stack.empty()); - REQUIRE(stack.pop() == nullptr); - REQUIRE(stack.push(&e0) == nullptr); - REQUIRE(stack.push(&e1) == &e0); - REQUIRE(stack.push(&e2) == &e1); - REQUIRE(stack.push(&e3) == &e2); - REQUIRE(!stack.empty()); - REQUIRE(stack.pop() == &e3); - REQUIRE(stack.pop() == &e2); - REQUIRE(stack.pop() == &e1); - REQUIRE(stack.pop() == &e0); - REQUIRE(stack.empty()); - REQUIRE(stack.pop() == nullptr); + +using stack_t = atomic_stack; + + +TEST_CASE("Atomic stack: empty", "[Atomic stack]") { + stack_t c; + REQUIRE(c.top() == nullptr); + REQUIRE(c.empty()); +} + + +TEST_CASE("Atomic stack - push", "[Atomic stack]") { + stack_t c; + element e1, e2; + + c.push(&e1); + REQUIRE(c.top() == &e1); + c.push(&e2); + REQUIRE(c.top() == &e2); +} + + +TEST_CASE("Atomic stack - pop", "[Atomic stack]") { + stack_t c; + element e1, e2; + + c.push(&e1); + c.push(&e2); + + REQUIRE(c.pop() == &e2); + REQUIRE(c.pop() == &e1); + REQUIRE(c.pop() == nullptr); } \ No newline at end of file diff --git a/test/helper_interleaving.hpp b/test/helper_interleaving.hpp deleted file mode 100644 index 93c6f89..0000000 --- a/test/helper_interleaving.hpp +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once - -#include "helper_schedulers.hpp" - -#include -#include - -#include - -#include - -using namespace asyncpp; - - -template -auto run_dependent_tasks(MainTask main_task, - SubTask sub_task, - std::tuple main_args, - std::tuple sub_args) { - using sub_result_t = std::invoke_result_t; - using main_result_t = std::invoke_result_t; - struct fixture { - thread_locked_scheduler main_sched; - thread_locked_scheduler sub_sched; - main_result_t main_result; - }; - - auto create_fixture = [=, main_args = std::move(main_args), sub_args = std::move(sub_args)] { - auto fixture_ = std::make_shared(); - auto sub_result = launch(std::apply(sub_task, sub_args), fixture_->sub_sched); - auto all_main_args = std::tuple_cat(std::forward_as_tuple(std::move(sub_result)), main_args); - fixture_->main_result = launch(std::apply(main_task, std::move(all_main_args)), fixture_->main_sched); - return fixture_; - }; - auto sub_thread = [](std::shared_ptr fixture_) { - fixture_->sub_sched.resume(); - }; - auto main_thread = [](std::shared_ptr fixture_) { - fixture_->main_sched.resume(); - if (!fixture_->main_result.ready()) { - INTERLEAVED_ACQUIRE(fixture_->main_sched.wait()); - fixture_->main_sched.resume(); - } - join(fixture_->main_result); - }; - - return [create_fixture = std::move(create_fixture), sub_thread = std::move(sub_thread), main_thread = std::move(main_thread)] { - return interleaving::run_all(std::function(create_fixture), - std::vector{ std::function(main_thread), std::function(sub_thread) }, - { "$main", "$sub" }); - }; -} - - -template -auto run_abandoned_task(MainTask main_task, - std::tuple main_args) { - using main_result_t = std::invoke_result_t; - struct fixture { - thread_locked_scheduler main_sched; - main_result_t main_result; - }; - - auto create_fixture = [main_task, main_args = std::move(main_args)] { - auto fixture_ = std::make_shared(); - fixture_->main_result = launch(std::apply(main_task, std::move(main_args)), fixture_->main_sched); - return fixture_; - }; - auto exec_thread = [](std::shared_ptr fixture_) { - fixture_->main_sched.resume(); - }; - auto abandon_thread = [](std::shared_ptr fixture_) { - fixture_->main_result = {}; - }; - - return [create_fixture = std::move(create_fixture), abandon_thread = std::move(abandon_thread), exec_thread = std::move(exec_thread)] { - return interleaving::run_all(std::function(create_fixture), - std::vector{ std::function(abandon_thread), std::function(exec_thread) }, - { "$abandon", "$exec" }); - }; -} - - -template InterleavingGenFunc> -void evaluate_interleavings(InterleavingGenFunc interleaving_gen_func) { - size_t count = 0; - auto before = impl::leak_checked_promise::snapshot(); - for (const auto& interleaving : interleaving_gen_func()) { - INFO(count << "\n" - << (interleaving::interleaving_printer{ interleaving, true })); - REQUIRE(impl::leak_checked_promise::check(before)); - auto before = impl::leak_checked_promise::snapshot(); - ++count; - } - REQUIRE(count >= 3); -} - - -template InterleavingGen> -void evaluate_interleavings(InterleavingGen interleaving_gen) { - size_t count = 0; - for (const auto& interleaving : interleaving_gen) { - INFO(count << "\n" - << (interleaving::interleaving_printer{ interleaving, true })); - ++count; - } - REQUIRE(count >= 3); -} \ No newline at end of file diff --git a/test/helper_schedulers.hpp b/test/helper_schedulers.hpp index 559b38e..58a2238 100644 --- a/test/helper_schedulers.hpp +++ b/test/helper_schedulers.hpp @@ -30,16 +30,10 @@ class thread_locked_scheduler : public asyncpp::scheduler { }; -class thread_queued_scheduler : public asyncpp::scheduler { -public: +struct collecting_scheduler : asyncpp::scheduler { void schedule(asyncpp::schedulable_promise& promise) override { - m_items.push(&promise); - } - - asyncpp::schedulable_promise* get() { - return m_items.pop(); + this->promise = &promise; } -private: - asyncpp::atomic_stack m_items; -}; + asyncpp::schedulable_promise* promise; +}; \ No newline at end of file diff --git a/test/interleaving/test_runner.cpp b/test/interleaving/test_runner.cpp deleted file mode 100644 index 5e518f0..0000000 --- a/test/interleaving/test_runner.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include -#include - -#include - -#include - - -using namespace asyncpp; - - -TEST_CASE("Sequence point: only initial points", "[Sequence point]") { - const auto func1 = [] {}; - const auto func2 = [] {}; - size_t count = 0; - for (auto interleaving_ : interleaving::run_all({ func1, func2 })) { - ++count; - } - REQUIRE(count == 2); -} - - -void test_func_1() { - SEQUENCE_POINT("a"); - SEQUENCE_POINT("b"); - SEQUENCE_POINT("c"); -} - - -void test_func_2() { - SEQUENCE_POINT("d"); -} - - -TEST_CASE("Sequence point: linear multiple points", "[Sequence point]") { - size_t count = 0; - for (auto interleaving_ : interleaving::run_all({ &test_func_1, &test_func_2 })) { - ++count; - } - REQUIRE(count == 15); -} - - -void test_func_branch_1(std::shared_ptr cond) { - if (cond->load()) { - SEQUENCE_POINT("b_true"); - } - else { - SEQUENCE_POINT("b_false"); - } -} - - -void test_func_branch_2(std::shared_ptr cond) { - cond->store(true); -} - - -TEST_CASE("Sequence point: branching", "[Sequence point]") { - auto fixture = std::function([] { return std::make_shared(false); }); - size_t count = 0; - for (auto interleaving_ : interleaving::run_all(fixture, std::vector{ std::function(test_func_branch_1), std::function(test_func_branch_2) })) { - ++count; - } - REQUIRE(count == 3); -} diff --git a/test/memory/test_rc_ptr.cpp b/test/memory/test_rc_ptr.cpp new file mode 100644 index 0000000..a3de534 --- /dev/null +++ b/test/memory/test_rc_ptr.cpp @@ -0,0 +1,164 @@ +#include + +#include + + +using namespace asyncpp; + + +struct managed : rc_from_this { + void destroy() { + ++destroyed; + } + size_t destroyed = 0; +}; + + +TEST_CASE("Refcounted pointer - empty", "[Refcounted pointer]") { + rc_ptr ptr; + REQUIRE(!ptr); + REQUIRE(ptr.use_count() == 0); + REQUIRE(ptr.unique() == false); + REQUIRE(ptr.get() == nullptr); +} + + +TEST_CASE("Refcounted pointer - unique", "[Refcounted pointer]") { + managed object; + { + rc_ptr ptr(&object); + REQUIRE(ptr); + REQUIRE(ptr.use_count() == 1); + REQUIRE(ptr.unique() == true); + REQUIRE(ptr.get() == &object); + } + REQUIRE(object.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - multiple", "[Refcounted pointer]") { + managed object; + { + rc_ptr ptr1(&object); + rc_ptr ptr2(&object); + + REQUIRE(ptr1.use_count() == 2); + REQUIRE(ptr1.unique() == false); + REQUIRE(ptr1.get() == &object); + + REQUIRE(ptr2.use_count() == 2); + REQUIRE(ptr2.unique() == false); + REQUIRE(ptr2.get() == &object); + + REQUIRE(ptr1 == ptr2); + } + REQUIRE(object.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - move construct", "[Refcounted pointer]") { + managed object; + { + rc_ptr ptr(&object); + rc_ptr copy(std::move(ptr)); + + REQUIRE(copy); + REQUIRE(copy.use_count() == 1); + REQUIRE(copy.unique() == true); + REQUIRE(copy.get() == &object); + } + REQUIRE(object.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - copy construct", "[Refcounted pointer]") { + managed object; + { + rc_ptr ptr(&object); + rc_ptr copy(ptr); + + REQUIRE(copy); + REQUIRE(copy.use_count() == 2); + REQUIRE(copy.unique() == false); + REQUIRE(copy.get() == &object); + } + REQUIRE(object.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - move assign", "[Refcounted pointer]") { + managed object; + { + rc_ptr ptr(&object); + rc_ptr copy; + copy = std::move(ptr); + + REQUIRE(copy); + REQUIRE(copy.use_count() == 1); + REQUIRE(copy.unique() == true); + REQUIRE(copy.get() == &object); + } + REQUIRE(object.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - copy assign", "[Refcounted pointer]") { + managed object; + { + rc_ptr ptr(&object); + rc_ptr copy; + copy = ptr; + + REQUIRE(copy); + REQUIRE(copy.use_count() == 2); + REQUIRE(copy.unique() == false); + REQUIRE(copy.get() == &object); + } + REQUIRE(object.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - move assign overwrite", "[Refcounted pointer]") { + managed object_prev; + managed object_over; + { + rc_ptr prev(&object_prev); + rc_ptr over(&object_over); + prev = std::move(over); + + REQUIRE(prev); + REQUIRE(!over); + REQUIRE(prev.use_count() == 1); + REQUIRE(object_prev.destroyed == 1); + REQUIRE(object_over.destroyed == 0); + } + REQUIRE(object_prev.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - copy assign overwrite", "[Refcounted pointer]") { + managed object_prev; + managed object_over; + { + rc_ptr prev(&object_prev); + rc_ptr over(&object_over); + prev = over; + + REQUIRE(prev); + REQUIRE(over); + REQUIRE(prev.use_count() == 2); + REQUIRE(object_prev.destroyed == 1); + REQUIRE(object_over.destroyed == 0); + } + REQUIRE(object_prev.destroyed == 1); +} + + +TEST_CASE("Refcounted pointer - dereference", "[Refcounted pointer]") { + managed object; + object.destroyed = 10; + rc_ptr ptr(&object); + REQUIRE(ptr->destroyed == 10); + REQUIRE((*ptr).destroyed == 10); + REQUIRE(ptr.get()->destroyed == 10); +} \ No newline at end of file diff --git a/test/monitor_task.hpp b/test/monitor_task.hpp new file mode 100644 index 0000000..b134aec --- /dev/null +++ b/test/monitor_task.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + + +class [[nodiscard]] monitor_task { + struct counters { + std::atomic_size_t suspensions; + std::atomic_bool done; + std::exception_ptr exception; + }; + + struct promise : asyncpp::resumable_promise, asyncpp::schedulable_promise, asyncpp::rc_from_this { + monitor_task get_return_object() noexcept { + return monitor_task{ asyncpp::rc_ptr(this) }; + } + + constexpr std::suspend_never initial_suspend() const noexcept { + return {}; + } + + std::suspend_always final_suspend() noexcept { + m_counters->done.store(true); + return {}; + } + + constexpr void return_void() const noexcept {} + + void unhandled_exception() noexcept { + m_counters->exception = std::current_exception(); + } + + void resume() override { + m_counters->suspensions.fetch_add(1); + m_scheduler ? m_scheduler->schedule(*this) : handle().resume(); + } + + std::coroutine_handle<> handle() override { + return std::coroutine_handle::from_promise(*this); + } + + void destroy() noexcept { + handle().destroy(); + } + + const counters& get_counters() const { + return *m_counters; + } + + private: + std::shared_ptr m_counters = std::make_shared(); + }; + +public: + using promise_type = promise; + + monitor_task() = default; + monitor_task(asyncpp::rc_ptr promise) : m_promise(std::move(promise)) {} + monitor_task(monitor_task&&) = default; + monitor_task(const monitor_task&) = delete; + monitor_task& operator=(monitor_task&&) = default; + monitor_task& operator=(const monitor_task&) = delete; + + promise_type& promise() { + assert(m_promise); + return *m_promise; + } + + std::coroutine_handle handle() { + assert(m_promise); + return std::coroutine_handle::from_promise(*m_promise); + } + + const counters& get_counters() const { + return m_promise->get_counters(); + } + +private: + asyncpp::rc_ptr m_promise; +}; \ No newline at end of file diff --git a/test/test_event.cpp b/test/test_event.cpp index 9c9b76e..3a41f88 100644 --- a/test/test_event.cpp +++ b/test/test_event.cpp @@ -1,38 +1,210 @@ -#include "helper_interleaving.hpp" +#include "helper_schedulers.hpp" +#include "monitor_task.hpp" #include -#include #include +#include -#include - +#include #include using namespace asyncpp; -TEST_CASE("Event: interleave co_await | set", "[Event]") { - struct fixture { +TEMPLATE_TEST_CASE("Event: set value", "[Event]", event, broadcast_event) { + TestType evt; + REQUIRE(evt._debug_get_result().has_value() == false); + evt.set_value(1); + REQUIRE(evt._debug_get_result().get_or_throw() == 1); +} + + +TEMPLATE_TEST_CASE("Event: set error", "[Event]", event, broadcast_event) { + TestType evt; + REQUIRE(evt._debug_get_result().has_value() == false); + evt.set_exception(std::make_exception_ptr(std::runtime_error("test"))); + REQUIRE_THROWS_AS(evt._debug_get_result().get_or_throw(), std::runtime_error); +} + + +TEMPLATE_TEST_CASE("Event: set twice", "[Event]", event, broadcast_event) { + TestType evt; + REQUIRE(evt._debug_get_result().has_value() == false); + evt.set_value(1); + REQUIRE_THROWS(evt.set_value(1)); + REQUIRE(evt._debug_get_result().get_or_throw() == 1); +} + + +template +monitor_task monitor_coro(Event& evt) { + co_await evt; +} + + +TEMPLATE_TEST_CASE("Event: await before set", "[Event]", event, broadcast_event) { + TestType evt; + + SECTION("value") { + auto monitor = monitor_coro(evt); + REQUIRE(monitor.get_counters().done == false); + evt.set_value(1); + REQUIRE(monitor.get_counters().suspensions == 1); + REQUIRE(monitor.get_counters().done == true); + REQUIRE(monitor.get_counters().exception == nullptr); + } + SECTION("exception") { + auto monitor = monitor_coro(evt); + REQUIRE(monitor.get_counters().done == false); + evt.set_exception(std::make_exception_ptr(std::runtime_error("test"))); + REQUIRE(monitor.get_counters().suspensions == 1); + REQUIRE(monitor.get_counters().done == true); + REQUIRE(monitor.get_counters().exception != nullptr); + } +} + + +TEMPLATE_TEST_CASE("Event: await after set", "[Event]", event, broadcast_event) { + TestType evt; + + SECTION("value") { + evt.set_value(1); + auto monitor = monitor_coro(evt); + REQUIRE(monitor.get_counters().suspensions == 0); + REQUIRE(monitor.get_counters().done == true); + REQUIRE(monitor.get_counters().exception == nullptr); + } + SECTION("exception") { + evt.set_exception(std::make_exception_ptr(std::runtime_error("test"))); + auto monitor = monitor_coro(evt); + REQUIRE(monitor.get_counters().suspensions == 0); + REQUIRE(monitor.get_counters().done == true); + REQUIRE(monitor.get_counters().exception != nullptr); + } +} + + +TEST_CASE("Event: types", "[Event]") { + SECTION("value") { event evt; - }; + evt.set_value(1); + auto monitor = [&]() -> monitor_task { + const auto result = co_await evt; + REQUIRE(result == 1); + }(); + } + SECTION("reference") { + event evt; + int value = 1; + evt.set_value(value); + auto monitor = [&]() -> monitor_task { + const auto& result = co_await evt; + REQUIRE(&result == &value); + }(); + } + SECTION("void") { + event evt; + evt.set_value(); + auto monitor = [&]() -> monitor_task { + co_await evt; + }(); + } +} - auto make_fixture = [] { - return std::make_shared(); - }; - auto wait_thread = [](std::shared_ptr f) { - const auto coro = [f]() -> task { - co_return co_await f->evt; - }; - auto t = coro(); - t.launch(); - }; - auto set_thread = [](std::shared_ptr f) { - f->evt.set_value(3); - }; +TEST_CASE("Event: broadcast types", "[Event]") { + SECTION("value") { + broadcast_event evt; + evt.set_value(1); + auto monitor = [&]() -> monitor_task { + const auto result = co_await evt; + REQUIRE(result == 1); + }(); + } + SECTION("reference") { + broadcast_event evt; + int value = 1; + evt.set_value(value); + auto monitor = [&]() -> monitor_task { + const auto& result = co_await evt; + REQUIRE(&result == &value); + }(); + } + SECTION("void") { + broadcast_event evt; + evt.set_value(); + auto monitor = [&]() -> monitor_task { + co_await evt; + }(); + } +} + + +TEST_CASE("Event: multiple awaiters", "[Event]") { + event evt; - auto gen = interleaving::run_all(std::function(make_fixture), - std::vector{ std::function(wait_thread), std::function(set_thread) }, - { "$wait", "$set" }); - evaluate_interleavings(std::move(gen)); + auto mon1 = monitor_coro(evt); + auto mon2 = monitor_coro(evt); + evt.set_value(1); + + REQUIRE(mon1.get_counters().suspensions == 1); + REQUIRE(mon2.get_counters().suspensions == 0); + + REQUIRE(mon1.get_counters().done == true); + REQUIRE(mon2.get_counters().done == true); + + REQUIRE(mon1.get_counters().exception == nullptr); + REQUIRE(mon2.get_counters().exception != nullptr); } + + +TEST_CASE("Event: broadcast multiple awaiters", "[Event]") { + broadcast_event evt; + + auto mon1 = monitor_coro(evt); + auto mon2 = monitor_coro(evt); + evt.set_value(1); + auto mon3 = monitor_coro(evt); + auto mon4 = monitor_coro(evt); + + REQUIRE(mon1.get_counters().suspensions == 1); + REQUIRE(mon2.get_counters().suspensions == 1); + REQUIRE(mon3.get_counters().suspensions == 0); + REQUIRE(mon4.get_counters().suspensions == 0); + + REQUIRE(mon1.get_counters().done == true); + REQUIRE(mon2.get_counters().done == true); + REQUIRE(mon3.get_counters().done == true); + REQUIRE(mon4.get_counters().done == true); + + REQUIRE(mon1.get_counters().exception == nullptr); + REQUIRE(mon2.get_counters().exception == nullptr); + REQUIRE(mon3.get_counters().exception == nullptr); + REQUIRE(mon4.get_counters().exception == nullptr); +} + + +TEMPLATE_TEST_CASE("Event: await-set interleave", "[Event]", event, broadcast_event) { + struct scenario : testing::validated_scenario { + TestType evt; + monitor_task monitor; + int result = 0; + + void thread_1() { + evt.set_value(1); + } + + void thread_2() { + monitor = [](scenario& s) -> monitor_task { + s.result = co_await s.evt; + }(*this); + } + + void validate(const testing::path& p) override { + INFO(p.dump()); + REQUIRE(result == 1); + } + }; + + INTERLEAVED_RUN(scenario, THREAD("t1", &scenario::thread_1), THREAD("t2", &scenario::thread_2)); +} \ No newline at end of file diff --git a/test/test_mutex.cpp b/test/test_mutex.cpp index 0323782..0e4a3ef 100644 --- a/test/test_mutex.cpp +++ b/test/test_mutex.cpp @@ -1,139 +1,154 @@ -#include -#include +#include "monitor_task.hpp" + #include -#include #include using namespace asyncpp; +struct [[nodiscard]] scope_clear { + ~scope_clear() { + mtx._debug_clear(); + } + mutex& mtx; +}; + + +static monitor_task lock_exclusively(mutex& mtx) { + co_await mtx.exclusive(); +} + + +static monitor_task lock(unique_lock& lk) { + co_await lk; +} + + TEST_CASE("Mutex: try lock", "[Mutex]") { - static const auto coro = [](mutex& mtx) -> task { - REQUIRE(mtx.try_lock()); - REQUIRE(!mtx.try_lock()); - mtx.unlock(); - co_return; - }; + mutex mtx; + scope_clear guard(mtx); + + REQUIRE(mtx.try_lock()); + REQUIRE(mtx._debug_is_locked()); +} + +TEST_CASE("Mutex: lock direct immediate", "[Mutex]") { mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + auto monitor = lock_exclusively(mtx); + REQUIRE(monitor.get_counters().done); + REQUIRE(mtx._debug_is_locked()); } -TEST_CASE("Mutex: lock", "[Mutex]") { - static const auto coro = [](mutex& mtx) -> task { - co_await mtx; - REQUIRE(!mtx.try_lock()); - mtx.unlock(); - }; +TEST_CASE("Mutex: lock spurious immediate", "[Mutex]") { + mutex mtx; + scope_clear guard(mtx); + + auto monitor = []() -> monitor_task { co_return; }(); + auto awaiter = mtx.exclusive(); + REQUIRE(false == awaiter.await_suspend(monitor.handle())); + REQUIRE(mtx._debug_is_locked()); +} + +TEST_CASE("Mutex: sequencial locking attempts", "[Mutex]") { mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + REQUIRE(mtx.try_lock()); + REQUIRE(!mtx.try_lock()); } TEST_CASE("Mutex: unlock", "[Mutex]") { - static const auto coro = [](mutex& mtx) -> task { - co_await mtx; + mutex mtx; + scope_clear guard(mtx); + + SECTION("exclusive -> free") { + mtx.try_lock(); mtx.unlock(); - REQUIRE(mtx.try_lock()); + REQUIRE(!mtx._debug_is_locked()); + } + SECTION("locked -> exclusive") { + mtx.try_lock(); + + auto monitor1 = lock_exclusively(mtx); + auto monitor2 = lock_exclusively(mtx); + + REQUIRE(!monitor1.get_counters().done); + REQUIRE(!monitor2.get_counters().done); + mtx.unlock(); - }; - mutex mtx; - join(coro(mtx)); + REQUIRE(monitor1.get_counters().done); + REQUIRE(!monitor2.get_counters().done); + } } -TEST_CASE("Mutex: unique lock try", "[Mutex]") { - static const auto coro = [](mutex& mtx) -> task { - unique_lock lk(mtx); - REQUIRE(!lk.owns_lock()); - REQUIRE(lk.try_lock()); - REQUIRE(lk.owns_lock()); - co_return; - }; - +TEST_CASE("Mutex: unique lock try_lock", "[Mutex]") { mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + unique_lock lk(mtx); + REQUIRE(!lk.owns_lock()); + + lk.try_lock(); + REQUIRE(lk.owns_lock()); + REQUIRE(mtx._debug_is_locked()); } TEST_CASE("Mutex: unique lock await", "[Mutex]") { - static const auto coro = [](mutex& mtx) -> task { - unique_lock lk(mtx); - REQUIRE(!lk.owns_lock()); - co_await lk; - REQUIRE(lk.owns_lock()); - co_return; - }; - mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + unique_lock lk(mtx); + auto monitor = lock(lk); + REQUIRE(monitor.get_counters().done); + REQUIRE(lk.owns_lock()); + REQUIRE(mtx._debug_is_locked()); } TEST_CASE("Mutex: unique lock start locked", "[Mutex]") { - static const auto coro = [](mutex& mtx) -> task { - unique_lock lk(co_await mtx); + mutex mtx; + scope_clear guard(mtx); + + auto monitor = [](mutex& mtx) -> monitor_task { + unique_lock lk(co_await mtx.exclusive()); REQUIRE(lk.owns_lock()); - co_return; - }; + REQUIRE(mtx._debug_is_locked()); + }(mtx); - mutex mtx; - join(coro(mtx)); + REQUIRE(monitor.get_counters().done); } TEST_CASE("Mutex: unique lock unlock", "[Mutex]") { - static const auto coro = [](mutex& mtx) -> task { - unique_lock lk(co_await mtx); - lk.unlock(); - REQUIRE(!lk.owns_lock()); - co_return; - }; - mutex mtx; - join(coro(mtx)); -} + scope_clear guard(mtx); + unique_lock lk(mtx); + lk.try_lock(); + lk.unlock(); + REQUIRE(!lk.owns_lock()); + REQUIRE(!mtx._debug_is_locked()); +} -TEST_CASE("Mutex: unique lock destroy", "[Shared mutex]") { - static const auto coro = [](mutex& mtx) -> task { - { - unique_lock lk(co_await mtx); - REQUIRE(lk.owns_lock()); - } - REQUIRE(mtx.try_lock()); - mtx.unlock(); - co_return; - }; +TEST_CASE("Mutex: unique lock destructor", "[Mutex]") { mutex mtx; - join(coro(mtx)); -} - + scope_clear guard(mtx); -TEST_CASE("Mutex: resume awaiting", "[Mutex]") { - static const auto awaiter = [](mutex& mtx, std::vector& sequence, int id) -> task { - co_await mtx; - sequence.push_back(id); - mtx.unlock(); - }; - static const auto main = [](mutex& mtx, std::vector& sequence) -> task { - auto t1 = awaiter(mtx, sequence, 1); - auto t2 = awaiter(mtx, sequence, 2); - - co_await mtx; - sequence.push_back(0); - t1.launch(); - t2.launch(); - mtx.unlock(); - }; + { + unique_lock lk(mtx); + lk.try_lock(); + } - mutex mtx; - std::vector sequence; - join(main(mtx, sequence)); - REQUIRE(sequence == std::vector{ 0, 1, 2 }); + REQUIRE(!mtx._debug_is_locked()); } \ No newline at end of file diff --git a/test/test_shared_mutex.cpp b/test/test_shared_mutex.cpp index 0f0d63c..1f3cefa 100644 --- a/test/test_shared_mutex.cpp +++ b/test/test_shared_mutex.cpp @@ -1,288 +1,299 @@ -#include -#include +#include "monitor_task.hpp" + #include -#include #include using namespace asyncpp; -TEST_CASE("Shared mutex: try lock", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - REQUIRE(mtx.try_lock()); - REQUIRE(!mtx.try_lock()); - REQUIRE(!mtx.try_lock_shared()); - mtx.unlock(); - co_return; - }; +struct [[nodiscard]] scope_clear { + ~scope_clear() { + mtx._debug_clear(); + } + shared_mutex& mtx; +}; - shared_mutex mtx; - join(coro(mtx)); + +static monitor_task lock_exclusively(shared_mutex& mtx) { + co_await mtx.exclusive(); } -TEST_CASE("Shared mutex: lock", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - co_await mtx.unique(); - REQUIRE(!mtx.try_lock()); - REQUIRE(!mtx.try_lock_shared()); - mtx.unlock(); - }; +static monitor_task lock_shared(shared_mutex& mtx) { + co_await mtx.shared(); +} - shared_mutex mtx; - join(coro(mtx)); + +static monitor_task lock(unique_lock& lk) { + co_await lk; } -TEST_CASE("Shared mutex: unlock", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - co_await mtx.unique(); - mtx.unlock(); +static monitor_task lock(shared_lock& lk) { + co_await lk; +} + + +TEST_CASE("Shared mutex: try lock", "[Shared mutex]") { + shared_mutex mtx; + scope_clear guard(mtx); + + SECTION("exclusive") { REQUIRE(mtx.try_lock()); - mtx.unlock(); + REQUIRE(mtx._debug_is_exclusive_locked()); + } + + SECTION("shared") { REQUIRE(mtx.try_lock_shared()); - mtx.unlock_shared(); - }; + REQUIRE(mtx._debug_is_shared_locked()); + } +} + +TEST_CASE("Shared mutex: lock direct immediate", "[Shared mutex]") { shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + SECTION("exclusive") { + auto monitor = lock_exclusively(mtx); + REQUIRE(monitor.get_counters().done); + REQUIRE(mtx._debug_is_exclusive_locked()); + } + SECTION("shared") { + auto monitor = lock_shared(mtx); + REQUIRE(monitor.get_counters().done); + REQUIRE(mtx._debug_is_shared_locked()); + } } -TEST_CASE("Shared mutex: try lock shared", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - REQUIRE(mtx.try_lock_shared()); - REQUIRE(!mtx.try_lock()); - REQUIRE(mtx.try_lock_shared()); - mtx.unlock_shared(); - mtx.unlock_shared(); - co_return; - }; - +TEST_CASE("Shared mutex: lock spurious immediate", "[Shared mutex]") { shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + SECTION("exclusive") { + auto awaiter = mtx.exclusive(); + auto enclosing = []() -> monitor_task { co_return; }(); + REQUIRE(false == awaiter.await_suspend(enclosing.handle())); + REQUIRE(mtx._debug_is_exclusive_locked()); + } + SECTION("shared") { + auto awaiter = mtx.shared(); + auto enclosing = []() -> monitor_task { co_return; }(); + REQUIRE(false == awaiter.await_suspend(enclosing.handle())); + REQUIRE(mtx._debug_is_shared_locked()); + } + SECTION("shared - shared") { + mtx.try_lock_shared(); + auto awaiter = mtx.shared(); + auto enclosing = []() -> monitor_task { co_return; }(); + REQUIRE(false == awaiter.await_suspend(enclosing.handle())); + REQUIRE(mtx._debug_is_shared_locked()); + } } -TEST_CASE("Shared mutex: lock shared", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - co_await mtx.shared(); +TEST_CASE("Shared mutex: sequencial locking attempts", "[Shared mutex]") { + shared_mutex mtx; + scope_clear guard(mtx); + + SECTION("exclusive - exclusive") { + REQUIRE(mtx.try_lock()); REQUIRE(!mtx.try_lock()); + } + SECTION("exclusive - shared") { + REQUIRE(mtx.try_lock()); + REQUIRE(!mtx.try_lock_shared()); + } + SECTION("shared - exclusive") { REQUIRE(mtx.try_lock_shared()); + REQUIRE(!mtx.try_lock()); + } + SECTION("shared - shared") { + REQUIRE(mtx.try_lock_shared()); + REQUIRE(mtx.try_lock_shared()); + } +} + + +TEST_CASE("Shared mutex: unlock", "[Shared mutex]") { + shared_mutex mtx; + scope_clear guard(mtx); + + SECTION("exclusive -> free") { + mtx.try_lock(); + mtx.unlock(); + REQUIRE(!mtx._debug_is_exclusive_locked()); + REQUIRE(!mtx._debug_is_shared_locked()); + } + SECTION("shared * n -> free") { + mtx.try_lock_shared(); + mtx.try_lock_shared(); mtx.unlock_shared(); + REQUIRE(!mtx._debug_is_exclusive_locked()); + REQUIRE(mtx._debug_is_shared_locked()); mtx.unlock_shared(); - }; + REQUIRE(!mtx._debug_is_exclusive_locked()); + REQUIRE(!mtx._debug_is_shared_locked()); + } + SECTION("locked -> exclusive") { + mtx.try_lock(); - shared_mutex mtx; - join(coro(mtx)); -} + auto monitor1 = lock_exclusively(mtx); + auto monitor2 = lock_exclusively(mtx); + REQUIRE(!monitor1.get_counters().done); + REQUIRE(!monitor2.get_counters().done); -TEST_CASE("Shared mutex: unlock shared", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - co_await mtx.shared(); - mtx.unlock_shared(); - REQUIRE(mtx.try_lock()); mtx.unlock(); - REQUIRE(mtx.try_lock_shared()); - mtx.unlock_shared(); - }; - shared_mutex mtx; - join(coro(mtx)); -} + REQUIRE(monitor1.get_counters().done); + REQUIRE(!monitor2.get_counters().done); + } + SECTION("locked -> shared * n") { + mtx.try_lock(); + auto monitor1 = lock_shared(mtx); + auto monitor2 = lock_shared(mtx); + auto monitor3 = lock_exclusively(mtx); + + REQUIRE(!monitor1.get_counters().done); + REQUIRE(!monitor2.get_counters().done); + REQUIRE(!monitor3.get_counters().done); + + mtx.unlock(); + + REQUIRE(monitor1.get_counters().done); + REQUIRE(monitor2.get_counters().done); + REQUIRE(!monitor3.get_counters().done); + } +} -TEST_CASE("Shared mutex: unique lock try", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - unique_lock lk(mtx); - REQUIRE(!lk.owns_lock()); - REQUIRE(lk.try_lock()); - REQUIRE(lk.owns_lock()); - co_return; - }; +TEST_CASE("Shared mutex: unique lock try_lock", "[Shared mutex]") { shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + unique_lock lk(mtx); + REQUIRE(!lk.owns_lock()); + + lk.try_lock(); + REQUIRE(lk.owns_lock()); + REQUIRE(mtx._debug_is_exclusive_locked()); } TEST_CASE("Shared mutex: unique lock await", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - unique_lock lk(mtx); - REQUIRE(!lk.owns_lock()); - co_await lk; - REQUIRE(lk.owns_lock()); - co_return; - }; - shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + unique_lock lk(mtx); + auto monitor = lock(lk); + REQUIRE(monitor.get_counters().done); + REQUIRE(lk.owns_lock()); + REQUIRE(mtx._debug_is_exclusive_locked()); } TEST_CASE("Shared mutex: unique lock start locked", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - unique_lock lk(co_await mtx.unique()); + shared_mutex mtx; + scope_clear guard(mtx); + + auto monitor = [](shared_mutex& mtx) -> monitor_task { + unique_lock lk(co_await mtx.exclusive()); REQUIRE(lk.owns_lock()); - co_return; - }; + REQUIRE(mtx._debug_is_exclusive_locked()); + }(mtx); - shared_mutex mtx; - join(coro(mtx)); + REQUIRE(monitor.get_counters().done); } TEST_CASE("Shared mutex: unique lock unlock", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - unique_lock lk(co_await mtx.unique()); - lk.unlock(); - REQUIRE(!lk.owns_lock()); - co_return; - }; - shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + unique_lock lk(mtx); + lk.try_lock(); + lk.unlock(); + REQUIRE(!lk.owns_lock()); + REQUIRE(!mtx._debug_is_exclusive_locked()); } -TEST_CASE("Shared mutex: shared lock try", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - shared_lock lk(mtx); - REQUIRE(!lk.owns_lock()); - REQUIRE(lk.try_lock()); - REQUIRE(lk.owns_lock()); - co_return; - }; +TEST_CASE("Shared mutex: unique lock destructor", "[Shared mutex]") { + shared_mutex mtx; + scope_clear guard(mtx); + { + unique_lock lk(mtx); + lk.try_lock(); + } + + REQUIRE(!mtx._debug_is_exclusive_locked()); +} + + +TEST_CASE("Shared mutex: shared lock try_lock", "[Shared mutex]") { shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + shared_lock lk(mtx); + REQUIRE(!lk.owns_lock()); + + lk.try_lock(); + REQUIRE(lk.owns_lock()); + REQUIRE(mtx._debug_is_shared_locked()); } TEST_CASE("Shared mutex: shared lock await", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - shared_lock lk(mtx); - REQUIRE(!lk.owns_lock()); - co_await lk; - REQUIRE(lk.owns_lock()); - co_return; - }; - shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + shared_lock lk(mtx); + auto monitor = lock(lk); + REQUIRE(monitor.get_counters().done); + REQUIRE(lk.owns_lock()); + REQUIRE(mtx._debug_is_shared_locked()); } TEST_CASE("Shared mutex: shared lock start locked", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - shared_lock lk(co_await mtx.shared()); - REQUIRE(lk.owns_lock()); - co_return; - }; - shared_mutex mtx; - join(coro(mtx)); -} - + scope_clear guard(mtx); -TEST_CASE("Shared mutex: shared lock unlock", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { + auto monitor = [](shared_mutex& mtx) -> monitor_task { shared_lock lk(co_await mtx.shared()); REQUIRE(lk.owns_lock()); - lk.unlock(); - REQUIRE(!lk.owns_lock()); - co_return; - }; + REQUIRE(mtx._debug_is_shared_locked()); + }(mtx); - shared_mutex mtx; - join(coro(mtx)); + REQUIRE(monitor.get_counters().done); } -TEST_CASE("Shared mutex: shared lock destroy", "[Shared mutex]") { - static const auto coro = [](shared_mutex& mtx) -> task { - { - shared_lock lk(co_await mtx.shared()); - REQUIRE(lk.owns_lock()); - } - REQUIRE(mtx.try_lock()); - mtx.unlock(); - co_return; - }; - +TEST_CASE("Shared mutex: shared lock unlock", "[Shared mutex]") { shared_mutex mtx; - join(coro(mtx)); + scope_clear guard(mtx); + + shared_lock lk(mtx); + lk.try_lock(); + lk.unlock(); + REQUIRE(!lk.owns_lock()); + REQUIRE(!mtx._debug_is_shared_locked()); } -TEST_CASE("Shared mutex: resume awaiting", "[Shared mutex]") { - static const auto awaiter = [](shared_mutex& mtx, std::vector& sequence, int id) -> task { - co_await mtx.unique(); - sequence.push_back(id); - mtx.unlock(); - }; - static const auto shared_awaiter = [](shared_mutex& mtx, std::vector& sequence, int id) -> task { - co_await mtx.shared(); - sequence.push_back(id); - mtx.unlock_shared(); - }; - static const auto main = [](shared_mutex& mtx, std::vector& sequence) -> task { - auto s1 = shared_awaiter(mtx, sequence, 1); - auto s2 = shared_awaiter(mtx, sequence, 2); - auto u1 = awaiter(mtx, sequence, -1); - auto u2 = awaiter(mtx, sequence, -2); - - co_await mtx.unique(); - sequence.push_back(0); - s1.launch(); - s2.launch(); - u1.launch(); - u2.launch(); - mtx.unlock(); - }; - +TEST_CASE("Shared mutex: shared lock destructor", "[Shared mutex]") { shared_mutex mtx; - std::vector sequence; - join(main(mtx, sequence)); - REQUIRE(sequence == std::vector{ 0, 1, 2, -1, -2 }); -} + scope_clear guard(mtx); + { + shared_lock lk(mtx); + lk.try_lock(); + } -TEST_CASE("Shared mutex: unique starvation", "[Shared mutex]") { - static const auto awaiter = [](shared_mutex& mtx, std::vector& sequence, int id) -> task { - co_await mtx.unique(); - sequence.push_back(id); - mtx.unlock(); - }; - static const auto shared_awaiter = [](shared_mutex& mtx, std::vector& sequence, int id) -> task { - co_await mtx.shared(); - sequence.push_back(id); - mtx.unlock_shared(); - }; - static const auto main = [](shared_mutex& mtx, std::vector& sequence) -> task { - auto s1 = shared_awaiter(mtx, sequence, 1); - auto s2 = shared_awaiter(mtx, sequence, 2); - auto s3 = shared_awaiter(mtx, sequence, 3); - auto s4 = shared_awaiter(mtx, sequence, 4); - auto u1 = awaiter(mtx, sequence, -1); - - co_await mtx.shared(); - sequence.push_back(0); - s1.launch(); - s2.launch(); - u1.launch(); - s3.launch(); - mtx.unlock_shared(); - co_await mtx.shared(); - sequence.push_back(0); - s4.launch(); - mtx.unlock_shared(); - }; - - shared_mutex mtx; - std::vector sequence; - join(main(mtx, sequence)); - REQUIRE(sequence == std::vector{ 0, 1, 2, -1, 3, 0, 4 }); + REQUIRE(!mtx._debug_is_shared_locked()); } \ No newline at end of file diff --git a/test/test_shared_task.cpp b/test/test_shared_task.cpp deleted file mode 100644 index e01bb21..0000000 --- a/test/test_shared_task.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "helper_interleaving.hpp" - -#include -#include -#include - -#include - - -using namespace asyncpp; - - -TEST_CASE("Shared task: interleaving co_await", "[Shared task]") { - static const auto sub_task = []() -> shared_task { - co_return 3; - }; - static const auto main_task = [](shared_task tsk) -> shared_task { - tsk.launch(); - co_return co_await tsk; - }; - - auto interleavings = run_dependent_tasks(main_task, sub_task, std::tuple{}, std::tuple{}); - evaluate_interleavings(std::move(interleavings)); -} - - -TEST_CASE("Shared task: interleaving abandon", "[Shared task]") { - static const auto abandoned_task = []() -> shared_task { co_return 3; }; - - auto interleavings = run_abandoned_task(abandoned_task, std::tuple{}); - evaluate_interleavings(std::move(interleavings)); -} - - -TEST_CASE("Shared task: abandon (not started)", "[Shared task]") { - static const auto coro = []() -> shared_task { - co_return; - }; - const auto before = impl::leak_checked_promise::snapshot(); - static_cast(coro()); - REQUIRE(impl::leak_checked_promise::check(before)); -} - - -TEST_CASE("Shared task: co_await value", "[Shared task]") { - static const auto coro = [](int value) -> shared_task { - co_return value; - }; - static const auto enclosing = [](int value) -> shared_task { - co_return co_await coro(value); - }; - REQUIRE(join(enclosing(42)) == 42); -} - - -TEST_CASE("Shared task: co_await ref", "[Shared task]") { - static int value = 42; - static const auto coro = [](int& value) -> shared_task { - co_return value; - }; - static const auto enclosing = [](int& value) -> shared_task { - co_return co_await coro(value); - }; - auto task = enclosing(value); - auto& result = join(task); - REQUIRE(result == 42); - REQUIRE(&result == &value); -} - - -TEST_CASE("Shared task: co_await void", "[Shared task]") { - static int value = 42; - static const auto coro = []() -> shared_task { - co_return; - }; - static const auto enclosing = []() -> shared_task { - co_await coro(); - }; - auto task = enclosing(); - join(task); -} \ No newline at end of file diff --git a/test/test_stream.cpp b/test/test_stream.cpp index c96d220..61c6147 100644 --- a/test/test_stream.cpp +++ b/test/test_stream.cpp @@ -1,14 +1,89 @@ +#include "monitor_task.hpp" + #include #include -#include - #include using namespace asyncpp; +TEST_CASE("Stream: creation", "[Stream]") { + const auto coro = []() -> stream { + co_yield 0; + }; + + SECTION("empty") { + stream s; + REQUIRE(!s.valid()); + } + SECTION("valid") { + auto s = coro(); + REQUIRE(s.valid()); + } +} + + +TEST_CASE("Stream: iteration", "[Stream]") { + static const auto coro = []() -> stream { + co_yield 0; + co_yield 1; + }; + + auto s = coro(); + auto r = [&]() -> monitor_task { + auto i1 = co_await s; + REQUIRE(i1); + REQUIRE(*i1 == 0); + auto i2 = co_await s; + REQUIRE(i2); + REQUIRE(*i2 == 1); + auto i3 = co_await s; + REQUIRE(!i3); + }(); + REQUIRE(r.get_counters().done); +} + + +TEST_CASE("Stream: data types", "[Stream]") { + SECTION("value") { + static const auto coro = []() -> stream { + co_yield 0; + }; + auto r = []() -> monitor_task { + auto s = coro(); + auto item = co_await s; + REQUIRE(*item == 0); + }(); + REQUIRE(r.get_counters().done); + } + SECTION("reference") { + static int value = 0; + static const auto coro = []() -> stream { + co_yield value; + }; + auto r = []() -> monitor_task { + auto s = coro(); + auto item = co_await s; + REQUIRE(&*item == &value); + }(); + REQUIRE(r.get_counters().done); + } + SECTION("exception") { + static const auto coro = []() -> stream { + throw std::runtime_error("test"); + co_return; + }; + auto r = []() -> monitor_task { + auto s = coro(); + REQUIRE_THROWS_AS(co_await s, std::runtime_error); + }(); + REQUIRE(r.get_counters().done); + } +} + + TEST_CASE("Stream: destroy", "[Task]") { static const auto coro = []() -> stream { co_yield 0; }; @@ -23,49 +98,8 @@ TEST_CASE("Stream: destroy", "[Task]") { const auto before = impl::leak_checked_promise::snapshot(); { auto s = coro(); - join(s); + void(join(s)); } REQUIRE(impl::leak_checked_promise::check(before)); } } - - -TEST_CASE("Stream: co_await", "[Generator]") { - static const auto coro = [](int count) -> stream { - static int i; - for (i = 0; i < count; ++i) { - co_yield i; - } - }; - static const auto enclosing = [](int count) -> stream> { - std::vector values; - const auto s = coro(count); - while (const auto value = co_await s) { - values.push_back(*value); - } - co_yield values; - }; - const auto results = join(enclosing(4)); - REQUIRE(results == std::vector{ 0, 1, 2, 3 }); -} - - -TEST_CASE("Stream: co_await - reference", "[Generator]") { - static const auto coro = [](int count) -> stream { - static int i; - for (i = 0; i < count; ++i) { - co_yield i; - } - }; - static const auto enclosing = [](int count) -> stream> { - std::vector values; - const auto s = coro(count); - while (const auto value = co_await s) { - values.push_back(*value); - ++value->get(); - } - co_yield values; - }; - const auto results = join(enclosing(8)); - REQUIRE(results == std::vector{ 0, 2, 4, 6 }); -} \ No newline at end of file diff --git a/test/test_task.cpp b/test/test_task.cpp index 7d3aaa7..8bf5bca 100644 --- a/test/test_task.cpp +++ b/test/test_task.cpp @@ -1,39 +1,121 @@ -#include "helper_interleaving.hpp" +#include "helper_schedulers.hpp" -#include #include +#include #include +#include +#include #include using namespace asyncpp; -TEST_CASE("Task: interleaving co_await", "[Task]") { - static const auto sub_task = []() -> task { - co_return 3; - }; - static const auto main_task = [](task tsk) -> task { - tsk.launch(); - co_return co_await tsk; +TEMPLATE_TEST_CASE("Task: valid", "[Task]", task, shared_task) { + SECTION("empty") { + TestType t; + REQUIRE(!t.valid()); + } + SECTION("valid") { + auto t = []() -> TestType { co_return; }(); + REQUIRE(t.valid()); + } +} + + +TEMPLATE_TEST_CASE("Task: launch & ready", "[Task]", task, shared_task) { + auto t = []() -> TestType { co_return; }(); + REQUIRE(!t.ready()); + t.launch(); + REQUIRE(t.ready()); +} + + +TEMPLATE_TEST_CASE("Task: bind", "[Task]", task, shared_task) { + auto t = []() -> TestType { co_return; }(); + thread_locked_scheduler sched; + t.bind(sched); + t.launch(); + REQUIRE(!t.ready()); + sched.resume(); + REQUIRE(t.ready()); +} + + +TEMPLATE_TEST_CASE("Task: interleaving co_await", "[Task]", task, shared_task) { + struct scenario { + thread_locked_scheduler awaiter_sched; + thread_locked_scheduler awaited_sched; + TestType result; + + scenario() { + constexpr auto awaited = []() -> TestType { + co_return 1; + }; + constexpr auto awaiter = [](TestType awaited) -> TestType { + co_return co_await awaited; + }; + + auto tmp = launch(awaited(), awaited_sched); + result = launch(awaiter(std::move(tmp)), awaiter_sched); + } + + void awaiter() { + awaiter_sched.resume(); + if (!result.ready()) { + INTERLEAVED_ACQUIRE(awaiter_sched.wait()); + awaiter_sched.resume(); + } + REQUIRE(1 == join(result)); + result = {}; + } + + void awaited() { + awaited_sched.resume(); + } }; - auto interleavings = run_dependent_tasks(main_task, sub_task, std::tuple{}, std::tuple{}); - evaluate_interleavings(std::move(interleavings)); + INTERLEAVED_RUN( + scenario, + THREAD("awaited", &scenario::awaited), + THREAD("awaiter", &scenario::awaiter)); } -TEST_CASE("Task: interleaving abandon", "[Task]") { - static const auto abandoned_task = []() -> task { co_return 3; }; +TEMPLATE_TEST_CASE("Task: interleaving abandon", "[Task]", task, shared_task) { + struct scenario : testing::validated_scenario { + thread_locked_scheduler sched; + TestType result; + + scenario() { + result = launch(coro(), sched); + } + + TestType coro() { + co_return 1; + }; + + void task() { + sched.resume(); + } - auto interleavings = run_abandoned_task(abandoned_task, std::tuple{}); - evaluate_interleavings(std::move(interleavings)); + void abandon() { + result = {}; + } + + void validate(const testing::path& path) override {} + }; + + INTERLEAVED_RUN( + scenario, + THREAD("task", &scenario::task), + THREAD("abandon", &scenario::abandon)); } -TEST_CASE("Task: abandon (not started)", "[Shared task]") { - static const auto coro = []() -> task { +TEMPLATE_TEST_CASE("Task: abandon (not started)", "[Task]", task, shared_task) { + static const auto coro = []() -> TestType { co_return; }; const auto before = impl::leak_checked_promise::snapshot(); @@ -42,23 +124,23 @@ TEST_CASE("Task: abandon (not started)", "[Shared task]") { } -TEST_CASE("Task: co_await value", "[Task]") { - static const auto coro = [](int value) -> task { +TEMPLATE_TEST_CASE("Task: co_await value", "[Task]", task, shared_task) { + static const auto coro = [](int value) -> TestType { co_return value; }; - static const auto enclosing = [](int value) -> task { + static const auto enclosing = [](int value) -> TestType { co_return co_await coro(value); }; REQUIRE(join(enclosing(42)) == 42); } -TEST_CASE("Task: co_await ref", "[Task]") { +TEMPLATE_TEST_CASE("Task: co_await ref", "[Task]", task, shared_task) { static int value = 42; - static const auto coro = [](int& value) -> task { + static const auto coro = [](int& value) -> TestType { co_return value; }; - static const auto enclosing = [](int& value) -> task { + static const auto enclosing = [](int& value) -> TestType { co_return co_await coro(value); }; auto task = enclosing(value); @@ -68,14 +150,28 @@ TEST_CASE("Task: co_await ref", "[Task]") { } -TEST_CASE("Task: co_await void", "[Task]") { +TEMPLATE_TEST_CASE("Task: co_await void", "[Task]", task, shared_task) { static int value = 42; - static const auto coro = []() -> task { + static const auto coro = []() -> TestType { co_return; }; - static const auto enclosing = []() -> task { + static const auto enclosing = []() -> TestType { co_await coro(); }; auto task = enclosing(); join(task); +} + + +TEMPLATE_TEST_CASE("Task: co_await exception", "[Task]", task, shared_task) { + static int value = 42; + static const auto coro = []() -> TestType { + throw std::runtime_error("test"); + co_return; + }; + static const auto enclosing = []() -> TestType { + REQUIRE_THROWS_AS(co_await coro(), std::runtime_error); + }; + auto task = enclosing(); + join(task); } \ No newline at end of file diff --git a/test/test_thread_pool.cpp b/test/test_thread_pool.cpp index e69f5ba..9c84699 100644 --- a/test/test_thread_pool.cpp +++ b/test/test_thread_pool.cpp @@ -1,5 +1,8 @@ +#include "helper_schedulers.hpp" + #include #include +#include #include #include @@ -14,12 +17,94 @@ using namespace asyncpp; +struct test_promise : schedulable_promise { + std::coroutine_handle<> handle() override { + ++num_queried; + return std::noop_coroutine(); + } + std::atomic_size_t num_queried = 0; +}; + + +TEST_CASE("Thread pool: schedule worklist selection", "[Thread pool]") { + std::condition_variable global_notification; + std::mutex global_mutex; + atomic_stack global_worklist; + std::atomic_size_t num_waiting; + std::vector workers(1); + + test_promise promise; + + SECTION("has local worker") { + thread_pool::schedule(promise, global_worklist, global_notification, global_mutex, num_waiting, &workers[0]); + REQUIRE(workers[0].worklist.pop() == &promise); + REQUIRE(global_worklist.empty()); + } + SECTION("no local worker") { + thread_pool::schedule(promise, global_worklist, global_notification, global_mutex, num_waiting, &workers[0]); + REQUIRE(workers[0].worklist.pop() == &promise); + } +} + + +TEST_CASE("Thread pool: steal from workers", "[Thread pool]") { + std::vector workers(4); + + test_promise promise; + + SECTION("no work items") { + REQUIRE(nullptr == thread_pool::steal(workers)); + } + SECTION("1 work item") { + workers[2].worklist.push(&promise); + REQUIRE(&promise == thread_pool::steal(workers)); + } +} + + +TEST_CASE("Thread pool: ensure execution", "[Thread pool]") { + // This test makes sure that no matter the interleaving, a scheduled promise + // will be picked up and executed by a worker thread. + + struct scenario : testing::validated_scenario { + std::condition_variable global_notification; + std::mutex global_mutex; + atomic_stack global_worklist; + std::atomic_size_t num_waiting; + std::vector workers; + std::atomic_flag terminate; + test_promise promise; + + scenario() : workers(1) {} + + void schedule() { + thread_pool::schedule(promise, global_worklist, global_notification, global_mutex, num_waiting); + std::unique_lock lk(global_mutex, std::defer_lock); + INTERLEAVED_ACQUIRE(lk.lock()); + INTERLEAVED(terminate.test_and_set()); + INTERLEAVED(global_notification.notify_all()); + } + + void execute() { + thread_pool::execute(workers[0], global_worklist, global_notification, global_mutex, terminate, num_waiting, std::span(workers)); + } + + void validate(const testing::path& p) override { + INFO(p.dump()); + REQUIRE(promise.num_queried.load() > 0); + } + }; + + INTERLEAVED_RUN(scenario, THREAD("schedule", &scenario::schedule), THREAD("execute", &scenario::execute)); +} + + constexpr size_t num_threads = 4; constexpr int64_t depth = 5; constexpr size_t branching = 10; -TEST_CASE("Thread pool: perf test", "[Scheduler]") { +TEST_CASE("Thread pool: smoke test - schedule tasks", "[Scheduler]") { thread_pool sched(num_threads); static const auto coro = [&sched](auto self, int depth) -> task { @@ -36,9 +121,6 @@ TEST_CASE("Thread pool: perf test", "[Scheduler]") { }; const auto count = int64_t(std::pow(branching, depth)); - const auto start = std::chrono::high_resolution_clock::now(); const auto result = join(bind(coro(coro, depth), sched)); - const auto end = std::chrono::high_resolution_clock::now(); - std::cout << "performance: " << 1e9 * double(count) / std::chrono::nanoseconds(end - start).count() << " / s" << std::endl; REQUIRE(result == count); } \ No newline at end of file diff --git a/test/testing/test_interleaver.cpp b/test/testing/test_interleaver.cpp new file mode 100644 index 0000000..cea2f6a --- /dev/null +++ b/test/testing/test_interleaver.cpp @@ -0,0 +1,331 @@ +#include + +#include + +#include + + +using namespace asyncpp::testing; + + +static suspension_point p1; +static suspension_point p2; +static suspension_point p3; + + +// Number of combinations. +template +T ncr(T n, T k) { + T result = 1; + for (auto factor = n; factor >= n - k + 1; --factor) { + result *= factor; + } + for (auto divisor = k; divisor >= 1; --divisor) { + result /= divisor; + } + return result; +} + + +TEST_CASE("Helper - nCr helper code", "[Helper]") { + REQUIRE(ncr(1, 1) == 1); + REQUIRE(ncr(4, 1) == 4); + REQUIRE(ncr(4, 2) == 6); +} + + +TEST_CASE("Interleaver - next from stable node", "[Interleaver]") { + const std::vector initial = { thread_state::suspended(p1) }; + + SECTION("first time: add new state") { + tree t; + auto& transition = t.next(t.root(), swarm_state(initial)); + + REQUIRE(t.root().swarm_states.size() == 1); + REQUIRE(t.root().swarm_states.begin()->first == swarm_state(initial)); + REQUIRE(&t.previous(transition) == &t.root()); + } + SECTION("subsequent times: fetch state") { + tree t; + auto& transition_1 = t.next(t.root(), swarm_state(initial)); + auto& transition_2 = t.next(t.root(), swarm_state(initial)); + REQUIRE(t.root().swarm_states.size() == 1); + REQUIRE(&transition_1 == &transition_2); + } +} + + +TEST_CASE("Interleaver - next from transition node", "[Interleaver]") { + const std::vector initial = { thread_state::suspended(p1) }; + + SECTION("first time: add new transition") { + tree t; + auto& transition = t.next(t.root(), swarm_state(initial)); + auto& next = t.next(transition, 0); + + REQUIRE(next.swarm_states.empty()); + REQUIRE(&t.previous(next) == &transition); + REQUIRE(transition.completed.empty()); + REQUIRE(transition.successors.contains(0)); + REQUIRE(transition.successors.find(0)->second.get() == &next); + } + SECTION("subsequent times: fetch transition") { + tree t; + auto& transition = t.next(t.root(), swarm_state(initial)); + auto& next_1 = t.next(transition, 0); + auto& next_2 = t.next(transition, 0); + + REQUIRE(transition.successors.contains(0)); + REQUIRE(transition.successors.find(0)->second.get() == &next_1); + REQUIRE(transition.successors.find(0)->second.get() == &next_2); + } +} + + +TEST_CASE("Interleaver - is transitively complete", "[Interleaver]") { + const std::vector initial = { thread_state::suspended(p1) }; + const std::vector final = { thread_state::completed }; + const std::vector blocked = { thread_state::blocked }; + + SECTION("empty root node") { + tree t; + REQUIRE(is_transitively_complete(t, t.root())); + } + SECTION("complete node") { + tree t; + t.next(t.root(), swarm_state(final)); + REQUIRE(is_transitively_complete(t, t.root())); + } + SECTION("complete transition") { + tree t; + auto& transition = t.next(t.root(), swarm_state(initial)); + transition.completed.insert(0); + REQUIRE(is_transitively_complete(t, t.root())); + } + SECTION("incomplete transition & incomplete node") { + tree t; + t.next(t.root(), swarm_state(initial)); + REQUIRE(!is_transitively_complete(t, t.root())); + } + SECTION("blocked") { + tree t; + t.next(t.root(), swarm_state(blocked)); + REQUIRE(is_transitively_complete(t, t.root())); + } +} + + +TEST_CASE("Interleaver - mark complete", "[Interleaver]") { + const std::vector at_p1 = { thread_state::suspended(p1) }; + const std::vector at_p2 = { thread_state::suspended(p2) }; + const std::vector final = { thread_state::completed }; + + tree t; + auto& n1 = t.root(); + auto& t1 = t.next(n1, swarm_state(at_p1)); + auto& n2 = t.next(t1, 0); + auto& t2 = t.next(t.root(), swarm_state(at_p2)); + auto& n3 = t.next(t2, 0); + + mark_complete(t, n3); + + REQUIRE(is_transitively_complete(t, n1)); +} + + +TEST_CASE("Interleaver - select resumed", "[Interleaver]") { + const std::vector initial = { thread_state::suspended(p3), thread_state::suspended(p3) }; + const std::vector both_ready = { thread_state::suspended(p1), thread_state::suspended(p1) }; + const std::vector left_ready = { thread_state::suspended(p1), thread_state::completed }; + const std::vector right_ready = { thread_state::completed, thread_state::suspended(p1) }; + const std::vector none_ready = { thread_state::completed, thread_state::completed }; + const std::vector left_blocked = { thread_state::blocked, thread_state::suspended(p1) }; + const std::vector right_blocked = { thread_state::suspended(p1), thread_state::blocked }; + + tree t; + auto& transition = t.next(t.root(), swarm_state(initial)); + + SECTION("both ready - none visited") { + REQUIRE(0 == select_resumed(swarm_state(both_ready), transition)); + } + SECTION("both ready - left visited") { + t.next(transition, 0); + REQUIRE(1 == select_resumed(swarm_state(both_ready), transition)); + } + SECTION("both ready - right visited") { + t.next(transition, 1); + REQUIRE(0 == select_resumed(swarm_state(both_ready), transition)); + } + SECTION("both ready - left completed") { + t.next(transition, 0); + t.next(transition, 1); + transition.completed.insert(0); + REQUIRE(1 == select_resumed(swarm_state(both_ready), transition)); + } + SECTION("both ready - right completed") { + t.next(transition, 0); + t.next(transition, 1); + transition.completed.insert(1); + REQUIRE(0 == select_resumed(swarm_state(both_ready), transition)); + } + SECTION("both ready - both completed") { + t.next(transition, 0); + t.next(transition, 1); + transition.completed.insert(0); + transition.completed.insert(1); + REQUIRE(0 == select_resumed(swarm_state(both_ready), transition)); + } + SECTION("left ready") { + REQUIRE(0 == select_resumed(swarm_state(left_ready), transition)); + } + SECTION("right ready") { + REQUIRE(1 == select_resumed(swarm_state(right_ready), transition)); + } + SECTION("left blocked") { + REQUIRE(1 == select_resumed(swarm_state(left_blocked), transition)); + } + SECTION("right blocked") { + REQUIRE(0 == select_resumed(swarm_state(right_blocked), transition)); + } + SECTION("none ready") { + REQUIRE_THROWS(select_resumed(swarm_state(none_ready), transition)); + } +} + + +struct CollectorScenario { + CollectorScenario() { + interleavings.push_back({}); + } + + static void hit(int id) { + interleavings.back().push_back(id); + } + + static void reset() { + interleavings.clear(); + } + + inline static std::vector> interleavings = {}; +}; + + +TEST_CASE("Interleaver - single thread combinatorics", "[Interleaver]") { + struct Scenario : CollectorScenario { + void thread_0() { + INTERLEAVED(hit(0)); + INTERLEAVED(hit(1)); + INTERLEAVED(hit(2)); + } + }; + + Scenario::reset(); + INTERLEAVED_RUN( + Scenario, + THREAD("thread_0", &Scenario::thread_0)); + const std::vector> expected = { + {0, 1, 2} + }; + REQUIRE(Scenario::interleavings == expected); +} + + +TEST_CASE("Interleaver - two thread combinatorics", "[Interleaver]") { + struct Scenario : CollectorScenario { + void thread_0() { + hit(10); + INTERLEAVED("A0"); + hit(11); + INTERLEAVED("A1"); + hit(12); + } + void thread_1() { + hit(20); + INTERLEAVED("B0"); + hit(21); + INTERLEAVED("B1"); + hit(22); + } + }; + + Scenario::reset(); + INTERLEAVED_RUN( + Scenario, + THREAD("thread_0", &Scenario::thread_0), + THREAD("thread_1", &Scenario::thread_1)); + + // Get and sort(!) all executed interleavings. + auto interleaveings = std::move(Scenario::interleavings); + std::ranges::sort(interleaveings); + + // Check no interleaving was run twice. + REQUIRE(std::ranges::unique(interleaveings).end() == interleaveings.end()); + + // Check we have all the interleavings. + REQUIRE(interleaveings.size() == ncr(6, 3)); +} + + +TEST_CASE("Interleaver - three thread combinatorics", "[Interleaver]") { + struct Scenario : CollectorScenario { + void thread_0() { + hit(10); + INTERLEAVED("A0"); + hit(11); + } + void thread_1() { + hit(20); + INTERLEAVED("B0"); + hit(21); + } + void thread_2() { + hit(30); + INTERLEAVED("C0"); + hit(31); + } + }; + + Scenario::reset(); + INTERLEAVED_RUN( + Scenario, + THREAD("thread_0", &Scenario::thread_0), + THREAD("thread_1", &Scenario::thread_1), + THREAD("thread_2", &Scenario::thread_2)); + + auto interleaveings = std::move(Scenario::interleavings); + std::ranges::sort(interleaveings); + + REQUIRE(std::ranges::unique(interleaveings).end() == interleaveings.end()); + REQUIRE(interleaveings.size() == ncr(4, 2) * ncr(6, 4)); +} + + +TEST_CASE("Interleaver - acquire", "[Interleaver]") { + struct Scenario : CollectorScenario { + std::atomic_flag f; + + void thread_0() { + INTERLEAVED_ACQUIRE("A0"); + while (!f.test()) { + } + INTERLEAVED("A1"); + hit(10); + } + void thread_1() { + INTERLEAVED("B0"); + hit(20); + f.test_and_set(); + } + }; + + Scenario::reset(); + INTERLEAVED_RUN( + Scenario, + THREAD("thread_0", &Scenario::thread_0), + THREAD("thread_1", &Scenario::thread_1)); + + auto interleaveings = std::move(Scenario::interleavings); + std::ranges::sort(interleaveings); + REQUIRE(interleaveings.size() >= 1); + REQUIRE(interleaveings[0] == std::vector{ 20, 10 }); +} \ No newline at end of file