diff --git a/Source/CTF/ctf/CMakeLists.txt b/Source/CTF/ctf/CMakeLists.txt index 33c6a0d..e295cce 100644 --- a/Source/CTF/ctf/CMakeLists.txt +++ b/Source/CTF/ctf/CMakeLists.txt @@ -57,6 +57,8 @@ set(SOURCES "BasicString.h" # threading + "Threading/CancellationToken.cpp" + "Threading/CancellationToken.h" #"Threading/Thread.cpp" "Threading/Thread.h" #"Threading/Mutex.cpp" diff --git a/Source/CTF/ctf/Threading/CancellationToken.cpp b/Source/CTF/ctf/Threading/CancellationToken.cpp new file mode 100644 index 0000000..16e12b5 --- /dev/null +++ b/Source/CTF/ctf/Threading/CancellationToken.cpp @@ -0,0 +1,32 @@ +#include "CTF.h" +#include "CancellationToken.h" + +namespace CTF +{ + void CancellationToken::ThrowIfCancellationRequested() const + { + if ( IsCancellationRequested() ) + { + throw OperationCanceledException(); + } + } + + CancellationTokenSource::CancellationTokenSource() + : m_State( std::make_shared() ) + { + } + + void CancellationTokenSource::Cancel() noexcept + { + m_State->IsCancellationRequested.store( + true, + std::memory_order_release ); + } + + [[nodiscard]] + bool CancellationTokenSource::IsCancellationRequested() const noexcept + { + return m_State->IsCancellationRequested.load( + std::memory_order_acquire ); + } +} \ No newline at end of file diff --git a/Source/CTF/ctf/Threading/CancellationToken.h b/Source/CTF/ctf/Threading/CancellationToken.h new file mode 100644 index 0000000..a680f4b --- /dev/null +++ b/Source/CTF/ctf/Threading/CancellationToken.h @@ -0,0 +1,130 @@ +/* + * Cereon Template Framework, a C++ 23 standard template library. + * Copyright (c) 2026 The Aridity Team, all rights reserved. + * + * This file is part of the Cereon Template Framework project. + * + * Cereon Template Framework is free software: you can redistribute + * it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either + * version 3 of the License, or any later version. + * + * Cereon Template Framework is distributed in the hope that it will + * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cereon Template Framework. If not, see . + */ + +#ifndef CTF_CANCELLATIONTOKEN_H +#define CTF_CANCELLATIONTOKEN_H +#pragma once + +#include +#include +#include +#include + +namespace CTF +{ + /** + * @brief Thrown if an operation has been canceled. + */ + class OperationCanceledException : public std::exception + { + public: + const char *what() const noexcept override + { + return "The operation was canceled."; + } + }; + + namespace Internal + { + struct CancellationState + { + std::atomic IsCancellationRequested = false; + }; + } + + /** + * @brief Propagates notification that operations should be canceled. + */ + class CTF_API CancellationToken + { + public: + constexpr CancellationToken() noexcept = default; + + /** + * @brief Gets whether this token is capable of being in the canceled state. + */ + [[nodiscard]] + bool CanBeCanceled() const noexcept + { + return static_cast( m_State ); + } + + /** + * @brief Gets whether cancellation has been requested for this token. + */ + [[nodiscard]] + bool IsCancellationRequested() const noexcept + { + return m_State && + m_State->IsCancellationRequested.load( + std::memory_order_acquire ); + } + + /** + * @brief Throws a OperationCanceledException if this token has had cancellation requested. + */ + void ThrowIfCancellationRequested() const; + + private: + friend class CancellationTokenSource; + + explicit CancellationToken( + std::shared_ptr state ) + : m_State( std::move( state ) ) + { + } + + std::shared_ptr m_State; + }; + + /** + * @brief Signals to a CancellationToken that it should be canceled. + */ + class CTF_API CancellationTokenSource + { + public: + CancellationTokenSource(); + + /** + * @brief Gets the CancellationToken associated with this CancellationTokenSource. + */ + [[nodiscard]] + CancellationToken GetToken() const + { + return CancellationToken( m_State ); + } + + /** + * @brief Communicates a request for cancellation. + */ + void Cancel() noexcept; + + /** + * @brief Gets whether cancellation has been requested for this CancellationTokenSource. + */ + [[nodiscard]] + bool IsCancellationRequested() const noexcept; + + private: + std::shared_ptr m_State; + }; +} + +#endif // !CTF_CANCELLATIONTOKEN_H diff --git a/Source/CTF/ctf/Threading/Task.h b/Source/CTF/ctf/Threading/Task.h index bde86d4..e7bdc1b 100644 --- a/Source/CTF/ctf/Threading/Task.h +++ b/Source/CTF/ctf/Threading/Task.h @@ -22,6 +22,8 @@ #define TASK_H #pragma once +#include +#include "CancellationToken.h" #include "Thread.h" #include "Mutex.h" #include @@ -29,67 +31,151 @@ namespace CTF::Threading { - template - class Task { - public: - explicit Task(std::function func) : done_(false) { - thread_ = Thread([this, func]() { - result_ = func(); - }); - } - - ~Task() { - if (thread_.joinable()) thread_.join(); - } - - T get() { - if (!done_) { - thread_.join(); - done_ = true; - } - return result_; - } - - private: - Thread thread_; - T result_{}; - bool done_; - mutable Mutex mutex_; - }; - - template <> - class Task { - public: - explicit Task(std::function func) : done_(false) { - thread_ = Thread([this, func]() { - func(); - }); - } - - ~Task() { - if (thread_.joinable()) thread_.join(); - } - - void get() { - if (!done_) { - thread_.join(); - done_ = true; - } - } - - private: - Thread thread_; - bool done_; - mutable Mutex mutex_; - }; + template + class Task + { + public: + explicit Task( + std::function func, + CancellationToken token = {} ) + : cancellationToken_( std::move( token ) ) + { + thread_ = Thread( [ this, func ]() + { + try + { + cancellationToken_.ThrowIfCancellationRequested(); + + result_ = func( cancellationToken_ ); + + canceled_ = cancellationToken_.IsCancellationRequested(); + } + catch ( ... ) + { + exception_ = std::current_exception(); + } + + done_ = true; + } ); + } + + ~Task() + { + if ( thread_.joinable() ) + thread_.join(); + } + + T get() + { + if ( thread_.joinable() ) + thread_.join(); + + done_ = true; + + if ( exception_ ) + std::rethrow_exception( exception_ ); + + return result_; + } + + bool IsCompleted() const + { + return done_; + } + + bool IsCanceled() const + { + return canceled_; + } + + private: + Thread thread_; + + CancellationToken cancellationToken_; + + T result_ {}; + + std::atomic done_ { false }; + std::atomic canceled_ { false }; + + std::exception_ptr exception_; + }; + + template<> + class Task + { + public: + explicit Task( + std::function func, + CancellationToken token = {} ) + : cancellationToken_( std::move( token ) ) + { + thread_ = Thread( [ this, func ]() + { + try + { + cancellationToken_.ThrowIfCancellationRequested(); + + func( cancellationToken_ ); + + canceled_ = cancellationToken_.IsCancellationRequested(); + } + catch ( ... ) + { + exception_ = std::current_exception(); + } + + done_ = true; + } ); + } + + ~Task() + { + if ( thread_.joinable() ) + thread_.join(); + } + + void get() + { + if ( thread_.joinable() ) + thread_.join(); + + done_ = true; + + if ( exception_ ) + std::rethrow_exception( exception_ ); + } + + bool IsCompleted() const + { + return done_; + } + + bool IsCanceled() const + { + return canceled_; + } + + private: + Thread thread_; + + CancellationToken cancellationToken_; + + std::atomic done_ { false }; + std::atomic canceled_ { false }; + + std::exception_ptr exception_; + }; } -namespace CTF::Threading::Internal { - template - auto make_task_async(Func&& func) { - using result_t = decltype(func()); - return Task(std::forward(func)); - } +namespace CTF::Threading::Internal +{ + template + auto make_task_async( Func &&func, CancellationToken token = {} ) + { + using result_t = decltype( func( token ) ); + return Task( std::forward( func ), token ); + } } #define async(func) CTF::Threading::make_task_async([&]() { return func; }) @@ -99,4 +185,4 @@ namespace CTF::Threading::Internal { } #define await(task) (task).get() -#endif // TASK_H \ No newline at end of file +#endif // TASK_H diff --git a/Source/CTF/tests/CMakeLists.txt b/Source/CTF/tests/CMakeLists.txt index 2cb3e40..3a4cac6 100644 --- a/Source/CTF/tests/CMakeLists.txt +++ b/Source/CTF/tests/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(io) +add_subdirectory(threading) add_subdirectory(json) add_subdirectory(math) #add_subdirectory(keyvalues) diff --git a/Source/CTF/tests/threading/CMakeLists.txt b/Source/CTF/tests/threading/CMakeLists.txt new file mode 100644 index 0000000..cee3e73 --- /dev/null +++ b/Source/CTF/tests/threading/CMakeLists.txt @@ -0,0 +1,55 @@ +project(testThreading) + +if(MSVC) + add_compile_options(/wd4172) +elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + add_compile_options(-Wno-return-local-addr) +endif() + +set(CTF_INC_DIRS ${SRC_DIR}/CTF/ctf) + +set(SOURCES + "main.cpp" + "tasktest.cpp" +) + +if(MSVC) + foreach(FILE ${SOURCES}) + get_filename_component(PARENT_DIR "${FILE}" DIRECTORY) + + string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}" "" GROUP "${PARENT_DIR}") + + string(REPLACE "/" "\\" GROUP "${GROUP}") + + if ("${FILE}" MATCHES ".*\\.cpp" OR "${FILE}" MATCHES ".*\\.inl" OR "${FILE}" MATCHES ".*\\.ui" OR "${FILE}" MATCHES ".*\\.qml") + set(GROUP "Source Files\\${GROUP}") + elseif("${FILE}" MATCHES ".*\\.h") + set(GROUP "Header Files\\${GROUP}") + endif() + + source_group("${GROUP}" FILES "${FILE}") + endforeach() +endif() + +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CTF_INC_DIRS} +) + +add_dependencies(${PROJECT_NAME} + CTF + gtest + gtest_main +) +target_link_libraries(${PROJECT_NAME} PRIVATE CTF) +target_link_libraries(${PROJECT_NAME} PRIVATE + gtest + gtest_main +) + +add_test( + NAME CTF_Threading_Test + COMMAND testThreading +) diff --git a/Source/CTF/tests/threading/main.cpp b/Source/CTF/tests/threading/main.cpp new file mode 100644 index 0000000..e2885cc --- /dev/null +++ b/Source/CTF/tests/threading/main.cpp @@ -0,0 +1,26 @@ +/* + * Cereon Template Framework, a C++ 23 standard template library. + * Copyright (c) 2026 The Aridity Team, all rights reserved. + * + * This file is part of the Cereon Template Framework project. + * + * Cereon Template Framework is free software: you can redistribute + * it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either + * version 3 of the License, or any later version. + * + * Cereon Template Framework is distributed in the hope that it will + * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cereon Template Framework. If not, see . + */ + +#include + +int main(int argc, char *argv[]) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/Source/CTF/tests/threading/tasktest.cpp b/Source/CTF/tests/threading/tasktest.cpp new file mode 100644 index 0000000..43c11a7 --- /dev/null +++ b/Source/CTF/tests/threading/tasktest.cpp @@ -0,0 +1,179 @@ +/* + * Cereon Template Framework, a C++ 23 standard template library. + * Copyright (c) 2026 The Aridity Team, all rights reserved. + * + * This file is part of the Cereon Template Framework project. + * + * Cereon Template Framework is free software: you can redistribute + * it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either + * version 3 of the License, or any later version. + * + * Cereon Template Framework is distributed in the hope that it will + * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cereon Template Framework. If not, see . + */ + +#include + +#include +#include + +using namespace CTF; +using namespace CTF::Threading; + +TEST( TaskTests, ReturnsResult ) +{ + Task task( + []( CancellationToken ) + { + return 42; + } ); + + EXPECT_EQ( task.get(), 42 ); +} + +TEST( TaskTests, VoidTaskCompletes ) +{ + bool executed = false; + + Task task( + [ & ]( CancellationToken ) + { + executed = true; + } ); + + task.get(); + + EXPECT_TRUE( executed ); +} + +TEST( CancellationTokenTests, InitialState ) +{ + CancellationTokenSource cts; + + auto token = cts.GetToken(); + + EXPECT_TRUE( token.CanBeCanceled() ); + EXPECT_FALSE( token.IsCancellationRequested() ); +} + +TEST( CancellationTokenTests, CancelSetsState ) +{ + CancellationTokenSource cts; + + auto token = cts.GetToken(); + + cts.Cancel(); + + EXPECT_TRUE( token.IsCancellationRequested() ); +} + +TEST( CancellationTokenTests, MultipleTokensShareState ) +{ + CancellationTokenSource cts; + + auto token1 = cts.GetToken(); + auto token2 = cts.GetToken(); + + cts.Cancel(); + + EXPECT_TRUE( token1.IsCancellationRequested() ); + EXPECT_TRUE( token2.IsCancellationRequested() ); +} + +TEST( CancellationTokenTests, ThrowIfCancellationRequested ) +{ + CancellationTokenSource cts; + + auto token = cts.GetToken(); + + cts.Cancel(); + + EXPECT_THROW( + token.ThrowIfCancellationRequested(), + OperationCanceledException ); +} + +TEST( TaskTests, TaskCanObserveCancellation ) +{ + CancellationTokenSource cts; + + Task task( + []( CancellationToken token ) + { + while ( !token.IsCancellationRequested() ) + { + std::this_thread::yield(); + } + + return true; + }, + cts.GetToken() ); + + std::this_thread::sleep_for( + std::chrono::milliseconds( 25 ) ); + + cts.Cancel(); + + EXPECT_TRUE( task.get() ); +} + +TEST( TaskTests, TaskThrowsWhenCanceled ) +{ + CancellationTokenSource cts; + + Task task( + []( CancellationToken token ) + { + while ( true ) + { + token.ThrowIfCancellationRequested(); + + std::this_thread::yield(); + } + }, + cts.GetToken() ); + + std::this_thread::sleep_for( + std::chrono::milliseconds( 25 ) ); + + cts.Cancel(); + + EXPECT_THROW( + task.get(), + OperationCanceledException ); +} + +TEST( TaskTests, ExceptionsPropagate ) +{ + Task task( + []( CancellationToken ) + { + throw std::runtime_error( "boom" ); + return 0; + } ); + + EXPECT_THROW( + task.get(), + std::runtime_error ); +} + +TEST( TaskTests, CompletionState ) +{ + Task task( + []( CancellationToken ) + { + return 123; + } ); + + EXPECT_FALSE( task.IsCompleted() ); + + EXPECT_EQ( task.get(), 123 ); + + EXPECT_TRUE( task.IsCompleted() ); +} \ No newline at end of file