├── .clang-format ├── .github └── workflows │ └── gcl-tests.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE.md ├── README.md ├── conanfile.py ├── include └── gcl.h ├── src └── gcl.cpp └── test ├── catch_amalgamated.cpp ├── catch_amalgamated.hpp ├── example.cpp ├── example.png └── test.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Microsoft 2 | AccessModifierOffset: -4 3 | AlignOperands: false 4 | AlignTrailingComments: false 5 | AllowAllParametersOfDeclarationOnNextLine: false 6 | AllowShortBlocksOnASingleLine: false 7 | AllowShortCaseLabelsOnASingleLine: false 8 | AllowShortFunctionsOnASingleLine: false 9 | AllowShortLoopsOnASingleLine: false 10 | AlwaysBreakAfterReturnType: All 11 | AlwaysBreakBeforeMultilineStrings: false 12 | AlwaysBreakTemplateDeclarations: true 13 | BinPackArguments: false 14 | BinPackParameters: false 15 | BreakBeforeBraces: Allman 16 | BreakBeforeTernaryOperators: true 17 | BreakConstructorInitializersBeforeComma: true 18 | ColumnLimit: 80 19 | CommentPragmas: "\/*(.*)*\/" 20 | ConstructorInitializerIndentWidth: 4 21 | Cpp11BracedListStyle: true 22 | IndentCaseLabels: false 23 | IndentWidth: 4 24 | Language: Cpp 25 | PointerAlignment: Left 26 | SpaceAfterCStyleCast: false 27 | SpaceBeforeParens: ControlStatements 28 | SpaceInEmptyParentheses: false 29 | SpacesBeforeTrailingComments: 1 30 | SpacesInAngles: false 31 | SpacesInCStyleCastParentheses: false 32 | SpacesInContainerLiterals: false 33 | SpacesInParentheses: false 34 | SpacesInSquareBrackets: false 35 | Standard: Cpp11 36 | UseTab: Never 37 | -------------------------------------------------------------------------------- /.github/workflows/gcl-tests.yml: -------------------------------------------------------------------------------- 1 | name: gcl 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | jobs: 8 | tests: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | config: [Debug, Release] 13 | asan: [ON, OFF] 14 | tsan: [ON, OFF] 15 | exclude: 16 | - asan: ON 17 | tsan: ON 18 | - asan: ON 19 | os: windows-latest 20 | - tsan: ON 21 | os: windows-latest 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: checkout repo 25 | uses: actions/checkout@v2 26 | - name: use msvc 27 | uses: ilammy/msvc-dev-cmd@v1 28 | - name: run tests 29 | run: | 30 | cmake -DCMAKE_BUILD_TYPE=${{ matrix.config }} -Dgcl_build_tests=ON -Dgcl_enable_asan=${{ matrix.asan }} -Dgcl_enable_tsan=${{ matrix.tsan }} . 31 | cmake --build . -j 4 32 | ctest --verbose 33 | - name: conan create 34 | run: | 35 | pip3 install conan==1.57.0 36 | conan create . -s build_type=${{ matrix.config }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | .project 3 | .cproject 4 | *~ 5 | /Debug 6 | /Release 7 | *.dot 8 | *.debug 9 | *.release 10 | *.dSYM 11 | CMakeCache.txt 12 | /CMakeFiles 13 | Makefile 14 | cmake_install.cmake 15 | /build 16 | /check_build* 17 | CMakeLists.txt.user 18 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project(gcl) 3 | 4 | option(gcl_build_tests "Build gcl tests." OFF) 5 | option(gcl_enable_asan "Build gcl with address sanitizer." OFF) 6 | option(gcl_enable_tsan "Build gcl with thread sanitizer." OFF) 7 | option(gcl_enable_coverage "Build gcl with coverage reporting." OFF) 8 | 9 | if(gcl_enable_asan AND gcl_enable_tsan) 10 | message(FATAL_ERROR "gcl_enable_asan and gcl_enable_tsan cannot both be ON") 11 | endif() 12 | 13 | function(gcl_add_flags target) 14 | set_property(TARGET ${target} PROPERTY CXX_STANDARD 14) 15 | set_property(TARGET ${target} PROPERTY CXX_EXTENSIONS OFF) 16 | target_include_directories(${target} PRIVATE include) 17 | if(MSVC) 18 | target_compile_options(${target} PRIVATE /W4 /bigobj /EHsc /wd4503 /wd4996 /wd4702 /wd4100 /wd4706) 19 | if(${MSVC_VERSION} GREATER_EQUAL 1929) 20 | if(gcl_enable_asan) 21 | target_compile_options(${target} PRIVATE /fsanitize=address) 22 | endif() 23 | endif() 24 | else() 25 | target_compile_options(${target} PRIVATE -Wall -Wconversion -Wextra -Wpedantic) 26 | target_link_libraries(${target} ${CMAKE_THREAD_LIBS_INIT}) 27 | if(CMAKE_COMPILER_IS_GNUCC) 28 | target_compile_options(${target} PRIVATE -pthread) 29 | endif() 30 | if(gcl_enable_asan) 31 | if(APPLE) 32 | target_compile_options(${target} PRIVATE -fsanitize=address,undefined) 33 | set_target_properties(${target} PROPERTIES LINK_FLAGS "-fsanitize=address,undefined") 34 | else() 35 | target_compile_options(${target} PRIVATE -fsanitize=address,leak,undefined) 36 | set_target_properties(${target} PROPERTIES LINK_FLAGS "-fsanitize=address,leak,undefined") 37 | endif() 38 | endif() 39 | if(gcl_enable_tsan) 40 | target_compile_options(${target} PRIVATE -fsanitize=thread) 41 | set_target_properties(${target} PROPERTIES LINK_FLAGS "-fsanitize=thread") 42 | endif() 43 | if(gcl_enable_coverage) 44 | target_compile_options(${target} PRIVATE --coverage) 45 | set_target_properties(${target} PROPERTIES LINK_FLAGS "--coverage") 46 | endif() 47 | endif() 48 | endfunction() 49 | 50 | find_package(Threads) 51 | 52 | add_library(gcl include/gcl.h src/gcl.cpp) 53 | gcl_add_flags(gcl) 54 | 55 | set_target_properties(gcl PROPERTIES PUBLIC_HEADER include/gcl.h) 56 | install(TARGETS gcl ARCHIVE DESTINATION lib LIBRARY DESTINATION lib PUBLIC_HEADER DESTINATION include) 57 | 58 | if(gcl_build_tests) 59 | enable_testing() 60 | add_executable(gcl_test include/gcl.h test/catch_amalgamated.hpp test/catch_amalgamated.cpp test/test.cpp) 61 | gcl_add_flags(gcl_test) 62 | target_link_libraries(gcl_test gcl) 63 | add_executable(gcl_example include/gcl.h test/example.cpp) 64 | gcl_add_flags(gcl_example) 65 | target_link_libraries(gcl_example gcl) 66 | add_test(gcl_test gcl_test) 67 | endif() 68 | 69 | set(gcl_source_files 70 | ${PROJECT_SOURCE_DIR}/include/gcl.h 71 | ${PROJECT_SOURCE_DIR}/src/gcl.cpp 72 | ${PROJECT_SOURCE_DIR}/test/test.cpp) 73 | 74 | add_custom_target( 75 | gcl_format 76 | COMMAND clang-format 77 | -style=file 78 | -i 79 | ${gcl_source_files} 80 | ) 81 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christian Blume 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcl 2 | 3 | [![Actions](https://github.com/bloomen/gcl/actions/workflows/gcl-tests.yml/badge.svg?branch=master)](https://github.com/bloomen/gcl/actions/workflows/gcl-tests.yml?query=branch%3Amaster) 4 | 5 | A **g**raph **c**oncurrent **l**ibrary for C++ that allows you to build a 6 | computational graph with an efficient interface and a small memory footprint. 7 | Requires a C++14 compliant compiler. Tested with Clang, GCC, and Visual Studio. 8 | 9 | Sample usage: 10 | ```cpp 11 | #include 12 | #include 13 | #include 14 | 15 | int main() { 16 | gcl::MetaData meta; // To collect graph metadata. Completely optional. 17 | auto t1 = gcl::task([]{ return 42.0; }).md(meta, "parent1"); 18 | auto t2 = gcl::task([]{ return 13.3; }).md(meta, "parent2"); 19 | auto t3 = gcl::tie(t1, t2).then([](auto t1, auto t2){ 20 | return *t1.get() + *t2.get(); }).md(meta, "child"); 21 | gcl::Async async{4}; 22 | t3.schedule_all(async); // Running the tasks using 4 threads 23 | t3.wait(); 24 | std::cout << *t3.get() << std::endl; // 55.3 25 | std::ofstream{"example.dot"} << gcl::to_dot(t3.edges(), meta); // output in dot notation 26 | } 27 | ``` 28 | 29 | The resulting graph looks like this: 30 | 31 | ![graph](https://raw.githubusercontent.com/bloomen/gcl/master/test/example.png) 32 | 33 | A bubble represents a task and each arrow is an edge between two tasks. 34 | The first line within a bubble is the unique task ID, followed by the task name, 35 | and finally the task instance number. 36 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from conans import ConanFile 2 | from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout 3 | 4 | class GclConan(ConanFile): 5 | name = "gcl" 6 | version = "1.1.0" 7 | license = "MIT" 8 | url = "https://github.com/bloomen/gcl" 9 | description = "Conan package for bloomen/gcl." 10 | settings = "os", "compiler", "build_type", "arch" 11 | options = {"shared": [True, False], "fPIC": [True, False]} 12 | default_options = {"shared": False, "fPIC": True} 13 | exports_sources = "CMakeLists.txt", "include/*", "src/*" 14 | 15 | def config_options(self): 16 | if self.settings.os == "Windows": 17 | del self.options.fPIC 18 | 19 | def layout(self): 20 | cmake_layout(self) 21 | 22 | def generate(self): 23 | tc = CMakeToolchain(self) 24 | tc.generate() 25 | 26 | def build(self): 27 | cmake = CMake(self) 28 | cmake.configure() 29 | cmake.build() 30 | 31 | def package(self): 32 | cmake = CMake(self) 33 | cmake.install() 34 | 35 | def package_info(self): 36 | self.cpp_info.libs = ["gcl"] 37 | -------------------------------------------------------------------------------- /include/gcl.h: -------------------------------------------------------------------------------- 1 | // gcl is a tiny graph concurrent library for C++ 2 | // Repo: https://github.com/bloomen/gcl 3 | // Author: Christian Blume 4 | // License: MIT http://www.opensource.org/licenses/mit-license.php 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #ifndef GCL_ASSERT 22 | #ifdef NDEBUG 23 | #define GCL_ASSERT(x) 24 | #else 25 | #include 26 | #define GCL_ASSERT(x) assert(x) 27 | #endif 28 | #endif 29 | 30 | namespace gcl 31 | { 32 | 33 | // Task interface used for scheduling 34 | class ITask 35 | { 36 | public: 37 | virtual ~ITask() = default; 38 | 39 | virtual void 40 | call() = 0; 41 | 42 | virtual int 43 | thread_affinity() const = 0; 44 | 45 | virtual const std::vector& 46 | children() const = 0; 47 | 48 | virtual bool 49 | set_parent_finished() = 0; 50 | 51 | virtual bool 52 | set_finished() = 0; 53 | 54 | virtual ITask*& 55 | next() = 0; 56 | 57 | virtual ITask*& 58 | previous() = 0; 59 | }; 60 | 61 | // Executor interface 62 | class Exec 63 | { 64 | public: 65 | virtual ~Exec() = default; 66 | 67 | virtual void 68 | set_active(bool active) = 0; 69 | 70 | virtual std::size_t 71 | n_threads() const = 0; 72 | 73 | virtual void 74 | execute(ITask& root) = 0; // Only called with tasks at the top of the graph 75 | // (no parents) and only when n_threads > 0. 76 | }; 77 | 78 | // Config struct for the Async class 79 | struct AsyncConfig 80 | { 81 | std::uint_fast64_t scheduler_random_seed = 82 | 0; // to select a random processor for a new task to run 83 | std::function on_processor_thread_started; 84 | std::function on_scheduler_thread_started; 85 | 86 | enum class QueueType 87 | { 88 | Mutex, // Use std::mutex for work queue synchronization + std::condition_variable for wait/notify 89 | Spin // Use a spin lock for work queue synchronization + busy wait with 90 | // optional interval sleep 91 | }; 92 | 93 | QueueType queue_type = QueueType::Mutex; 94 | 95 | // Below are only used for QueueType::Spin 96 | struct 97 | { 98 | bool active = false; // Whether we're in active mode which skips 99 | // interval sleeping, i.e. full busy waits 100 | bool spin_lock_yields = true; 101 | std::chrono::microseconds processor_sleep_interval{100}; 102 | std::chrono::microseconds scheduler_sleep_interval{100}; 103 | } spin_config; 104 | }; 105 | 106 | // Async executor for asynchronous execution. 107 | // It works such that it runs a task as soon as the task's parents finish. 108 | // Async consists of a scheduler thread which manages when a task should run 109 | // and `n_threads` processor threads which actually run the tasks 110 | class Async : public Exec 111 | { 112 | public: 113 | // Spawns a scheduler thread and `n_threads` processor threads. 114 | explicit Async(std::size_t n_threads = 0, gcl::AsyncConfig config = {}); 115 | 116 | Async(const Async&) = delete; 117 | Async& 118 | operator=(const Async&) = delete; 119 | 120 | Async(Async&&) noexcept = default; 121 | Async& 122 | operator=(Async&&) noexcept = default; 123 | 124 | ~Async(); 125 | 126 | void 127 | set_active(bool active) override; // Only relevant for QueueType::Spin 128 | 129 | std::size_t 130 | n_threads() const override; 131 | 132 | void 133 | execute(ITask& root) override; 134 | 135 | private: 136 | struct Impl; 137 | std::unique_ptr m_impl; 138 | }; 139 | 140 | // The unique id of a task 141 | using TaskId = std::size_t; 142 | 143 | // An edge between tasks 144 | struct Edge 145 | { 146 | gcl::TaskId parent; 147 | gcl::TaskId child; 148 | }; 149 | 150 | // A metadata store to collect graph info 151 | class MetaData 152 | { 153 | public: 154 | struct TaskData 155 | { 156 | std::size_t instance_no; 157 | std::string name; 158 | }; 159 | 160 | void 161 | add(gcl::TaskId id, std::string name); 162 | 163 | const TaskData* 164 | get(gcl::TaskId id) const; 165 | 166 | private: 167 | std::size_t m_counter = 0; 168 | std::unordered_map m_map; 169 | }; 170 | 171 | // Generates a description of the graph in dot format 172 | std::string 173 | to_dot(const std::vector& edges, const gcl::MetaData& meta = {}); 174 | 175 | namespace detail 176 | { 177 | 178 | struct CollectParents; 179 | 180 | template 181 | class BaseTask 182 | { 183 | public: 184 | using result_type = Result; 185 | 186 | // Creates a child to this task (continuation) 187 | template 188 | auto 189 | then(Functor&& functor) const&; 190 | 191 | // Creates a child to this task (continuation) 192 | template 193 | auto 194 | then(Functor&& functor) &&; 195 | 196 | // Specifies a particular thread the task should run on. 197 | // If not specified then it'll run on a randomly selected thread 198 | void 199 | set_thread_affinity(std::size_t thread_index); 200 | 201 | // Schedules this task and its parents for execution. Returns true if 202 | // successfully scheduled and false if already scheduled and not finished 203 | bool 204 | schedule_all(gcl::Exec& exec); 205 | 206 | // Runs this task and its parents synchronously on the current thread. 207 | // Returns true if successfully scheduled and false if already scheduled and 208 | // not finished 209 | bool 210 | schedule_all(); 211 | 212 | // Returns true if this task is currently being scheduled 213 | bool 214 | is_scheduled() const; 215 | 216 | // Returns true if this task has a result 217 | bool 218 | has_result() const; 219 | 220 | // Waits for this task to finish 221 | void 222 | wait() const; 223 | 224 | // Sets whether this task's parents' results should be automatically 225 | // released 226 | void 227 | set_auto_release_parents(bool auto_release); 228 | 229 | // Sets whether this task's result should be automatically released 230 | void 231 | set_auto_release(bool auto_release); 232 | 233 | // Releases this task's parents' results. Returns true if successfully 234 | // released and false if currently scheduled and not finished 235 | bool 236 | release_parents(); 237 | 238 | // Releases this task's result. Returns true if successfully released 239 | // and false if currently scheduled and not finished 240 | bool 241 | release(); 242 | 243 | // Returns the unique id of the task 244 | gcl::TaskId 245 | id() const; 246 | 247 | // Returns the edges between this task and all its parent tasks 248 | std::vector 249 | edges() const; 250 | 251 | protected: 252 | friend struct CollectParents; 253 | 254 | BaseTask() = default; 255 | 256 | template 257 | void 258 | init(Functor&& functor, Parents&&... parents); 259 | 260 | struct Impl; 261 | std::shared_ptr m_impl; 262 | }; 263 | 264 | } // namespace detail 265 | 266 | // The task type for general result types 267 | template 268 | class Task : public gcl::detail::BaseTask 269 | { 270 | public: 271 | // Collects metadata for this task; currently instance counter and optional 272 | // name 273 | Task 274 | md(gcl::MetaData& meta, std::string name = {}) const; 275 | 276 | // Returns the task's result if the task finished (has_result() == true), 277 | // null otherwise. Re-throws any exception thrown from running this task or 278 | // any of its parents. 279 | const Result* 280 | get() const; 281 | 282 | template 283 | static Task 284 | create(Functor&& functor, Parents&&... parents) 285 | { 286 | Task task; 287 | task.init(std::forward(functor), 288 | std::forward(parents)...); 289 | return task; 290 | } 291 | 292 | private: 293 | Task() = default; 294 | }; 295 | 296 | // The task type for reference result types 297 | template 298 | class Task : public gcl::detail::BaseTask 299 | { 300 | public: 301 | // Collects metadata for this task; currently instance counter and optional 302 | // name 303 | Task 304 | md(gcl::MetaData& meta, std::string name = {}) const; 305 | 306 | // Returns the task's result if the task finished (has_result() == true), 307 | // null otherwise. Re-throws any exception thrown from running this task or 308 | // any of its parents. 309 | Result* 310 | get() const; 311 | 312 | template 313 | static Task 314 | create(Functor&& functor, Parents&&... parents) 315 | { 316 | Task task; 317 | task.init(std::forward(functor), 318 | std::forward(parents)...); 319 | return task; 320 | } 321 | 322 | private: 323 | Task() = default; 324 | }; 325 | 326 | // The task type for void result 327 | template <> 328 | class Task : public gcl::detail::BaseTask 329 | { 330 | public: 331 | // Collects metadata for this task; currently instance counter and optional 332 | // name 333 | Task 334 | md(gcl::MetaData& meta, std::string name = {}) const; 335 | 336 | // Returns true if the task finished (has_result() == true), false 337 | // otherwise. Re-throws any exception thrown from running this task or any 338 | // of its parents. 339 | bool 340 | get() const; 341 | 342 | template 343 | static Task 344 | create(Functor&& functor, Parents&&... parents) 345 | { 346 | Task task; 347 | task.init(std::forward(functor), 348 | std::forward(parents)...); 349 | return task; 350 | } 351 | 352 | private: 353 | Task() = default; 354 | }; 355 | 356 | // Vector to hold multiple tasks of the same type 357 | template 358 | using Vec = std::vector>; 359 | 360 | // Creates a new task 361 | template 362 | auto 363 | task(Functor&& functor) 364 | { 365 | return gcl::Task::create( 366 | std::forward(functor)); 367 | } 368 | 369 | // Creates a new vector of tasks of the same type 370 | template 371 | auto 372 | vec(Tasks&&... tasks) 373 | { 374 | static_assert(sizeof...(tasks) > 0, "Need to provide at least one task"); 375 | using ResultType = std::tuple_element_t< 376 | 0, 377 | std::tuple::result_type...>>; 378 | return gcl::Vec{std::forward(tasks)...}; 379 | } 380 | 381 | namespace detail 382 | { 383 | 384 | template 385 | decltype(auto) 386 | call_impl(Functor&& f, Tuple&& t, std::index_sequence) 387 | { 388 | return std::forward(f)(std::get(std::forward(t))...); 389 | } 390 | 391 | template 392 | decltype(auto) 393 | call(Functor&& f, Tuple&& t) 394 | { 395 | return call_impl( 396 | std::forward(f), 397 | std::forward(t), 398 | std::make_index_sequence< 399 | std::tuple_size>::value>{}); 400 | } 401 | 402 | template 403 | void 404 | for_each_impl(const F&) 405 | { 406 | } 407 | 408 | template 409 | void 410 | for_each_impl(const F& f, const gcl::Task& t, Tasks&&... tasks) 411 | { 412 | f(t); 413 | for_each_impl(f, std::forward(tasks)...); 414 | } 415 | 416 | template 417 | void 418 | for_each_impl(const F& f, gcl::Task&& t, Tasks&&... tasks) 419 | { 420 | f(std::move(t)); 421 | for_each_impl(f, std::forward(tasks)...); 422 | } 423 | 424 | template 425 | void 426 | for_each_impl(const F& f, const gcl::Vec& ts, Tasks&&... tasks) 427 | { 428 | for (const gcl::Task& t : ts) 429 | { 430 | f(t); 431 | } 432 | for_each_impl(f, std::forward(tasks)...); 433 | } 434 | 435 | template 436 | void 437 | for_each_impl(const F& f, gcl::Vec&& ts, Tasks&&... tasks) 438 | { 439 | for (gcl::Task&& t : ts) 440 | { 441 | f(std::move(t)); 442 | } 443 | for_each_impl(f, std::forward(tasks)...); 444 | } 445 | 446 | // Applies functor `f` to each task in `tasks` which can be of type `Task` and/or `Vec` 447 | template 448 | void 449 | for_each(const Functor& f, Tasks&&... tasks) 450 | { 451 | gcl::detail::for_each_impl(f, std::forward(tasks)...); 452 | } 453 | 454 | // Unless otherwise mentioned a given method is called from the control thread 455 | class BaseImpl : public gcl::ITask 456 | { 457 | public: 458 | virtual ~BaseImpl() = default; 459 | 460 | // Called from scheduler thread 461 | int 462 | thread_affinity() const override 463 | { 464 | return m_thread_affinity; 465 | } 466 | 467 | // Called from scheduler thread 468 | const std::vector& 469 | children() const override 470 | { 471 | return m_children; 472 | } 473 | 474 | // Called from scheduler thread 475 | bool 476 | set_parent_finished() override 477 | { 478 | return ++m_parents_ready == m_parents.size(); 479 | } 480 | 481 | // Called from scheduler thread 482 | bool 483 | set_finished() override 484 | { 485 | for (const auto parent : parents()) 486 | { 487 | if (parent->set_child_finished()) 488 | { 489 | parent->auto_release_if(); 490 | } 491 | } 492 | if (m_children.empty()) 493 | { 494 | auto_release_if(); 495 | } 496 | const bool has_children = !m_children.empty(); 497 | m_promise.set_value(); 498 | m_scheduled = false; 499 | return has_children; 500 | } 501 | 502 | // Called from scheduler and processor threads but never simultaneously 503 | gcl::ITask*& 504 | next() override 505 | { 506 | return m_next; 507 | } 508 | 509 | // Called from scheduler and processor threads but never simultaneously 510 | gcl::ITask*& 511 | previous() override 512 | { 513 | return m_previous; 514 | } 515 | 516 | virtual void 517 | prepare() = 0; 518 | 519 | virtual void 520 | release() = 0; 521 | 522 | void 523 | set_thread_affinity(const int affinity) 524 | { 525 | m_thread_affinity = affinity; 526 | } 527 | 528 | void 529 | set_auto_release(const bool auto_release) 530 | { 531 | m_auto_release = auto_release; 532 | } 533 | 534 | template 535 | void 536 | visit(const Visitor& visitor) 537 | { 538 | if (!m_task_cache) 539 | { 540 | m_task_cache = std::make_unique>(); 541 | m_task_cache->emplace_back(this); 542 | std::queue q; 543 | q.emplace(this); 544 | while (!q.empty()) 545 | { 546 | const auto v = q.front(); 547 | q.pop(); 548 | for (const auto w : v->m_parents) 549 | { 550 | if (!w->m_visited) 551 | { 552 | q.emplace(w); 553 | w->m_visited = true; 554 | m_task_cache->emplace_back(w); 555 | } 556 | } 557 | } 558 | for (const auto task : *m_task_cache) 559 | { 560 | task->m_visited = false; 561 | } 562 | } 563 | for (auto task = m_task_cache->rbegin(); task != m_task_cache->rend(); 564 | ++task) 565 | { 566 | visitor(**task); 567 | } 568 | } 569 | 570 | void 571 | wait() const 572 | { 573 | m_future.wait(); 574 | while (is_scheduled()) 575 | ; 576 | } 577 | 578 | bool 579 | is_scheduled() const 580 | { 581 | return m_scheduled; 582 | } 583 | 584 | void 585 | add_child(BaseImpl& child) 586 | { 587 | m_children.emplace_back(&child); 588 | m_task_cache.reset(); 589 | } 590 | 591 | void 592 | add_parent(BaseImpl& parent) 593 | { 594 | m_parents.emplace_back(&parent); 595 | parent.add_child(*this); 596 | } 597 | 598 | // Called from scheduler thread 599 | const std::vector& 600 | parents() const 601 | { 602 | return m_parents; 603 | } 604 | 605 | // Called from scheduler thread 606 | bool 607 | set_child_finished() 608 | { 609 | return ++m_children_ready == m_children.size(); 610 | } 611 | 612 | // Called from scheduler thread 613 | void 614 | auto_release_if() 615 | { 616 | if (m_auto_release) 617 | { 618 | release(); 619 | } 620 | } 621 | 622 | gcl::TaskId 623 | id() const 624 | { 625 | return std::hash{}(this); 626 | } 627 | 628 | std::vector 629 | edges() 630 | { 631 | std::vector es; 632 | visit([&es](BaseImpl& i) { 633 | for (const auto p : i.m_parents) 634 | { 635 | es.push_back({p->id(), i.id()}); 636 | } 637 | }); 638 | return es; 639 | } 640 | 641 | protected: 642 | BaseImpl() = default; 643 | 644 | std::promise m_promise; 645 | std::future m_future; 646 | int m_thread_affinity = -1; 647 | std::atomic m_auto_release{false}; 648 | std::atomic m_scheduled{false}; 649 | bool m_visited = false; 650 | std::unique_ptr> m_task_cache; 651 | std::vector m_parents; 652 | std::vector m_children; 653 | std::uint32_t m_parents_ready = 0; 654 | std::uint32_t m_children_ready = 0; 655 | gcl::ITask* m_next = nullptr; 656 | gcl::ITask* m_previous = nullptr; 657 | }; 658 | 659 | struct CollectParents 660 | { 661 | gcl::detail::BaseImpl* impl; 662 | template 663 | void 664 | operator()(const Parent& parent) const 665 | { 666 | impl->add_parent(*parent.m_impl); 667 | } 668 | }; 669 | 670 | template 671 | class ChannelElement 672 | { 673 | public: 674 | ChannelElement(Result&& value) 675 | : m_value{std::move(value)} 676 | , m_has_value{true} 677 | { 678 | } 679 | ChannelElement(std::exception_ptr&& exception) 680 | : m_exception{std::move(exception)} 681 | { 682 | } 683 | 684 | ChannelElement(const ChannelElement&) = delete; 685 | ChannelElement& 686 | operator=(const ChannelElement&) = delete; 687 | 688 | ChannelElement(ChannelElement&& other) noexcept 689 | { 690 | m_has_value = other.m_has_value; 691 | if (m_has_value) 692 | { 693 | new (&m_value) Result{std::move(other.m_value)}; 694 | } 695 | else 696 | { 697 | new (&m_exception) std::exception_ptr{std::move(other.m_exception)}; 698 | } 699 | } 700 | 701 | ChannelElement& 702 | operator=(ChannelElement&&) = delete; 703 | 704 | bool 705 | has_value() const 706 | { 707 | return m_has_value; 708 | } 709 | 710 | const Result* 711 | value() const 712 | { 713 | return &m_value; 714 | } 715 | 716 | const std::exception_ptr& 717 | exception() const 718 | { 719 | return m_exception; 720 | } 721 | 722 | ~ChannelElement() 723 | { 724 | if (m_has_value) 725 | { 726 | m_value.~Result(); 727 | } 728 | else 729 | { 730 | m_exception.~exception_ptr(); 731 | } 732 | } 733 | 734 | private: 735 | union 736 | { 737 | Result m_value; 738 | std::exception_ptr m_exception; 739 | }; 740 | bool m_has_value = false; 741 | }; 742 | 743 | template 744 | class ChannelElement 745 | { 746 | public: 747 | ChannelElement(Result& value) 748 | : m_value{&value} 749 | , m_has_value{true} 750 | { 751 | } 752 | ChannelElement(std::exception_ptr&& exception) 753 | : m_exception{std::move(exception)} 754 | { 755 | } 756 | 757 | ChannelElement(const ChannelElement&) = delete; 758 | ChannelElement& 759 | operator=(const ChannelElement&) = delete; 760 | 761 | ChannelElement(ChannelElement&& other) noexcept 762 | { 763 | m_has_value = other.m_has_value; 764 | if (m_has_value) 765 | { 766 | new (&m_value) Result* {std::move(other.m_value)}; 767 | } 768 | else 769 | { 770 | new (&m_exception) std::exception_ptr{std::move(other.m_exception)}; 771 | } 772 | } 773 | 774 | ChannelElement& 775 | operator=(ChannelElement&&) = delete; 776 | 777 | bool 778 | has_value() const 779 | { 780 | return m_has_value; 781 | } 782 | 783 | Result* 784 | value() const 785 | { 786 | return m_value; 787 | } 788 | 789 | const std::exception_ptr& 790 | exception() const 791 | { 792 | return m_exception; 793 | } 794 | 795 | ~ChannelElement() 796 | { 797 | if (!m_has_value) 798 | { 799 | m_exception.~exception_ptr(); 800 | } 801 | } 802 | 803 | private: 804 | union 805 | { 806 | Result* m_value; 807 | std::exception_ptr m_exception; 808 | }; 809 | bool m_has_value = false; 810 | }; 811 | 812 | template <> 813 | class ChannelElement 814 | { 815 | public: 816 | ChannelElement() 817 | { 818 | } 819 | ChannelElement(std::exception_ptr&& exception) 820 | : m_exception{std::move(exception)} 821 | { 822 | } 823 | 824 | ChannelElement(const ChannelElement&) = delete; 825 | ChannelElement& 826 | operator=(const ChannelElement&) = delete; 827 | 828 | ChannelElement(ChannelElement&&) noexcept = default; 829 | ChannelElement& 830 | operator=(ChannelElement&&) = delete; 831 | 832 | bool 833 | has_value() const 834 | { 835 | return !m_exception; 836 | } 837 | 838 | const std::exception_ptr& 839 | exception() const 840 | { 841 | return m_exception; 842 | } 843 | 844 | private: 845 | std::exception_ptr m_exception; 846 | }; 847 | 848 | // Unless otherwise mentioned a given method is called from the control thread 849 | template 850 | class Channel 851 | { 852 | public: 853 | Channel() = default; 854 | 855 | Channel(const Channel&) = delete; 856 | Channel& 857 | operator=(const Channel&) = delete; 858 | 859 | ~Channel() 860 | { 861 | reset(); 862 | } 863 | 864 | void 865 | set_future(const std::future& future) 866 | { 867 | GCL_ASSERT(future.valid()); 868 | m_future = &future; 869 | } 870 | 871 | // Called from a processor thread 872 | void 873 | set(gcl::detail::ChannelElement&& element) 874 | { 875 | new (m_storage) gcl::detail::ChannelElement{std::move(element)}; 876 | } 877 | 878 | const gcl::detail::ChannelElement* 879 | get() const 880 | { 881 | if (!m_future || 882 | m_future.load()->wait_for(std::chrono::seconds{0}) != 883 | std::future_status::ready) 884 | { 885 | return nullptr; 886 | } 887 | return reinterpret_cast*>( 888 | m_storage); 889 | } 890 | 891 | // Called from either control or scheduler thread 892 | void 893 | reset() 894 | { 895 | if (const auto element = get()) 896 | { 897 | element->~ChannelElement(); 898 | } 899 | m_future = nullptr; 900 | } 901 | 902 | private: 903 | std::atomic*> m_future{nullptr}; 904 | char m_storage[sizeof(gcl::detail::ChannelElement)]; 905 | }; 906 | 907 | template 908 | class Binding 909 | { 910 | public: 911 | virtual ~Binding() = default; 912 | virtual Result 913 | evaluate() = 0; 914 | }; 915 | 916 | template 917 | class BindingImpl : public gcl::detail::Binding 918 | { 919 | public: 920 | template 921 | explicit BindingImpl(F&& functor, P&&... parents) 922 | : m_functor{std::forward(functor)} 923 | , m_parents{std::make_tuple(std::forward

(parents)...)} 924 | { 925 | } 926 | 927 | BindingImpl(const BindingImpl&) = delete; 928 | BindingImpl& 929 | operator=(const BindingImpl&) = delete; 930 | 931 | Result 932 | evaluate() override 933 | { 934 | return gcl::detail::call( 935 | [this](auto&&... p) -> Result { 936 | #ifndef NDEBUG 937 | gcl::detail::for_each( 938 | [](const auto& p) { GCL_ASSERT(p.has_result()); }, p...); 939 | #endif 940 | return m_functor(std::forward(p)...); 941 | }, 942 | m_parents); 943 | } 944 | 945 | private: 946 | std::remove_reference_t m_functor; 947 | std::tuple...> m_parents; 948 | }; 949 | 950 | template 951 | struct Evaluate 952 | { 953 | void 954 | operator()(gcl::detail::Channel& channel, 955 | gcl::detail::Binding& binding) const 956 | { 957 | try 958 | { 959 | channel.set(binding.evaluate()); 960 | } 961 | catch (...) 962 | { 963 | channel.set(std::current_exception()); 964 | } 965 | } 966 | }; 967 | 968 | template <> 969 | struct Evaluate 970 | { 971 | void 972 | operator()(gcl::detail::Channel& channel, 973 | gcl::detail::Binding& binding) const 974 | { 975 | try 976 | { 977 | binding.evaluate(); 978 | channel.set({}); 979 | } 980 | catch (...) 981 | { 982 | channel.set(std::current_exception()); 983 | } 984 | } 985 | }; 986 | 987 | // Unless otherwise mentioned a given method is called from the control thread 988 | template 989 | struct BaseTask::Impl : BaseImpl 990 | { 991 | template 992 | explicit Impl(Functor&& functor, Parents&&... parents) 993 | { 994 | gcl::detail::for_each(gcl::detail::CollectParents{this}, parents...); 995 | m_binding = std::make_unique< 996 | gcl::detail::BindingImpl>( 997 | std::forward(functor), std::forward(parents)...); 998 | m_future = m_promise.get_future(); 999 | } 1000 | 1001 | void 1002 | prepare() override 1003 | { 1004 | m_parents_ready = 0; 1005 | m_children_ready = 0; 1006 | m_promise = {}; 1007 | m_future = m_promise.get_future(); 1008 | m_scheduled = true; 1009 | m_channel.reset(); 1010 | m_channel.set_future(m_future); 1011 | } 1012 | 1013 | // Called from a processor thread 1014 | void 1015 | call() override 1016 | { 1017 | gcl::detail::Evaluate{}(m_channel, *m_binding); 1018 | } 1019 | 1020 | // Called from either control or scheduler thread 1021 | void 1022 | release() override 1023 | { 1024 | m_channel.reset(); 1025 | } 1026 | 1027 | const ChannelElement* 1028 | channel_element() const 1029 | { 1030 | if (m_scheduled) 1031 | { 1032 | return nullptr; 1033 | } 1034 | return m_channel.get(); 1035 | } 1036 | 1037 | private: 1038 | std::unique_ptr> m_binding; 1039 | Channel m_channel; 1040 | }; 1041 | 1042 | template 1043 | template 1044 | auto 1045 | BaseTask::then(Functor&& functor) const& 1046 | { 1047 | return gcl::Task&>( 1048 | *this)))>::create(std::forward(functor), 1049 | static_cast&>(*this)); 1050 | } 1051 | 1052 | template 1053 | template 1054 | auto 1055 | BaseTask::then(Functor&& functor) && 1056 | { 1057 | return gcl::Task&&>( 1058 | *this)))>::create(std::forward(functor), 1059 | static_cast&&>(*this)); 1060 | } 1061 | 1062 | template 1063 | template 1064 | void 1065 | BaseTask::init(Functor&& functor, Parents&&... parents) 1066 | { 1067 | m_impl = std::make_shared(std::forward(functor), 1068 | std::forward(parents)...); 1069 | } 1070 | 1071 | template 1072 | void 1073 | BaseTask::set_thread_affinity(const std::size_t thread_index) 1074 | { 1075 | m_impl->set_thread_affinity(static_cast(thread_index)); 1076 | } 1077 | 1078 | template 1079 | bool 1080 | BaseTask::schedule_all(gcl::Exec& exec) 1081 | { 1082 | if (is_scheduled()) 1083 | { 1084 | return false; 1085 | } 1086 | if (exec.n_threads() == 0) 1087 | { 1088 | return schedule_all(); 1089 | } 1090 | std::vector roots; 1091 | m_impl->visit([&roots](BaseImpl& i) { 1092 | i.prepare(); 1093 | if (i.parents().empty()) 1094 | { 1095 | roots.emplace_back(&i); 1096 | } 1097 | }); 1098 | for (const auto root : roots) 1099 | { 1100 | exec.execute(*root); 1101 | } 1102 | return true; 1103 | } 1104 | 1105 | template 1106 | bool 1107 | BaseTask::schedule_all() 1108 | { 1109 | if (is_scheduled()) 1110 | { 1111 | return false; 1112 | } 1113 | m_impl->visit([](BaseImpl& i) { 1114 | i.prepare(); 1115 | i.call(); 1116 | i.set_finished(); 1117 | }); 1118 | return true; 1119 | } 1120 | 1121 | template 1122 | bool 1123 | BaseTask::is_scheduled() const 1124 | { 1125 | return m_impl->is_scheduled(); 1126 | } 1127 | 1128 | template 1129 | bool 1130 | BaseTask::has_result() const 1131 | { 1132 | return m_impl->channel_element(); 1133 | } 1134 | 1135 | template 1136 | void 1137 | BaseTask::wait() const 1138 | { 1139 | m_impl->wait(); 1140 | } 1141 | 1142 | template 1143 | void 1144 | BaseTask::set_auto_release_parents(const bool auto_release) 1145 | { 1146 | const auto final = m_impl.get(); 1147 | m_impl->visit([auto_release, final](BaseImpl& i) { 1148 | if (&i != final) 1149 | { 1150 | i.set_auto_release(auto_release); 1151 | } 1152 | }); 1153 | } 1154 | 1155 | template 1156 | void 1157 | BaseTask::set_auto_release(const bool auto_release) 1158 | { 1159 | m_impl->set_auto_release(auto_release); 1160 | } 1161 | 1162 | template 1163 | bool 1164 | BaseTask::release_parents() 1165 | { 1166 | if (is_scheduled()) 1167 | { 1168 | return false; 1169 | } 1170 | const auto final = m_impl.get(); 1171 | m_impl->visit([final](BaseImpl& i) { 1172 | if (&i != final) 1173 | { 1174 | i.release(); 1175 | } 1176 | }); 1177 | return true; 1178 | } 1179 | 1180 | template 1181 | bool 1182 | BaseTask::release() 1183 | { 1184 | if (is_scheduled()) 1185 | { 1186 | return false; 1187 | } 1188 | m_impl->release(); 1189 | return true; 1190 | } 1191 | 1192 | template 1193 | gcl::TaskId 1194 | BaseTask::id() const 1195 | { 1196 | return m_impl->id(); 1197 | } 1198 | 1199 | template 1200 | std::vector 1201 | BaseTask::edges() const 1202 | { 1203 | return m_impl->edges(); 1204 | } 1205 | 1206 | } // namespace detail 1207 | 1208 | template 1209 | Task 1210 | Task::md(gcl::MetaData& meta, std::string name) const 1211 | { 1212 | meta.add(this->id(), std::move(name)); 1213 | return *this; 1214 | } 1215 | 1216 | template 1217 | const Result* 1218 | Task::get() const 1219 | { 1220 | if (const auto element = this->m_impl->channel_element()) 1221 | { 1222 | if (element->has_value()) 1223 | { 1224 | return element->value(); 1225 | } 1226 | else 1227 | { 1228 | std::rethrow_exception(element->exception()); 1229 | } 1230 | } 1231 | return nullptr; 1232 | } 1233 | 1234 | template 1235 | Task 1236 | Task::md(gcl::MetaData& meta, std::string name) const 1237 | { 1238 | meta.add(this->id(), std::move(name)); 1239 | return *this; 1240 | } 1241 | 1242 | template 1243 | Result* 1244 | Task::get() const 1245 | { 1246 | if (const auto element = this->m_impl->channel_element()) 1247 | { 1248 | if (element->has_value()) 1249 | { 1250 | return element->value(); 1251 | } 1252 | else 1253 | { 1254 | std::rethrow_exception(element->exception()); 1255 | } 1256 | } 1257 | return nullptr; 1258 | } 1259 | 1260 | inline Task 1261 | Task::md(gcl::MetaData& meta, std::string name) const 1262 | { 1263 | meta.add(this->id(), std::move(name)); 1264 | return *this; 1265 | } 1266 | 1267 | inline bool 1268 | Task::get() const 1269 | { 1270 | if (const auto element = this->m_impl->channel_element()) 1271 | { 1272 | if (!element->has_value()) 1273 | { 1274 | std::rethrow_exception(element->exception()); 1275 | } 1276 | return true; 1277 | } 1278 | return false; 1279 | } 1280 | 1281 | // Ties tasks together which can be of type `Task` and/or `Vec` 1282 | template 1283 | class Tie 1284 | { 1285 | public: 1286 | static_assert(sizeof...(Tasks) > 0, "Need to provide at least one task"); 1287 | 1288 | explicit Tie(const Tasks&... tasks) 1289 | : m_tasks{std::make_tuple(tasks...)} 1290 | { 1291 | } 1292 | 1293 | explicit Tie(Tasks&&... tasks) 1294 | : m_tasks{std::make_tuple(std::move(tasks)...)} 1295 | { 1296 | } 1297 | 1298 | // Creates a child to all tied tasks (continuation) 1299 | template 1300 | auto 1301 | then(Functor&& functor) const& 1302 | { 1303 | return then_impl(std::forward(functor), 1304 | std::index_sequence_for{}); 1305 | } 1306 | 1307 | // Creates a child to all tied tasks (continuation) 1308 | template 1309 | auto 1310 | then(Functor&& functor) && 1311 | { 1312 | return then_impl(std::forward(functor), 1313 | std::index_sequence_for{}); 1314 | } 1315 | 1316 | private: 1317 | template 1318 | auto 1319 | then_impl(Functor&& functor, std::index_sequence) const& 1320 | { 1321 | return gcl::Task(m_tasks)...))>::create( 1322 | std::forward(functor), std::get(m_tasks)...); 1323 | } 1324 | 1325 | template 1326 | auto 1327 | then_impl(Functor&& functor, std::index_sequence) && 1328 | { 1329 | return gcl::Task(m_tasks)...))>::create( 1330 | std::forward(functor), 1331 | std::get(std::move(m_tasks))...); 1332 | } 1333 | 1334 | std::tuple m_tasks; 1335 | }; 1336 | 1337 | // Ties tasks together where `tasks` can be of type `Task` and/or `Vec` 1338 | template 1339 | auto 1340 | tie(Tasks&&... tasks) 1341 | { 1342 | return gcl::Tie...>{ 1343 | std::forward(tasks)...}; 1344 | } 1345 | 1346 | // Creates a child that waits for all tasks to finish that are part of `tie` 1347 | template 1348 | gcl::Task 1349 | when(const gcl::Tie& tie) 1350 | { 1351 | return tie.then([](auto&&... ts) { 1352 | gcl::detail::for_each( 1353 | [](auto&& t) { 1354 | t.wait(); 1355 | std::forward(t).get(); 1356 | }, 1357 | std::forward(ts)...); 1358 | }); 1359 | } 1360 | 1361 | // Creates a child that waits for all tasks to finish that are part of `tie` 1362 | template 1363 | gcl::Task 1364 | when(gcl::Tie&& tie) 1365 | { 1366 | return std::move(tie).then([](auto&&... ts) { 1367 | gcl::detail::for_each( 1368 | [](auto&& t) { 1369 | t.wait(); 1370 | std::forward(t).get(); 1371 | }, 1372 | std::forward(ts)...); 1373 | }); 1374 | } 1375 | 1376 | // Creates a child that waits for all tasks to finish where `tasks` can be of type `Task` and/or `Vec` 1377 | template 1378 | gcl::Task 1379 | when(Tasks... tasks) 1380 | { 1381 | return gcl::when(gcl::tie(std::move(tasks)...)); 1382 | } 1383 | 1384 | // Used to facilitate task canceling 1385 | class CancelToken 1386 | { 1387 | public: 1388 | // Called from outside the task 1389 | void 1390 | set_canceled(const bool canceled = true) 1391 | { 1392 | m_token = canceled; 1393 | } 1394 | 1395 | // Checked from within a task's functor 1396 | bool 1397 | is_canceled() const 1398 | { 1399 | return m_token.load(); 1400 | } 1401 | 1402 | private: 1403 | std::atomic m_token{false}; 1404 | }; 1405 | 1406 | namespace detail 1407 | { 1408 | 1409 | template 1410 | struct Distance; 1411 | 1412 | template <> 1413 | struct Distance 1414 | { 1415 | template 1416 | auto 1417 | operator()(const Number1 first, const Number2 last) const 1418 | { 1419 | static_assert(std::is_integral::value && 1420 | std::is_integral::value, 1421 | "Number type must be integral"); 1422 | GCL_ASSERT(last >= first); 1423 | return last - first; 1424 | } 1425 | }; 1426 | 1427 | template <> 1428 | struct Distance 1429 | { 1430 | template 1431 | auto 1432 | operator()(const Iterator first, const Iterator last) const 1433 | { 1434 | return std::distance(first, last); 1435 | } 1436 | }; 1437 | 1438 | } // namespace detail 1439 | 1440 | // A function similar to std::for_each but returning a task for asynchronous 1441 | // execution. This function creates a graph with distance(first, last) + 1 1442 | // tasks. UB if first > last. Note that `unary_op` takes an object of type T. 1443 | template 1444 | gcl::Task 1445 | for_each(T first, const U last, UnaryOperation unary_op) 1446 | { 1447 | const auto distance = 1448 | gcl::detail::Distance::value>{}(first, last); 1449 | GCL_ASSERT(distance >= 0); 1450 | gcl::Vec tasks; 1451 | tasks.reserve(static_cast(distance)); 1452 | for (; first != last; ++first) 1453 | { 1454 | tasks.emplace_back(gcl::task([unary_op, first] { unary_op(first); })); 1455 | } 1456 | return gcl::when(std::move(tasks)); 1457 | } 1458 | 1459 | } // namespace gcl 1460 | -------------------------------------------------------------------------------- /src/gcl.cpp: -------------------------------------------------------------------------------- 1 | // gcl is a tiny graph concurrent library for C++ 2 | // Repo: https://github.com/bloomen/gcl 3 | // Author: Christian Blume 4 | // License: MIT http://www.opensource.org/licenses/mit-license.php 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace gcl 13 | { 14 | 15 | namespace 16 | { 17 | 18 | class SpinLock 19 | { 20 | public: 21 | explicit SpinLock(const bool yields) 22 | : m_yields{yields} 23 | { 24 | } 25 | 26 | void 27 | lock() noexcept 28 | { 29 | while (m_locked.test_and_set(std::memory_order_acquire)) 30 | { 31 | if (m_yields) 32 | { 33 | std::this_thread::yield(); 34 | } 35 | } 36 | } 37 | 38 | void 39 | unlock() noexcept 40 | { 41 | m_locked.clear(std::memory_order_release); 42 | } 43 | 44 | private: 45 | const bool m_yields; 46 | std::atomic_flag m_locked = ATOMIC_FLAG_INIT; 47 | }; 48 | 49 | class TaskQueue 50 | { 51 | public: 52 | TaskQueue() = default; 53 | 54 | TaskQueue(const TaskQueue&) = delete; 55 | TaskQueue& 56 | operator=(const TaskQueue&) = delete; 57 | 58 | virtual ~TaskQueue() = default; 59 | 60 | virtual void 61 | shutdown() 62 | { 63 | } 64 | 65 | virtual void 66 | yield() const 67 | { 68 | } 69 | 70 | virtual std::size_t 71 | size() const 72 | { 73 | return m_size; 74 | } 75 | 76 | virtual void 77 | push(ITask* const task) 78 | { 79 | if (m_head) 80 | { 81 | m_tail->next() = task; 82 | task->previous() = m_tail; 83 | m_tail = task; 84 | } 85 | else 86 | { 87 | m_head = task; 88 | m_tail = task; 89 | } 90 | ++m_size; 91 | } 92 | 93 | virtual ITask* 94 | pop() 95 | { 96 | ITask* task = nullptr; 97 | if (m_head) 98 | { 99 | task = m_head; 100 | if (task->next()) 101 | { 102 | m_head = task->next(); 103 | m_head->previous() = nullptr; 104 | } 105 | else 106 | { 107 | m_head = nullptr; 108 | m_tail = nullptr; 109 | } 110 | task->next() = nullptr; 111 | --m_size; 112 | } 113 | return task; 114 | } 115 | 116 | private: 117 | std::size_t m_size = 0; 118 | ITask* m_head = nullptr; 119 | ITask* m_tail = nullptr; 120 | }; 121 | 122 | // Mp-Sc queue 123 | class TaskQueueMutex : public TaskQueue 124 | { 125 | public: 126 | explicit TaskQueueMutex(const std::atomic& done) 127 | : m_done{done} 128 | { 129 | } 130 | 131 | void 132 | shutdown() override 133 | { 134 | m_cv.notify_one(); 135 | } 136 | 137 | // multiple producer 138 | void 139 | push(ITask* const task) override 140 | { 141 | std::lock_guard lock{m_mutex}; 142 | TaskQueue::push(task); 143 | m_cv.notify_one(); 144 | } 145 | 146 | // single consumer 147 | ITask* 148 | pop() override 149 | { 150 | std::unique_lock lock{m_mutex}; 151 | m_cv.wait(lock, [this] { return TaskQueue::size() > 0 || m_done; }); 152 | return TaskQueue::pop(); 153 | } 154 | 155 | private: 156 | const std::atomic& m_done; 157 | mutable std::mutex m_mutex; 158 | std::condition_variable m_cv; 159 | }; 160 | 161 | // Mp-Sc queue 162 | class TaskQueueSpin : public TaskQueue 163 | { 164 | public: 165 | explicit TaskQueueSpin(const bool yields, 166 | const std::atomic& active, 167 | const std::chrono::microseconds sleep_interval) 168 | : m_spin{yields} 169 | , m_active{active} 170 | , m_sleep_interval{sleep_interval} 171 | { 172 | } 173 | 174 | void 175 | yield() const override 176 | { 177 | if (m_sleep_interval <= std::chrono::microseconds{0}) 178 | { 179 | return; 180 | } 181 | while (!m_active && size() == 0) 182 | { 183 | std::this_thread::sleep_for(m_sleep_interval); 184 | } 185 | } 186 | 187 | std::size_t 188 | size() const override 189 | { 190 | std::lock_guard lock{m_spin}; 191 | return TaskQueue::size(); 192 | } 193 | 194 | // multiple producer 195 | void 196 | push(ITask* const task) override 197 | { 198 | std::lock_guard lock{m_spin}; 199 | TaskQueue::push(task); 200 | } 201 | 202 | // single consumer 203 | ITask* 204 | pop() override 205 | { 206 | std::lock_guard lock{m_spin}; 207 | return TaskQueue::pop(); 208 | } 209 | 210 | private: 211 | mutable SpinLock m_spin; 212 | const std::atomic& m_active; 213 | const std::chrono::microseconds m_sleep_interval; 214 | }; 215 | 216 | std::unique_ptr 217 | make_task_queue(const AsyncConfig::QueueType queue_type, 218 | std::atomic& done, 219 | const bool spin_lock_yields, 220 | const std::atomic& active, 221 | const std::chrono::microseconds sleep_interval) 222 | { 223 | switch (queue_type) 224 | { 225 | case AsyncConfig::QueueType::Mutex: 226 | return std::make_unique(done); 227 | case AsyncConfig::QueueType::Spin: 228 | return std::make_unique( 229 | spin_lock_yields, active, sleep_interval); 230 | } 231 | GCL_ASSERT(false); 232 | return nullptr; 233 | } 234 | 235 | class Worker 236 | { 237 | public: 238 | virtual ~Worker() = default; 239 | virtual void 240 | run() = 0; 241 | virtual void 242 | shutdown() = 0; 243 | }; 244 | 245 | class Thread 246 | { 247 | public: 248 | void 249 | start(Worker& worker) 250 | { 251 | GCL_ASSERT(!m_worker); 252 | m_thread = std::thread{[&worker] { worker.run(); }}; 253 | m_worker = &worker; 254 | } 255 | 256 | ~Thread() noexcept 257 | { 258 | if (m_worker) 259 | { 260 | m_worker->shutdown(); 261 | m_thread.join(); 262 | } 263 | } 264 | 265 | private: 266 | Worker* m_worker = nullptr; 267 | std::thread m_thread; 268 | }; 269 | 270 | class Processor : public Worker 271 | { 272 | public: 273 | explicit Processor(const std::size_t index, 274 | const AsyncConfig& config, 275 | TaskQueue& completed, 276 | const std::atomic& active) 277 | : m_config{config} 278 | , m_completed{completed} 279 | , m_active{active} 280 | , m_index{index} 281 | , m_scheduled{ 282 | make_task_queue(m_config.queue_type, 283 | m_done, 284 | m_config.spin_config.spin_lock_yields, 285 | m_active, 286 | m_config.spin_config.processor_sleep_interval)} 287 | { 288 | m_thread.start(*this); 289 | } 290 | 291 | void 292 | push(ITask* const task) 293 | { 294 | m_scheduled->push(task); 295 | } 296 | 297 | private: 298 | void 299 | run() override 300 | { 301 | if (m_config.on_processor_thread_started) 302 | { 303 | m_config.on_processor_thread_started(m_index); 304 | } 305 | while (!m_done) 306 | { 307 | while (const auto task = m_scheduled->pop()) 308 | { 309 | task->call(); 310 | m_completed.push(task); 311 | } 312 | m_scheduled->yield(); 313 | } 314 | } 315 | 316 | void 317 | shutdown() override 318 | { 319 | m_done = true; 320 | m_scheduled->shutdown(); 321 | } 322 | 323 | const AsyncConfig& m_config; 324 | TaskQueue& m_completed; 325 | const std::atomic& m_active; 326 | std::size_t m_index; 327 | std::atomic m_done{false}; 328 | std::unique_ptr m_scheduled; 329 | Thread m_thread; 330 | }; 331 | 332 | } // namespace 333 | 334 | struct Async::Impl : public Worker 335 | { 336 | explicit Impl(const std::size_t n_threads, AsyncConfig config) 337 | : m_config{std::move(config)} 338 | , m_active{m_config.spin_config.active} 339 | , m_completed{make_task_queue( 340 | m_config.queue_type, 341 | m_done, 342 | m_config.spin_config.spin_lock_yields, 343 | m_active, 344 | m_config.spin_config.scheduler_sleep_interval)} 345 | , m_randgen{m_config.scheduler_random_seed > 0 346 | ? m_config.scheduler_random_seed 347 | : std::random_device{}()} 348 | { 349 | if (n_threads == 0) 350 | { 351 | return; 352 | } 353 | m_thread.start(*this); 354 | m_processors.reserve(n_threads); 355 | for (std::size_t i = 0; i < n_threads; ++i) 356 | { 357 | m_processors.emplace_back(std::make_unique( 358 | i, m_config, *m_completed, m_active)); 359 | } 360 | } 361 | 362 | void 363 | set_active(const bool active) 364 | { 365 | m_active = active; 366 | } 367 | 368 | std::size_t 369 | n_threads() const 370 | { 371 | return m_processors.size(); 372 | } 373 | 374 | void 375 | execute(ITask& root) 376 | { 377 | GCL_ASSERT(n_threads() > 0); 378 | std::size_t index; 379 | if (root.thread_affinity() >= 0 && 380 | static_cast(root.thread_affinity()) < 381 | m_processors.size()) 382 | { 383 | index = static_cast(root.thread_affinity()); 384 | } 385 | else 386 | { 387 | std::uniform_int_distribution dist{ 388 | 0u, m_processors.size() - 1u}; 389 | index = dist(m_randgen); 390 | } 391 | m_processors[index]->push(&root); 392 | } 393 | 394 | private: 395 | void 396 | run() override 397 | { 398 | if (m_config.on_scheduler_thread_started) 399 | { 400 | m_config.on_scheduler_thread_started(); 401 | } 402 | while (!m_done) 403 | { 404 | while (const auto task = m_completed->pop()) 405 | { 406 | if (task->set_finished()) 407 | { 408 | for (const auto child : task->children()) 409 | { 410 | if (child->set_parent_finished()) 411 | { 412 | execute(*child); 413 | } 414 | } 415 | } 416 | } 417 | m_completed->yield(); 418 | } 419 | } 420 | 421 | void 422 | shutdown() override 423 | { 424 | m_active = true; 425 | m_done = true; 426 | m_completed->shutdown(); 427 | } 428 | 429 | AsyncConfig m_config; 430 | std::atomic m_active; 431 | std::atomic m_done{false}; 432 | std::unique_ptr m_completed; 433 | std::vector> m_processors; 434 | std::mt19937_64 m_randgen; 435 | Thread m_thread; 436 | }; 437 | 438 | Async::Async(const std::size_t n_threads, AsyncConfig config) 439 | : m_impl{std::make_unique(n_threads, std::move(config))} 440 | { 441 | } 442 | 443 | Async::~Async() = default; 444 | 445 | void 446 | Async::set_active(const bool active) 447 | { 448 | m_impl->set_active(active); 449 | } 450 | 451 | std::size_t 452 | Async::n_threads() const 453 | { 454 | return m_impl->n_threads(); 455 | } 456 | 457 | void 458 | Async::execute(ITask& root) 459 | { 460 | m_impl->execute(root); 461 | } 462 | 463 | void 464 | MetaData::add(gcl::TaskId id, std::string name) 465 | { 466 | m_map.emplace(id, TaskData{m_counter++, std::move(name)}); 467 | } 468 | 469 | const MetaData::TaskData* 470 | MetaData::get(gcl::TaskId id) const 471 | { 472 | auto pair = m_map.find(id); 473 | if (pair == m_map.end()) 474 | { 475 | return nullptr; 476 | } 477 | return &pair->second; 478 | } 479 | 480 | std::string 481 | to_dot(const std::vector& edges, const MetaData& meta) 482 | { 483 | auto str = [&meta](const auto& id) { 484 | std::string task = "\"" + std::to_string(id); 485 | if (auto data = meta.get(id)) 486 | { 487 | if (!data->name.empty()) 488 | { 489 | task += "\n" + data->name; 490 | } 491 | task += "\n" + std::to_string(data->instance_no); 492 | } 493 | task += "\""; 494 | return task; 495 | }; 496 | std::string dot = "digraph {\n"; 497 | for (const auto& edge : edges) 498 | { 499 | dot += str(edge.parent) + " -> " + str(edge.child) + "\n"; 500 | } 501 | dot += "}"; 502 | return dot; 503 | } 504 | 505 | } // namespace gcl 506 | -------------------------------------------------------------------------------- /test/example.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() { 6 | gcl::MetaData meta; // To collect graph metadata. Completely optional. 7 | auto t1 = gcl::task([]{ return 42.0; }).md(meta, "parent1"); 8 | auto t2 = gcl::task([]{ return 13.3; }).md(meta, "parent2"); 9 | auto t3 = gcl::tie(t1, t2).then([](auto t1, auto t2){ 10 | return *t1.get() + *t2.get(); }).md(meta, "child"); 11 | gcl::Async async{4}; 12 | t3.schedule_all(async); // Running the tasks using 4 threads 13 | t3.wait(); 14 | std::cout << *t3.get() << std::endl; // 55.3 15 | std::ofstream{"example.dot"} << gcl::to_dot(t3.edges(), meta); // output in dot notation 16 | } 17 | -------------------------------------------------------------------------------- /test/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomen/gcl/5fd250ef29b45a762ae4e52b8240d49d2db573ce/test/example.png -------------------------------------------------------------------------------- /test/test.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #if __clang__ || __GNUC__ 3 | #pragma GCC diagnostic push 4 | #pragma GCC diagnostic ignored "-Wconversion" 5 | #endif 6 | #include "catch_amalgamated.hpp" 7 | #if __clang__ || __GNUC__ 8 | #pragma GCC diagnostic pop 9 | #endif 10 | #include "gcl.h" 11 | 12 | #include 13 | 14 | namespace gcl 15 | { 16 | 17 | bool 18 | operator==(const Edge& lhs, const Edge& rhs) 19 | { 20 | return lhs.parent == rhs.parent && lhs.child == rhs.child; 21 | } 22 | 23 | } // namespace gcl 24 | 25 | namespace 26 | { 27 | 28 | bool 29 | contains(const std::string& str, const char* token) 30 | { 31 | return str.find(token) != std::string::npos; 32 | } 33 | 34 | template 35 | bool 36 | contains(const std::string& str, const T& token) 37 | { 38 | return str.find(std::to_string(token)) != std::string::npos; 39 | } 40 | 41 | } // namespace 42 | 43 | void 44 | test_schedule(const std::size_t n_threads) 45 | { 46 | auto p1 = gcl::task([] { return 42; }); 47 | auto p2 = gcl::task([] { return 13; }); 48 | auto t = gcl::tie(p1, p2).then( 49 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 50 | gcl::Async async{n_threads}; 51 | REQUIRE(t.schedule_all(async)); 52 | t.wait(); 53 | REQUIRE(55 == *t.get()); 54 | } 55 | 56 | TEST_CASE("schedule") 57 | { 58 | test_schedule(0); 59 | } 60 | 61 | TEST_CASE("schedule_with_1_thread") 62 | { 63 | test_schedule(1); 64 | } 65 | 66 | TEST_CASE("schedule_with_4_threads") 67 | { 68 | test_schedule(4); 69 | } 70 | 71 | void 72 | test_schedule_and_cancel(const std::size_t n_threads) 73 | { 74 | auto p1 = gcl::task([] { return 42; }); 75 | auto p2 = gcl::task([] { return 13; }); 76 | gcl::CancelToken ct; 77 | auto t = gcl::tie(p1, p2).then([&ct](auto p1, auto p2) { 78 | if (ct.is_canceled()) 79 | { 80 | return -1; 81 | } 82 | return *p1.get() + *p2.get(); 83 | }); 84 | ct.set_canceled(); 85 | gcl::Async async{n_threads}; 86 | REQUIRE(t.schedule_all(async)); 87 | t.wait(); 88 | REQUIRE(-1 == *t.get()); 89 | } 90 | 91 | TEST_CASE("schedule_and_cancel") 92 | { 93 | test_schedule_and_cancel(0); 94 | } 95 | 96 | TEST_CASE("schedule_and_cancel_with_1_thread") 97 | { 98 | test_schedule_and_cancel(1); 99 | } 100 | 101 | TEST_CASE("schedule_and_cancel_with_4_threads") 102 | { 103 | test_schedule_and_cancel(4); 104 | } 105 | 106 | void 107 | test_schedule_and_release(const std::size_t n_threads) 108 | { 109 | auto p1 = gcl::task([] { return 42; }); 110 | auto p2 = gcl::task([] { return 13; }); 111 | auto t = gcl::tie(p1, p2).then( 112 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 113 | gcl::Async async{n_threads}; 114 | REQUIRE(t.schedule_all(async)); 115 | t.wait(); 116 | REQUIRE(p1.has_result()); 117 | REQUIRE(p2.has_result()); 118 | REQUIRE(t.has_result()); 119 | t.release_parents(); 120 | REQUIRE(!p1.has_result()); 121 | REQUIRE(!p2.has_result()); 122 | REQUIRE(t.has_result()); 123 | REQUIRE(t.schedule_all(async)); 124 | t.wait(); 125 | REQUIRE(p1.has_result()); 126 | REQUIRE(p2.has_result()); 127 | REQUIRE(t.has_result()); 128 | p2.release(); 129 | REQUIRE(p1.has_result()); 130 | REQUIRE(!p2.has_result()); 131 | REQUIRE(t.has_result()); 132 | } 133 | 134 | TEST_CASE("schedule_and_release") 135 | { 136 | test_schedule_and_release(0); 137 | } 138 | 139 | TEST_CASE("schedule_and_release_with_1_thread") 140 | { 141 | test_schedule_and_release(1); 142 | } 143 | 144 | TEST_CASE("schedule_and_release_with_4_threads") 145 | { 146 | test_schedule_and_release(4); 147 | } 148 | 149 | void 150 | test_schedule_and_auto_release(const std::size_t n_threads) 151 | { 152 | auto p1 = gcl::task([] { return 42; }); 153 | auto p2 = gcl::task([] { return 13; }); 154 | auto t = gcl::tie(p1, p2).then( 155 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 156 | t.set_auto_release_parents(true); 157 | gcl::Async async{n_threads}; 158 | REQUIRE(t.schedule_all(async)); 159 | t.wait(); 160 | REQUIRE(!p1.has_result()); 161 | REQUIRE(!p2.has_result()); 162 | REQUIRE(t.has_result()); 163 | REQUIRE(55 == *t.get()); 164 | p1.set_auto_release(false); 165 | REQUIRE(t.schedule_all(async)); 166 | t.wait(); 167 | REQUIRE(p1.has_result()); 168 | REQUIRE(!p2.has_result()); 169 | REQUIRE(t.has_result()); 170 | REQUIRE(55 == *t.get()); 171 | t.set_auto_release_parents(true); 172 | t.set_auto_release(true); 173 | REQUIRE(t.schedule_all(async)); 174 | t.wait(); 175 | REQUIRE(!p1.has_result()); 176 | REQUIRE(!p2.has_result()); 177 | REQUIRE(!t.has_result()); 178 | } 179 | 180 | TEST_CASE("schedule_and_auto_release") 181 | { 182 | test_schedule_and_auto_release(0); 183 | } 184 | 185 | TEST_CASE("schedule_and_auto_release_with_1_thread") 186 | { 187 | test_schedule_and_auto_release(1); 188 | } 189 | 190 | TEST_CASE("schedule_and_auto_release_with_4_threads") 191 | { 192 | test_schedule_and_auto_release(4); 193 | } 194 | 195 | TEST_CASE("schedule_with_vec_parents") 196 | { 197 | auto p1 = gcl::task([] { return 42; }); 198 | auto p2 = gcl::task([] { return 13; }); 199 | auto t = gcl::tie(gcl::vec(p1, p2)).then([](gcl::Vec p) { 200 | return *p[0].get() + *p[1].get(); 201 | }); 202 | gcl::Async async; 203 | REQUIRE(t.schedule_all(async)); 204 | REQUIRE(55 == *t.get()); 205 | } 206 | 207 | TEST_CASE("schedule_using_reference_type") 208 | { 209 | int x = 42; 210 | auto p = gcl::task([&x]() -> int& { return x; }); 211 | auto t = p.then([](auto p) -> int& { return *p.get(); }); 212 | gcl::Async async{4}; 213 | REQUIRE(t.schedule_all(async)); 214 | t.wait(); 215 | REQUIRE(42 == *p.get()); 216 | REQUIRE(&x == p.get()); 217 | } 218 | 219 | void 220 | test_schedule_a_wide_graph(const std::size_t n_threads, 221 | const gcl::AsyncConfig::QueueType queue_type, 222 | const bool use_schedule_overload) 223 | { 224 | std::atomic x{0}; 225 | auto top = gcl::task([&x] { return x++; }); 226 | gcl::Vec tasks; 227 | for (int i = 0; i < 10; ++i) 228 | { 229 | auto functor = [&x](auto t) { 230 | x++; 231 | return *t.get(); 232 | }; 233 | auto t1 = top.then(functor); 234 | auto t2 = t1.then(functor); 235 | tasks.push_back(t2); 236 | } 237 | auto t = gcl::tie(tasks).then([&x](gcl::Vec) { x++; }); 238 | if (use_schedule_overload) 239 | { 240 | t.schedule_all(); 241 | } 242 | else 243 | { 244 | gcl::AsyncConfig config; 245 | config.queue_type = queue_type; 246 | gcl::Async async{n_threads, config}; 247 | REQUIRE(t.schedule_all(async)); 248 | t.wait(); 249 | } 250 | REQUIRE(22 == x); 251 | } 252 | 253 | TEST_CASE("schedule_a_wide_graph") 254 | { 255 | test_schedule_a_wide_graph(0, gcl::AsyncConfig::QueueType::Spin, false); 256 | test_schedule_a_wide_graph(0, gcl::AsyncConfig::QueueType::Mutex, true); 257 | test_schedule_a_wide_graph(0, gcl::AsyncConfig::QueueType::Mutex, false); 258 | test_schedule_a_wide_graph(0, gcl::AsyncConfig::QueueType::Spin, true); 259 | } 260 | 261 | TEST_CASE("schedule_a_wide_graph_with_1_thread") 262 | { 263 | test_schedule_a_wide_graph(1, gcl::AsyncConfig::QueueType::Spin, false); 264 | test_schedule_a_wide_graph(1, gcl::AsyncConfig::QueueType::Mutex, true); 265 | test_schedule_a_wide_graph(1, gcl::AsyncConfig::QueueType::Mutex, false); 266 | test_schedule_a_wide_graph(1, gcl::AsyncConfig::QueueType::Spin, true); 267 | } 268 | 269 | TEST_CASE("schedule_a_wide_graph_with_4_threads") 270 | { 271 | test_schedule_a_wide_graph(4, gcl::AsyncConfig::QueueType::Spin, false); 272 | test_schedule_a_wide_graph(4, gcl::AsyncConfig::QueueType::Mutex, true); 273 | test_schedule_a_wide_graph(4, gcl::AsyncConfig::QueueType::Mutex, false); 274 | test_schedule_a_wide_graph(4, gcl::AsyncConfig::QueueType::Spin, true); 275 | } 276 | 277 | TEST_CASE("schedule_with_mixed_parents") 278 | { 279 | int x = 0; 280 | auto p1 = gcl::task([&x] { 281 | x++; 282 | return 42.0; 283 | }); 284 | auto p2 = gcl::task([&x] { 285 | x++; 286 | return 13; 287 | }); 288 | auto p3 = gcl::task([&x] { 289 | x++; 290 | return 20; 291 | }); 292 | auto p4 = gcl::task([&x] { 293 | x++; 294 | return 21; 295 | }); 296 | auto p6 = gcl::task([&x] { 297 | x++; 298 | return std::string{"guan"}; 299 | }); 300 | auto t = gcl::when(p1, p2, gcl::vec(p3, p4), p6); 301 | gcl::Async async; 302 | REQUIRE(t.schedule_all(async)); 303 | REQUIRE(5 == x); 304 | } 305 | 306 | TEST_CASE("schedule_with_bind_and_when") 307 | { 308 | int x = 0; 309 | auto p1 = gcl::task([&x] { x++; }); 310 | auto p2 = gcl::task([&x] { x++; }); 311 | auto t = gcl::when(gcl::tie(p1, p2)); 312 | gcl::Async async; 313 | REQUIRE(t.schedule_all(async)); 314 | REQUIRE(2 == x); 315 | } 316 | 317 | TEST_CASE("edges") 318 | { 319 | auto p1 = gcl::task([] { return 42; }); 320 | auto p2 = gcl::task([] { return 13; }); 321 | auto t = gcl::when(p1, p2); 322 | gcl::Async async; 323 | REQUIRE(t.schedule_all(async)); 324 | const std::vector exp_edges = {{p1.id(), t.id()}, 325 | {p2.id(), t.id()}}; 326 | REQUIRE(exp_edges == t.edges()); 327 | } 328 | 329 | void 330 | test_schedule_twice(const std::size_t n_threads) 331 | { 332 | std::atomic x{0}; 333 | auto p1 = gcl::task([&x] { x++; }); 334 | auto p2 = gcl::task([&x] { x++; }); 335 | auto t = gcl::when(p1, p2); 336 | gcl::Async async{n_threads}; 337 | REQUIRE(t.schedule_all(async)); 338 | t.wait(); 339 | REQUIRE(2 == x); 340 | REQUIRE(t.schedule_all(async)); 341 | t.wait(); 342 | REQUIRE(4 == x); 343 | } 344 | 345 | TEST_CASE("schedule_twice") 346 | { 347 | test_schedule_twice(0); 348 | } 349 | 350 | TEST_CASE("schedule_twice_with_1_thread") 351 | { 352 | test_schedule_twice(1); 353 | } 354 | 355 | TEST_CASE("schedule_twice_with_4_thread") 356 | { 357 | test_schedule_twice(4); 358 | } 359 | 360 | namespace 361 | { 362 | 363 | struct CopyOnly 364 | { 365 | CopyOnly() = default; 366 | CopyOnly(const CopyOnly&) = default; 367 | CopyOnly& 368 | operator=(const CopyOnly&) = default; 369 | int 370 | operator()() const 371 | { 372 | return 42; 373 | } 374 | }; 375 | 376 | } // namespace 377 | 378 | TEST_CASE("functor_only_copyable_as_rvalue") 379 | { 380 | auto t = gcl::task(CopyOnly{}); 381 | gcl::Async async; 382 | REQUIRE(t.schedule_all(async)); 383 | REQUIRE(42 == *t.get()); 384 | } 385 | 386 | TEST_CASE("functor_only_copyable_as_lvalue") 387 | { 388 | CopyOnly functor; 389 | auto t = gcl::task(functor); 390 | gcl::Async async; 391 | REQUIRE(t.schedule_all(async)); 392 | REQUIRE(42 == *t.get()); 393 | } 394 | 395 | TEST_CASE("functor_only_copyable_as_const_lvalue") 396 | { 397 | const CopyOnly functor; 398 | auto t = gcl::task(functor); 399 | gcl::Async async; 400 | REQUIRE(t.schedule_all(async)); 401 | REQUIRE(42 == *t.get()); 402 | } 403 | 404 | namespace 405 | { 406 | 407 | struct MoveOnly 408 | { 409 | MoveOnly() = default; 410 | MoveOnly(MoveOnly&&) = default; 411 | MoveOnly& 412 | operator=(MoveOnly&&) = default; 413 | int 414 | operator()() const 415 | { 416 | return 42; 417 | } 418 | }; 419 | 420 | } // namespace 421 | 422 | TEST_CASE("functor_only_movable") 423 | { 424 | auto t = gcl::task(MoveOnly{}); 425 | gcl::Async async; 426 | REQUIRE(t.schedule_all(async)); 427 | REQUIRE(42 == *t.get()); 428 | } 429 | 430 | TEST_CASE("task_chaining_with_int") 431 | { 432 | int x = 0; 433 | auto f = [&x](auto) { 434 | x++; 435 | return 0; 436 | }; 437 | auto t = gcl::task([&x] { 438 | x++; 439 | return 0; 440 | }) 441 | .then(f) 442 | .then(f) 443 | .then(f) 444 | .then(f); 445 | gcl::Async async; 446 | REQUIRE(t.schedule_all(async)); 447 | REQUIRE(5 == x); 448 | } 449 | 450 | TEST_CASE("task_chaining_with_void") 451 | { 452 | int x = 0; 453 | auto f = [&x](gcl::Task) { x++; }; 454 | auto t = gcl::task([&x] { x++; }).then(f).then(f).then(f).then(f); 455 | gcl::Async async; 456 | REQUIRE(t.schedule_all(async)); 457 | REQUIRE(5 == x); 458 | } 459 | 460 | void 461 | test_for_each(const std::size_t n_threads) 462 | { 463 | std::vector data{1, 2, 3, 4, 5}; 464 | auto t = gcl::for_each(data.begin(), data.end(), [](auto it) { *it *= 2; }); 465 | gcl::Async async{n_threads}; 466 | REQUIRE(t.schedule_all(async)); 467 | t.wait(); 468 | const std::vector data_exp{2, 4, 6, 8, 10}; 469 | REQUIRE(data_exp == data); 470 | } 471 | 472 | TEST_CASE("for_each") 473 | { 474 | test_for_each(0); 475 | } 476 | 477 | TEST_CASE("for_each_with_1_thread") 478 | { 479 | test_for_each(1); 480 | } 481 | 482 | TEST_CASE("for_each_with_4_threads") 483 | { 484 | test_for_each(4); 485 | } 486 | 487 | TEST_CASE("for_each_with_counters") 488 | { 489 | std::vector data{1, 2, 3, 4, 5}; 490 | auto t = gcl::for_each(0u, data.size(), [&data](auto i) { data[i] *= 2; }); 491 | gcl::Async async; 492 | REQUIRE(t.schedule_all(async)); 493 | const std::vector data_exp{2, 4, 6, 8, 10}; 494 | REQUIRE(data_exp == data); 495 | } 496 | 497 | TEST_CASE("for_each_with_empty_range") 498 | { 499 | std::vector data; 500 | auto t = gcl::for_each(data.begin(), data.end(), [](auto it) { *it *= 2; }); 501 | gcl::Async async{2}; 502 | REQUIRE(t.schedule_all(async)); 503 | t.wait(); 504 | const std::vector data_exp; 505 | REQUIRE(data_exp == data); 506 | } 507 | 508 | TEST_CASE("for_each_with_empty_counter_range") 509 | { 510 | std::vector data; 511 | auto t = gcl::for_each(0u, data.size(), [](auto) {}); 512 | gcl::Async async{2}; 513 | REQUIRE(t.schedule_all(async)); 514 | t.wait(); 515 | const std::vector data_exp; 516 | REQUIRE(data_exp == data); 517 | } 518 | 519 | TEST_CASE("schedule_with_thread_affinity") 520 | { 521 | auto p1 = gcl::task([] { return 42; }); 522 | p1.set_thread_affinity(0); 523 | auto p2 = gcl::task([] { return 13; }); 524 | p1.set_thread_affinity(1); 525 | auto t = gcl::tie(p1, p2).then( 526 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 527 | t.set_thread_affinity(2); 528 | gcl::Async async{2}; 529 | REQUIRE(t.schedule_all(async)); 530 | t.wait(); 531 | REQUIRE(55 == *t.get()); 532 | } 533 | 534 | TEST_CASE("to_dot_without_meta") 535 | { 536 | auto p1 = gcl::task([] { return 42; }); 537 | auto p2 = gcl::task([] { return 13; }); 538 | auto t = gcl::tie(p1, p2).then( 539 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 540 | const auto dot = gcl::to_dot(t.edges()); 541 | REQUIRE(contains(dot, p1.id())); 542 | REQUIRE(contains(dot, p2.id())); 543 | REQUIRE(contains(dot, t.id())); 544 | } 545 | 546 | TEST_CASE("to_dot_with_meta") 547 | { 548 | gcl::MetaData meta; 549 | auto p1 = gcl::task([] { return 42; }).md(meta, "parent1"); 550 | auto p2 = gcl::task([] { return 13; }).md(meta); 551 | auto t = gcl::tie(p1, p2) 552 | .then([](auto p1, auto p2) { return *p1.get() + *p2.get(); }) 553 | .md(meta, "child"); 554 | const auto dot = gcl::to_dot(t.edges(), meta); 555 | REQUIRE(contains(dot, p1.id())); 556 | REQUIRE(contains(dot, "parent1")); 557 | REQUIRE(contains(dot, p2.id())); 558 | REQUIRE(contains(dot, t.id())); 559 | REQUIRE(contains(dot, "child")); 560 | } 561 | 562 | TEST_CASE("async_with_spin_queue_and_no_interval_sleep") 563 | { 564 | auto p1 = gcl::task([] { return 42; }); 565 | auto p2 = gcl::task([] { return 13; }); 566 | auto t = gcl::tie(p1, p2).then( 567 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 568 | gcl::AsyncConfig config; 569 | config.queue_type = gcl::AsyncConfig::QueueType::Spin; 570 | config.spin_config.processor_sleep_interval = std::chrono::microseconds{0}; 571 | gcl::Async async{4, config}; 572 | REQUIRE(t.schedule_all(async)); 573 | t.wait(); 574 | REQUIRE(55 == *t.get()); 575 | } 576 | 577 | TEST_CASE("async_with_spin_queue_and_active") 578 | { 579 | auto p1 = gcl::task([] { return 42; }); 580 | auto p2 = gcl::task([] { return 13; }); 581 | auto t = gcl::tie(p1, p2).then( 582 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 583 | gcl::AsyncConfig config; 584 | config.queue_type = gcl::AsyncConfig::QueueType::Spin; 585 | gcl::Async async{4, config}; 586 | async.set_active(true); 587 | REQUIRE(t.schedule_all(async)); 588 | t.wait(); 589 | REQUIRE(55 == *t.get()); 590 | } 591 | 592 | TEST_CASE("async_with_spin_queue_and_yields") 593 | { 594 | auto p1 = gcl::task([] { return 42; }); 595 | auto p2 = gcl::task([] { return 13; }); 596 | auto t = gcl::tie(p1, p2).then( 597 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 598 | gcl::AsyncConfig config; 599 | config.queue_type = gcl::AsyncConfig::QueueType::Spin; 600 | config.spin_config.spin_lock_yields = true; 601 | gcl::Async async{4, config}; 602 | REQUIRE(t.schedule_all(async)); 603 | t.wait(); 604 | REQUIRE(55 == *t.get()); 605 | } 606 | 607 | TEST_CASE("schedule_with_thread_callbacks") 608 | { 609 | auto p1 = gcl::task([] { return 42; }); 610 | auto p2 = gcl::task([] { return 13; }); 611 | auto t = gcl::tie(p1, p2).then( 612 | [](auto p1, auto p2) { return *p1.get() + *p2.get(); }); 613 | gcl::AsyncConfig config; 614 | std::atomic scheduler{0}; 615 | std::atomic processor{0}; 616 | config.on_scheduler_thread_started = [&scheduler] { ++scheduler; }; 617 | config.on_processor_thread_started = [&processor](const std::size_t index) { 618 | ++processor; 619 | if (index >= 4) 620 | { 621 | throw std::runtime_error{std::to_string(index)}; 622 | } 623 | }; 624 | gcl::Async async{4, config}; 625 | REQUIRE(t.schedule_all(async)); 626 | t.wait(); 627 | REQUIRE(1 == scheduler.load()); 628 | while (processor.load() < 4) 629 | { 630 | std::this_thread::yield(); 631 | } 632 | REQUIRE(55 == *t.get()); 633 | REQUIRE(4 == processor.load()); 634 | } 635 | 636 | TEST_CASE("schedule_with_exception_with_int_return") 637 | { 638 | auto t = gcl::task([]() -> int { throw std::runtime_error{"booh"}; }); 639 | REQUIRE(!t.get()); 640 | REQUIRE(t.schedule_all()); 641 | REQUIRE_THROWS_AS(*t.get(), std::runtime_error); 642 | } 643 | 644 | TEST_CASE("schedule_with_exception_with_int&_return") 645 | { 646 | auto t = gcl::task([]() -> int& { throw std::runtime_error{"booh"}; }); 647 | REQUIRE(!t.get()); 648 | REQUIRE(t.schedule_all()); 649 | REQUIRE_THROWS_AS(*t.get(), std::runtime_error); 650 | } 651 | 652 | TEST_CASE("schedule_with_exception_with_void_return") 653 | { 654 | auto t = gcl::task([]() { throw std::runtime_error{"booh"}; }); 655 | REQUIRE(!t.get()); 656 | REQUIRE(t.schedule_all()); 657 | REQUIRE_THROWS_AS(t.get(), std::runtime_error); 658 | } 659 | --------------------------------------------------------------------------------