├── .gitignore ├── images ├── graph_w1.png ├── graph_w2.png ├── what_black.png ├── what_white.png ├── coros_graphs_13.png └── coros_graphs_14.png ├── .gitattributes ├── include ├── task_life_time.h ├── start_barrier.h ├── test_deque.h ├── constructor_counter.hpp ├── wait_barrier.h ├── start_tasks.h ├── enqueue_tasks.h ├── thread_pool.h ├── deque.h ├── task.h ├── chain_tasks.h └── wait_tasks.h ├── examples ├── CMakeLists.txt ├── fib.cpp ├── enqueueing.cpp ├── simple_task_execution.cpp ├── waiting.cpp └── chaining.cpp ├── tests ├── thread_pool_test.cpp ├── wait_barrier_test.cpp ├── CMakeLists.txt ├── start_tasks_test.cpp ├── deque_test.cpp ├── enqueue_tasks_test.cpp ├── chain_test.cpp ├── wait_task_test.cpp └── task_test.cpp ├── benchmarks ├── tbb │ ├── fib_tbb.h │ ├── main.cpp │ └── mat_tbb.h ├── coros_fib.h ├── CMakeLists.txt ├── openmp_fib.h ├── main.cpp ├── coros_mat.h └── openmp_mat.h ├── CMakeLists.txt ├── LICENSE ├── .github └── workflows │ ├── gcc_test.yml │ ├── clang_test.yml │ ├── add_build.yml │ ├── test_build.yml │ └── tsan_build.yml ├── .clang-format └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | commit_template.txt 3 | .clang-format 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/graph_w1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtmucha/coros/HEAD/images/graph_w1.png -------------------------------------------------------------------------------- /images/graph_w2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtmucha/coros/HEAD/images/graph_w2.png -------------------------------------------------------------------------------- /images/what_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtmucha/coros/HEAD/images/what_black.png -------------------------------------------------------------------------------- /images/what_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtmucha/coros/HEAD/images/what_white.png -------------------------------------------------------------------------------- /images/coros_graphs_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtmucha/coros/HEAD/images/coros_graphs_13.png -------------------------------------------------------------------------------- /images/coros_graphs_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtmucha/coros/HEAD/images/coros_graphs_14.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /include/task_life_time.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_TASK_LIFE_TIME_H_ 2 | #define COROS_INCLUDE_TASK_LIFE_TIME_H_ 3 | 4 | namespace coros { 5 | namespace detail { 6 | 7 | // Thread pool managed tasks are destroyed when the thread pool is destroyed. 8 | enum class TaskLifeTime { 9 | SCOPE_MANAGED, 10 | THREAD_POOL_MANAGED, 11 | NOOP, /*Used with noop coroutines*/ 12 | }; 13 | 14 | } // namespace detail 15 | } // namespace coros 16 | 17 | #endif // COROS_INCLUDE_TASK_LIFE_TIME_H_ 18 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project(coros_example) 4 | 5 | add_executable(simple_task_execution simple_task_execution.cpp) 6 | add_executable(waiting waiting.cpp) 7 | add_executable(enqueueing enqueueing.cpp) 8 | add_executable(chaining chaining.cpp) 9 | add_executable(fib fib.cpp) 10 | 11 | target_include_directories(simple_task_execution PUBLIC ${CMAKE_SOURCE_DIR}/include) 12 | target_include_directories(waiting PUBLIC ${CMAKE_SOURCE_DIR}/include) 13 | target_include_directories(enqueueing PUBLIC ${CMAKE_SOURCE_DIR}/include) 14 | target_include_directories(chaining PUBLIC ${CMAKE_SOURCE_DIR}/include) 15 | target_include_directories(fib PUBLIC ${CMAKE_SOURCE_DIR}/include) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/thread_pool_test.cpp: -------------------------------------------------------------------------------- 1 | #include "thread_pool.h" 2 | #include 3 | 4 | #include "wait_tasks.h" 5 | #include "start_tasks.h" 6 | 7 | namespace { 8 | coros::Task fib(coros::ThreadPool& tp, int index) { 9 | if (index == 0) co_return 0; 10 | if (index == 1) co_return 1; 11 | 12 | coros::Task a = fib(tp, index - 1); 13 | coros::Task b = fib(tp, index - 2); 14 | 15 | co_await coros::wait_tasks(a, b); 16 | 17 | co_return *a + *b; 18 | } 19 | 20 | } 21 | 22 | TEST(ThreadPoolTest, Construction) { 23 | coros::ThreadPool tp{2}; 24 | } 25 | 26 | 27 | TEST(ThreadPoolTest, Fib) { 28 | coros::ThreadPool tp{4}; 29 | coros::Task t = fib(tp, 20); 30 | coros::start_sync(tp, t); 31 | EXPECT_EQ(*t, 6765); 32 | } 33 | -------------------------------------------------------------------------------- /examples/fib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "start_tasks.h" 4 | #include "thread_pool.h" 5 | #include "wait_tasks.h" 6 | 7 | coros::Task fib(int n) { 8 | if (n < 2) co_return n; 9 | 10 | coros::Task a = fib(n - 1); 11 | coros::Task b = fib(n - 2); 12 | 13 | co_await coros::wait_tasks(a, b); 14 | 15 | co_return *a + *b; 16 | } 17 | 18 | 19 | /*long fib(int n) { 20 | if (n < 2) return n; 21 | 22 | long a = fib(n - 1); 23 | long b = fib(n - 1); 24 | 25 | 26 | 27 | return a + b; 28 | }*/ 29 | 30 | int main() { 31 | coros::Task task = fib(10); 32 | 33 | { 34 | coros::ThreadPool tp{4}; 35 | coros::start_sync(tp, task); 36 | } // Once the ThreadPool goes out of scope, all worker threads are joined and destroyed. 37 | 38 | std::cout << "The 10th fibonacci number is : " << *task << std::endl; 39 | } 40 | -------------------------------------------------------------------------------- /benchmarks/tbb/fib_tbb.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | extern int g_thread_num; 7 | 8 | long fibonacci(int n) { 9 | if (n < 2) return n; 10 | 11 | long a, b; 12 | 13 | tbb::parallel_invoke( 14 | [&] { a = fibonacci(n-1); }, 15 | [&] { b = fibonacci(n-2); } 16 | ); 17 | 18 | return a + b; 19 | } 20 | 21 | int bench_tbb_fib() { 22 | int n = 30; 23 | int result = -1; 24 | int result_time = -1; 25 | 26 | tbb::task_arena arena(4); 27 | 28 | auto start = std::chrono::high_resolution_clock::now(); 29 | 30 | arena.execute([&]{ 31 | result = fibonacci(n); 32 | }); 33 | 34 | 35 | auto end = std::chrono::high_resolution_clock::now(); 36 | auto duration = std::chrono::duration_cast(end - start); 37 | result_time = duration.count(); 38 | 39 | if (result != 832040) { 40 | std::cerr << "Wrong result OneTBB: " << result << std::endl; 41 | } 42 | 43 | 44 | return result_time; 45 | } 46 | -------------------------------------------------------------------------------- /benchmarks/coros_fib.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "start_tasks.h" 5 | #include "thread_pool.h" 6 | #include "wait_tasks.h" 7 | 8 | 9 | extern int g_thread_num; 10 | 11 | inline coros::Task fib(int index) { 12 | if (index < 2) co_return index; 13 | 14 | coros::Task a = fib(index - 1); 15 | coros::Task b = fib(index - 2); 16 | 17 | co_await coros::wait_tasks(a, b); 18 | 19 | co_return *a + *b; 20 | } 21 | 22 | 23 | inline int bench_workstealing_fib() { 24 | coros::ThreadPool tp{g_thread_num}; 25 | coros::Task t = fib(30); 26 | 27 | auto start = std::chrono::high_resolution_clock::now(); 28 | 29 | coros::start_sync(tp, t); 30 | 31 | auto end = std::chrono::high_resolution_clock::now(); 32 | auto duration = std::chrono::duration_cast(end - start); 33 | 34 | if (*t != 832040) { 35 | std::cerr << "Wrong result Coros fib : " << *t << std::endl; 36 | } 37 | 38 | return duration.count(); 39 | } 40 | -------------------------------------------------------------------------------- /benchmarks/tbb/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "fib_tbb.h" 8 | #include "mat_tbb.h" 9 | 10 | int g_thread_num; 11 | 12 | void make_test(std::string file_name, std::function bench_func) { 13 | std::ofstream outFile(file_name); 14 | if (outFile.is_open()) { 15 | for (int i = 0; i < 50; i++) { 16 | outFile << bench_func() << std::endl; 17 | } 18 | outFile.close(); 19 | } else { 20 | std::cerr << "Unable to open file for writing" << std::endl; 21 | } 22 | } 23 | 24 | int main(int argc, char const *argv[]) { 25 | 26 | if (argc != 2) { 27 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 28 | } 29 | 30 | g_thread_num = std::atoi(argv[1]); 31 | 32 | make_test("onetbb_fib_" + std::to_string(g_thread_num) + ".txt", bench_tbb_fib); 33 | make_test("onetbb_matmul_" + std::to_string(g_thread_num) + ".txt", bench_tbb_matmul); 34 | 35 | return 0; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /benchmarks/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project(coros_benchmark) 4 | 5 | add_executable( 6 | coros_benchmark 7 | main.cpp 8 | ) 9 | 10 | target_include_directories(coros_benchmark 11 | PUBLIC ${CMAKE_SOURCE_DIR}/include 12 | ) 13 | 14 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 15 | target_compile_options(coros_benchmark PRIVATE 16 | -std=c++23 17 | -O3 18 | -fcoroutines 19 | -g 20 | -fopenmp 21 | -Winterference-size 22 | ) 23 | target_link_options(coros_benchmark PRIVATE -fopenmp) 24 | endif() 25 | 26 | 27 | if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 28 | target_compile_options(coros_benchmark PRIVATE 29 | -std=c++23 30 | -stdlib=libc++ 31 | -O3 32 | -g 33 | -fopenmp 34 | ) 35 | target_link_options(coros_benchmark PRIVATE "-stdlib=libc++" "-lomp") 36 | #target_include_directories(coros_benchmark PRIVATE /usr/lib/llvm-14/include/c++/v1/) 37 | #target_link_directories(coros_benchmark PRIVATE /usr/lib/llvm-14/lib) 38 | endif() 39 | -------------------------------------------------------------------------------- /examples/enqueueing.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "start_tasks.h" 4 | #include "enqueue_tasks.h" 5 | 6 | std::atomic counter = 0; 7 | 8 | coros::Task add_one() { 9 | counter++; 10 | co_return; 11 | } 12 | 13 | coros::Task increase_counter(coros::ThreadPool& tp) { 14 | 15 | // The temporary task object is destroyed once the 16 | // task completes. 17 | // 18 | // Enqueued tasks cannot be awaited. 19 | coros::enqueue_tasks(add_one()); 20 | 21 | // This overload enqueues the task into a different 22 | // threadpool queue. 23 | coros::enqueue_tasks(tp, add_one()); 24 | 25 | co_return; 26 | } 27 | 28 | 29 | int main () { 30 | coros::ThreadPool tp{2}; 31 | coros::ThreadPool tp2{2}; 32 | 33 | coros::Task t = increase_counter(tp2); 34 | 35 | coros::start_sync(tp, t); 36 | 37 | // Given that start_sync only waits for the foo() task, we have no 38 | // guarantee that the counter will be 2. It can be 0, 1 or 2, depending 39 | // on the order of execution of individual tasks. 40 | std::cout << "counter value : " << counter << std::endl; 41 | } 42 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | enable_testing() 4 | 5 | project(coros) 6 | 7 | set(CMAKE_CXX_STANDARD 23) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | 10 | # Exclude pthread search for MSVC 11 | if (NOT MSVC) 12 | find_package(Threads REQUIRED) 13 | endif() 14 | 15 | 16 | # compiler options for GCC 17 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 18 | add_compile_options(-std=c++23 -Wall -Wextra -Wpedantic -Wno-interference-size -fcoroutines) 19 | endif() 20 | 21 | # compiler options for Clang 22 | if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 23 | add_compile_options(-std=c++23 -stdlib=libc++ -Wall -Wextra -Wpedantic) 24 | add_link_options(-std=c++23, -stdlib=libc++) 25 | endif() 26 | 27 | # Compiler options for MSVC 28 | if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 29 | add_compile_options(/std:c++latest /W4 /await:strict /permissive-) 30 | 31 | # add_compile_options(/W4) # /W4 is similar to -Wall -Wextra in GCC/Clang 32 | 33 | #add_compile_options(/wd4996) 34 | endif() 35 | 36 | add_subdirectory(tests) 37 | add_subdirectory(benchmarks) 38 | add_subdirectory(examples) 39 | 40 | 41 | -------------------------------------------------------------------------------- /benchmarks/openmp_fib.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "omp.h" 7 | 8 | extern int g_thread_num; 9 | 10 | long fibonacci(int n) { 11 | if (n < 2) return n; 12 | 13 | long a, b; 14 | 15 | #pragma omp task shared(a) 16 | a = fibonacci(n - 1); 17 | 18 | #pragma omp task shared(b) 19 | b = fibonacci(n - 2); 20 | 21 | #pragma omp taskwait 22 | 23 | return a + b; 24 | } 25 | 26 | int bench_openmp_fib() { 27 | int n = 30; 28 | int result = -1; 29 | int result_time = -1; 30 | omp_set_num_threads(g_thread_num); 31 | 32 | #pragma omp parallel 33 | { 34 | #pragma omp single 35 | { 36 | auto start = std::chrono::high_resolution_clock::now(); 37 | 38 | result = fibonacci(n); 39 | 40 | auto end = std::chrono::high_resolution_clock::now(); 41 | auto duration = std::chrono::duration_cast(end - start); 42 | result_time = duration.count(); 43 | } 44 | } 45 | 46 | if (result != 832040) { 47 | std::cerr << "Wrong result OpenMP fib: " << result << std::endl; 48 | } 49 | 50 | return result_time; 51 | } 52 | -------------------------------------------------------------------------------- /tests/wait_barrier_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "wait_barrier.h" 4 | 5 | TEST(WaitBarrierTest, IntConstruction) { 6 | coros::detail::WaitBarrier barrier(5, nullptr); 7 | EXPECT_EQ(barrier.get_continuation(), nullptr); 8 | } 9 | 10 | TEST(WaitBarrierTest, DecreaseCounter) { 11 | coros::detail::WaitBarrier barrier(5, nullptr); 12 | barrier.decrement_and_resume(); 13 | EXPECT_EQ(barrier.get_counter(), 4); 14 | EXPECT_EQ(barrier.get_continuation(), nullptr); 15 | } 16 | 17 | 18 | /*TEST(WaitBarrierTest, Async) { 19 | { 20 | coros::ThreadPool tp{1}; 21 | coros::WaitTask t1 = 22 | []() -> coros::WaitTask { 23 | co_return; 24 | }(); 25 | 26 | EXPECT_EQ(coros::WaitTask::instance_count(), 27 | 1); 28 | 29 | coros::WaitForTasksAwaitableAsync awaitable{ 30 | tp, 31 | std::array, 1>{std::move(t1)}}; 32 | 33 | EXPECT_EQ(coros::WaitTask::instance_count(), 34 | 2); 35 | } 36 | 37 | }*/ 38 | 39 | 40 | -------------------------------------------------------------------------------- /include/start_barrier.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_START_BARRIER_H_ 2 | #define COROS_INCLUDE_START_BARRIER_H_ 3 | 4 | #include 5 | 6 | namespace coros { 7 | namespace detail { 8 | 9 | 10 | // This barrier is used with StartTask. Once the StartTask 11 | // finishes its execution the main thread is notified. 12 | // In case of async the barrier is checked once the user calls 13 | // wait on the StartingTask. 14 | class StartingBarrier { 15 | public: 16 | StartingBarrier(){}; 17 | ~StartingBarrier(){}; 18 | 19 | void wait() { 20 | std::unique_lock lock(mtx_); 21 | cv_.wait(lock, [this] { return flag_; }); 22 | } 23 | 24 | // Signals once the task is done. 25 | void notify() { 26 | { 27 | std::lock_guard lock(mtx_); 28 | flag_ = true; 29 | cv_.notify_all(); 30 | } 31 | } 32 | 33 | bool is_set () { 34 | std::lock_guard lock(mtx_); 35 | return flag_; 36 | } 37 | 38 | private: 39 | bool flag_ = false; 40 | std::condition_variable cv_; 41 | std::mutex mtx_; 42 | }; 43 | 44 | } // namespace detail 45 | } // namespace coros 46 | 47 | #endif // COROS_INCLUDE_START_BARRIER_H_ 48 | -------------------------------------------------------------------------------- /benchmarks/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int g_thread_num; 4 | 5 | #include "coros_fib.h" 6 | #include "coros_mat.h" 7 | 8 | #include "openmp_fib.h" 9 | #include "openmp_mat.h" 10 | 11 | #include 12 | 13 | void make_test(std::string file_name, std::function bench_func) { 14 | std::ofstream outFile(file_name); 15 | if (outFile.is_open()) { 16 | for (int i = 0; i < 50; i++) { 17 | outFile << bench_func() << std::endl; 18 | } 19 | outFile.close(); 20 | } else { 21 | std::cerr << "Unable to open file for writing" << std::endl; 22 | } 23 | } 24 | 25 | int main(int argc, char const *argv[]) { 26 | 27 | if (argc != 2) { 28 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 29 | } 30 | 31 | g_thread_num = std::atoi(argv[1]); 32 | 33 | make_test("openmp_fib_" + std::to_string(g_thread_num) + ".txt", bench_openmp_fib); 34 | make_test("openmp_matmul_" + std::to_string(g_thread_num) + ".txt", bench_openmp_matmul); 35 | 36 | make_test("workstealing_fib_" + std::to_string(g_thread_num) + ".txt", bench_workstealing_fib); 37 | make_test("workstealing_matmul_" + std::to_string(g_thread_num) + ".txt", bench_workstealing_matmul); 38 | 39 | return 0; 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /.github/workflows/gcc_test.yml: -------------------------------------------------------------------------------- 1 | name: GCC 13 2 | 3 | # Trigger the workflow on push or pull request to the main branch 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest # Use the latest Ubuntu environment 15 | 16 | steps: 17 | # Step 1: Checkout the code from the repository 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | # Step 2: Install GCC 13 and CMake 22 | - name: Install GCC 13 and CMake 23 | run: | 24 | sudo apt update 25 | sudo apt install -y software-properties-common 26 | sudo add-apt-repository ppa:ubuntu-toolchain-r/test 27 | sudo apt update 28 | sudo apt install -y gcc-13 g++-13 cmake 29 | 30 | # Step 3: Set GCC 13 as the default compiler 31 | - name: Set GCC 13 as default 32 | run: | 33 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 34 | sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 100 35 | 36 | # Step 4: Create a build directory and build the project with CMake 37 | - name: Build project 38 | run: | 39 | mkdir -p build 40 | cd build 41 | cmake .. 42 | make 43 | 44 | - name: Run tests 45 | run: | 46 | cd build/tests 47 | ./coros_test 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /include/test_deque.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_TEST_DEQUE_H_ 2 | #define COROS_INCLUDE_TEST_DEQUE_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "task_life_time.h" 11 | 12 | namespace coros { 13 | namespace test { 14 | 15 | // Deque used for testing. Utilizes mutex. 16 | class TestDeque { 17 | public: 18 | void pushBottom(std::pair, detail::TaskLifeTime> value) { 19 | std::lock_guard lock(mutex_); 20 | deque_.push_back(value); 21 | } 22 | 23 | std::optional> popBottom() { 24 | std::lock_guard lock(mutex_); 25 | if (!deque_.empty()) { 26 | std::pair, detail::TaskLifeTime> value = deque_.back(); 27 | deque_.pop_back(); 28 | return value.first; 29 | } 30 | return std::nullopt; 31 | } 32 | 33 | std::optional> steal() { 34 | std::lock_guard lock(mutex_); 35 | if (!deque_.empty()) { 36 | std::pair, detail::TaskLifeTime> value = deque_.front(); 37 | deque_.pop_front(); 38 | return value.first; 39 | } 40 | return std::nullopt; 41 | } 42 | 43 | bool empty() { 44 | std::lock_guard lock(mutex_); 45 | return deque_.empty(); 46 | } 47 | 48 | ~TestDeque() { 49 | for (auto& [handle, life_time] : deque_) { 50 | if (life_time == detail::TaskLifeTime::THREAD_POOL_MANAGED) { 51 | handle.destroy(); 52 | } 53 | } 54 | } 55 | 56 | private: 57 | std::deque, detail::TaskLifeTime>> deque_; 58 | std::mutex mutex_; 59 | }; 60 | 61 | } // namespace detail 62 | } // namespace coros 63 | 64 | #endif // COROS_INCLUDE_TEST_DEQUE_H_ 65 | -------------------------------------------------------------------------------- /examples/simple_task_execution.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "start_tasks.h" 4 | 5 | 6 | coros::Task add_value(int val) { 7 | co_return val + 1; 8 | } 9 | 10 | coros::Task throw_exception(int val) { 11 | throw std::bad_alloc(); 12 | co_return val + 1; 13 | } 14 | 15 | 16 | int main () { 17 | // Construction of the thread pool, number of desired threads can 18 | // be specified. 19 | coros::ThreadPool tp{2}; 20 | 21 | // Construction of the task objects. This objects need to 22 | // outlive the execution time (cannot be destroyed before the 23 | // execution finishes). They hold the final value. 24 | coros::Task t1 = add_value(41); 25 | coros::Task t2 = throw_exception(41); 26 | 27 | // Starting tasks in the specified thread pool. This method 28 | // blocks until the tasks are finished. There is also an 29 | // async version of this method. 30 | coros::start_sync(tp, t1, t2); 31 | 32 | // If a task successfully finishes and stores the value 33 | // has_value() returns true. 34 | if (t1.has_value()) { 35 | // Operator *t1 allows for accessing the stored value inside the task. 36 | std::cout << "t1 value : " << *t1 << std::endl; 37 | } else { 38 | // Task did not finish successfully and an exception 39 | // has been thrown, which is stored inside the coros::Task 40 | // object. It can be retrieved with error() method, see below. 41 | } 42 | 43 | if (t2.has_value()) { 44 | std::cout << "t2 value : " << *t1 << std::endl; 45 | } else { 46 | // In case an unexpected value has been stored, in this case 47 | // an exception. It can be accessed with error() method. 48 | 49 | // For example it can be rethrown and processed. 50 | try { 51 | std::rethrow_exception(t2.error()); 52 | } 53 | catch (const std::exception& e) { 54 | std::cerr << "Exception caught from task: " << e.what() << std::endl; 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/clang_test.yml: -------------------------------------------------------------------------------- 1 | name: Clang 17 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Step 1: Checkout the code from the repository 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | # Step 2: Add LLVM repository and install Clang 17 and libc++ 19 | - name: Install Clang 17 and libc++ from LLVM repo 20 | run: | 21 | sudo apt update 22 | sudo apt install -y wget lsb-release software-properties-common 23 | # Add the official LLVM repository 24 | wget https://apt.llvm.org/llvm.sh 25 | chmod +x llvm.sh 26 | sudo ./llvm.sh 17 27 | sudo apt install -y libc++-17-dev libc++abi-17-dev libomp-17-dev 28 | 29 | # Step 3: Set Clang 17 as the default compiler and use libc++ 30 | - name: Set Clang 17 as default for both clang and clang++ 31 | run: | 32 | # Set clang and clang++ to Clang 17 33 | sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100 34 | sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-17 100 35 | sudo update-alternatives --set clang /usr/bin/clang-17 36 | sudo update-alternatives --set clang++ /usr/bin/clang++-17 37 | 38 | # Verify the correct versions are set 39 | clang --version 40 | clang++ --version 41 | 42 | # Step 4: Create a build directory and build the project with Clang and libc++ 43 | - name: Build project with Clang 17 and libc++ 44 | run: | 45 | mkdir -p build 46 | cd build 47 | cmake .. -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang 48 | make 49 | 50 | # Step 5: Run tests with CTest 51 | - name: Run tests 52 | run: | 53 | cd build/tests 54 | ./coros_test 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/add_build.yml: -------------------------------------------------------------------------------- 1 | name: address sanitizer 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Step 1: Checkout the code from the repository 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | # Step 2: Add LLVM repository and install Clang 17 and libc++ 19 | - name: Install Clang 17 and libc++ from LLVM repo 20 | run: | 21 | sudo apt update 22 | sudo apt install -y wget lsb-release software-properties-common 23 | # Add the official LLVM repository 24 | wget https://apt.llvm.org/llvm.sh 25 | chmod +x llvm.sh 26 | sudo ./llvm.sh 17 27 | sudo apt install -y libc++-17-dev libc++abi-17-dev libomp-17-dev 28 | 29 | # Step 3: Set Clang 17 as the default compiler and use libc++ 30 | - name: Set Clang 17 as default for both clang and clang++ 31 | run: | 32 | # Set clang and clang++ to Clang 17 33 | sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100 34 | sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-17 100 35 | sudo update-alternatives --set clang /usr/bin/clang-17 36 | sudo update-alternatives --set clang++ /usr/bin/clang++-17 37 | 38 | # Verify the correct versions are set 39 | clang --version 40 | clang++ --version 41 | 42 | # Step 4: Create a build directory and build the project with Clang and libc++ 43 | - name: Build project with Clang 17 and libc++ 44 | run: | 45 | mkdir -p build 46 | cd build 47 | cmake .. -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang 48 | make 49 | 50 | # Step 5: Run tests with CTest 51 | - name: Run tests 52 | run: | 53 | cd build/tests 54 | ./coros_test 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/test_build.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Step 1: Checkout the code from the repository 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | # Step 2: Add LLVM repository and install Clang 17 and libc++ 19 | - name: Install Clang 17 and libc++ from LLVM repo 20 | run: | 21 | sudo apt update 22 | sudo apt install -y wget lsb-release software-properties-common 23 | # Add the official LLVM repository 24 | wget https://apt.llvm.org/llvm.sh 25 | chmod +x llvm.sh 26 | sudo ./llvm.sh 17 27 | sudo apt install -y libc++-17-dev libc++abi-17-dev libomp-17-dev 28 | 29 | # Step 3: Set Clang 17 as the default compiler and use libc++ 30 | - name: Set Clang 17 as default for both clang and clang++ 31 | run: | 32 | # Set clang and clang++ to Clang 17 33 | sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100 34 | sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-17 100 35 | sudo update-alternatives --set clang /usr/bin/clang-17 36 | sudo update-alternatives --set clang++ /usr/bin/clang++-17 37 | 38 | # Verify the correct versions are set 39 | clang --version 40 | clang++ --version 41 | 42 | # Step 4: Create a build directory and build the project with Clang and libc++ 43 | - name: Build project with Clang 17 and libc++ 44 | run: | 45 | mkdir -p build 46 | cd build 47 | cmake .. -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DDEQUE=TEST 48 | make 49 | 50 | # Step 5: Run tests with CTest 51 | - name: Run tests 52 | run: | 53 | cd build/tests 54 | ./coros_test 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/tsan_build.yml: -------------------------------------------------------------------------------- 1 | name: thread sanitizer 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Step 1: Checkout the code from the repository 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | # Step 2: Add LLVM repository and install Clang 17 and libc++ 19 | - name: Install Clang 17 and libc++ from LLVM repo 20 | run: | 21 | sudo apt update 22 | sudo apt install -y wget lsb-release software-properties-common 23 | # Add the official LLVM repository 24 | wget https://apt.llvm.org/llvm.sh 25 | chmod +x llvm.sh 26 | sudo ./llvm.sh 17 27 | sudo apt install -y libc++-17-dev libc++abi-17-dev libomp-17-dev 28 | 29 | # Step 3: Set Clang 17 as the default compiler and use libc++ 30 | - name: Set Clang 17 as default for both clang and clang++ 31 | run: | 32 | # Set clang and clang++ to Clang 17 33 | sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100 34 | sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-17 100 35 | sudo update-alternatives --set clang /usr/bin/clang-17 36 | sudo update-alternatives --set clang++ /usr/bin/clang++-17 37 | 38 | # Verify the correct versions are set 39 | clang --version 40 | clang++ --version 41 | 42 | # Step 4: Create a build directory and build the project with Clang and libc++ 43 | - name: Build project with Clang 17 and libc++ 44 | run: | 45 | mkdir -p build 46 | cd build 47 | cmake .. -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DSANITIZER=thread 48 | make 49 | 50 | # Step 5: Run tests with CTest 51 | - name: Run tests 52 | run: | 53 | cd build/tests 54 | ./coros_test 55 | 56 | -------------------------------------------------------------------------------- /benchmarks/coros_mat.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "thread_pool.h" 5 | #include "wait_tasks.h" 6 | 7 | 8 | 9 | extern int g_thread_num; 10 | 11 | namespace { 12 | 13 | coros::Task matmul(int *a, int *b, std::atomic *c, int n, int N) { 14 | if (n <= 32) { 15 | // Base case: Use simple triple-loop multiplication for small matrices 16 | for (int i = 0; i < n; i++) { 17 | for (int j = 0; j < n; j++) { 18 | for (int k = 0; k < n; k++) { 19 | c[i * N + j].fetch_add(a[i * N + k] * b[k * N + j], std::memory_order::relaxed); 20 | } 21 | } 22 | } 23 | } else { 24 | // Recursive case: Divide the matrices into 4 submatrices and multiply them 25 | int k = n / 2; 26 | 27 | co_await coros::wait_tasks( 28 | matmul(a, b, c, k, N), 29 | matmul(a + k, b + k * N, c, k, N), 30 | matmul(a, b + k, c + k, k, N), 31 | matmul(a + k, b + k * N + k, c + k, k, N), 32 | matmul(a + k * N, b, c + k * N, k, N), 33 | matmul(a + k * N + k, b + k * N, c + k * N, k, N), 34 | matmul(a + k * N, b + k, c + k * N + k, k, N), 35 | matmul(a + k * N + k, b + k * N + k, c + k * N + k, k, N) 36 | ); 37 | } 38 | } 39 | 40 | } 41 | 42 | int bench_workstealing_matmul() { 43 | int N = 1024; 44 | int *A = new int[N * N]; 45 | int *B = new int[N * N]; 46 | std::atomic *C = new std::atomic[N * N]; 47 | 48 | for (int i = 0; i < N; i++) { 49 | for (int j = 0; j < N; j++) { 50 | A[i * N + j] = 1; 51 | B[i * N + j] = 1; 52 | C[i * N + j] = 0; 53 | } 54 | } 55 | 56 | coros::ThreadPool tp{g_thread_num}; 57 | coros::Task t = matmul(A, B, C, N, N); 58 | 59 | auto start = std::chrono::high_resolution_clock::now(); 60 | 61 | coros::start_sync(tp, t); 62 | 63 | auto end = std::chrono::high_resolution_clock::now(); 64 | auto duration = std::chrono::duration_cast(end - start); 65 | 66 | bool done = false; 67 | for (int i = 0; i < N && !done; i++) { 68 | for (int j = 0; j < N && !done; j++) { 69 | if (C[i * N + j] != N) { 70 | std::cout << "Wrong result Coros matmul : " << std::endl; 71 | done = true; 72 | } 73 | } 74 | } 75 | 76 | 77 | return duration.count(); 78 | } 79 | -------------------------------------------------------------------------------- /benchmarks/tbb/mat_tbb.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | extern int g_thread_num; 8 | 9 | void matmul(std::atomic *a, std::atomic *b, std::atomic *c, int n, int N) { 10 | if (n <= 32) { 11 | // Base case: Use simple triple-loop multiplication for small matrices 12 | for (int i = 0; i < n; i++) { 13 | for (int j = 0; j < n; j++) { 14 | for (int k = 0; k < n; k++) { 15 | c[i * N + j].fetch_add( 16 | a[i * N + k].load(std::memory_order::relaxed) * b[k * N + j].load(std::memory_order::relaxed), 17 | std::memory_order::relaxed); 18 | } 19 | } 20 | } 21 | } else { 22 | // Recursive case: Divide the matrices into 4 submatrices and multiply them 23 | int k = n / 2; 24 | 25 | tbb::parallel_invoke( 26 | [&]() { matmul(a, b, c, k, N); }, 27 | [&]() { matmul(a + k, b + k * N, c, k, N); }, 28 | [&]() { matmul(a, b + k, c + k, k, N); }, 29 | [&]() { matmul(a + k, b + k * N + k, c + k, k, N); }, 30 | [&]() { matmul(a + k * N, b, c + k * N, k, N); }, 31 | [&]() { matmul(a + k * N + k, b + k * N, c + k * N, k, N); }, 32 | [&]() { matmul(a + k * N, b + k, c + k * N + k, k, N); }, 33 | [&]() { matmul(a + k * N + k, b + k * N + k, c + k * N + k, k, N); } 34 | ); 35 | } 36 | } 37 | 38 | int bench_tbb_matmul() { 39 | int N = 1024; 40 | std::atomic *A = new std::atomic[N * N]; 41 | std::atomic *B = new std::atomic[N * N]; 42 | std::atomic *C = new std::atomic[N * N]; 43 | 44 | for (int i = 0; i < N; i++) { 45 | for (int j = 0; j < N; j++) { 46 | A[i * N + j] = 1; 47 | B[i * N + j] = 1; 48 | C[i * N + j] = 0; 49 | } 50 | } 51 | 52 | int result_time = -1; 53 | 54 | tbb::task_arena arena(4); 55 | 56 | auto start = std::chrono::high_resolution_clock::now(); 57 | 58 | arena.execute([&]{ 59 | matmul((std::atomic*)A, (std::atomic*)B, (std::atomic*)C, N, N); 60 | }); 61 | 62 | 63 | auto end = std::chrono::high_resolution_clock::now(); 64 | auto duration = std::chrono::duration_cast(end - start); 65 | result_time = duration.count(); 66 | 67 | 68 | bool done = false; 69 | for (int i = 0; i < N && !done; i++) { 70 | for (int j = 0; j < N && !done; j++) { 71 | if ( C[i * N + j] != N) { 72 | std::cout << "Wrong result OpenMP matmul : " << std::endl; 73 | done = true; 74 | } 75 | } 76 | } 77 | 78 | return result_time; 79 | } 80 | -------------------------------------------------------------------------------- /include/constructor_counter.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_INSTANCE_COUNTER_H_ 2 | #define COROS_INCLUDE_INSTANCE_COUNTER_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace coros { 8 | namespace test { 9 | 10 | // Used in tests for couting the number of instances alive. 11 | template 12 | class InstanceCounter { 13 | public: 14 | InstanceCounter() { 15 | count_.fetch_add(1, std::memory_order::relaxed); 16 | } 17 | 18 | ~InstanceCounter(){ 19 | count_.fetch_sub(1, std::memory_order::relaxed); 20 | } 21 | 22 | static std::size_t instance_count() { 23 | return count_.load(std::memory_order::relaxed); 24 | } 25 | 26 | private: 27 | inline static std::atomic count_ = 0; 28 | }; 29 | 30 | // Used for checking whether the correct constructor has been used. 31 | class ConstructorCounter { 32 | public: 33 | inline static std::atomic default_constructed = 0; 34 | inline static std::atomic copy_constructed = 0; 35 | inline static std::atomic move_constructed = 0; 36 | inline static std::atomic copy_assigned = 0; 37 | inline static std::atomic move_assigned = 0; 38 | 39 | // Default constructor 40 | ConstructorCounter() { 41 | default_constructed.fetch_add(1, std::memory_order_relaxed); 42 | } 43 | 44 | // Copy constructor 45 | ConstructorCounter(const ConstructorCounter&) { 46 | copy_constructed.fetch_add(1, std::memory_order_relaxed); 47 | } 48 | 49 | // Move constructor 50 | ConstructorCounter(ConstructorCounter&&) noexcept { 51 | move_constructed.fetch_add(1, std::memory_order_relaxed); 52 | } 53 | 54 | // Copy assignment operator 55 | ConstructorCounter& operator=(const ConstructorCounter&) { 56 | copy_assigned.fetch_add(1, std::memory_order_relaxed); 57 | return *this; 58 | } 59 | 60 | // Move assignment operator 61 | ConstructorCounter& operator=(ConstructorCounter&&) noexcept { 62 | move_assigned.fetch_add(1, std::memory_order_relaxed); 63 | return *this; 64 | } 65 | 66 | static void clear_count() { 67 | ConstructorCounter::default_constructed.store(0, std::memory_order_relaxed); 68 | ConstructorCounter::copy_constructed.store(0, std::memory_order_relaxed); 69 | ConstructorCounter::move_constructed.store(0, std::memory_order_relaxed); 70 | ConstructorCounter::copy_assigned.store(0, std::memory_order_relaxed); 71 | ConstructorCounter::move_assigned.store(0, std::memory_order_relaxed); 72 | } 73 | }; 74 | 75 | } // namespace detail 76 | } // namespace coros 77 | 78 | 79 | #endif // COROS_INCLUDE_INSTANCE_COUNTER_H_ 80 | -------------------------------------------------------------------------------- /benchmarks/openmp_mat.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "omp.h" 8 | 9 | extern int g_thread_num; 10 | 11 | typedef std::vector> matrix; 12 | 13 | 14 | namespace { 15 | void matmul(int *a, int *b, int *c, int n, int N) { 16 | if (n <= 32) { 17 | // Base case: Use simple triple-loop multiplication for small matrices 18 | for (int i = 0; i < n; i++) { 19 | for (int j = 0; j < n; j++) { 20 | for (int k = 0; k < n; k++) { 21 | #pragma omp atomic update relaxed 22 | c[i * N + j] += a[i * N + k] * b[k * N + j]; 23 | } 24 | } 25 | } 26 | } else { 27 | // Recursive case: Divide the matrices into 4 submatrices and multiply them 28 | int k = n / 2; 29 | 30 | #pragma omp task 31 | matmul(a, b, c, k, N); 32 | 33 | #pragma omp task 34 | matmul(a + k, b + k * N, c, k, N); 35 | 36 | #pragma omp task 37 | matmul(a, b + k, c + k, k, N); 38 | 39 | #pragma omp task 40 | matmul(a + k, b + k * N + k, c + k, k, N); 41 | 42 | #pragma omp task 43 | matmul(a + k * N, b, c + k * N, k, N); 44 | 45 | #pragma omp task 46 | matmul(a + k * N + k, b + k * N, c + k * N, k, N); 47 | 48 | #pragma omp task 49 | matmul(a + k * N, b + k, c + k * N + k, k, N); 50 | 51 | #pragma omp task 52 | matmul(a + k * N + k, b + k * N + k, c + k * N + k, k, N); 53 | 54 | #pragma omp taskwait // Wait for all tasks to complete before returning 55 | } 56 | } 57 | 58 | } 59 | 60 | int bench_openmp_matmul() { 61 | int N = 1024; 62 | int *A = new int[N * N]; 63 | int *B = new int[N * N]; 64 | int *C = new int[N * N]; 65 | 66 | for (int i = 0; i < N; i++) { 67 | for (int j = 0; j < N; j++) { 68 | A[i * N + j] = 1; 69 | B[i * N + j] = 1; 70 | C[i * N + j] = 0; 71 | } 72 | } 73 | 74 | int result; 75 | omp_set_num_threads(g_thread_num); 76 | 77 | #pragma omp parallel 78 | { 79 | #pragma omp single 80 | { 81 | auto start = std::chrono::high_resolution_clock::now(); 82 | 83 | matmul((int *)A, (int *)B, (int *)C, N, N); 84 | 85 | auto end = std::chrono::high_resolution_clock::now(); 86 | auto duration = std::chrono::duration_cast(end - start); 87 | result = duration.count(); 88 | } 89 | } 90 | 91 | bool done = false; 92 | 93 | for (int i = 0; i < N && !done; i++) { 94 | for (int j = 0; j < N && !done; j++) { 95 | if ( C[i * N + j] != N) { 96 | std::cout << "Wrong result OpenMP matmul : " << std::endl; 97 | done = true; 98 | } 99 | } 100 | } 101 | 102 | 103 | return result; 104 | } 105 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(coros_test) 2 | 3 | set(CMAKE_CXX_STANDARD 23) 4 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 5 | 6 | # Define the Clang include directory variable 7 | # set(CLANG_INCLUDE_DIR /usr/lib/clang/18/include) 8 | 9 | include(FetchContent) 10 | FetchContent_Declare( 11 | googletest 12 | GIT_REPOSITORY https://github.com/google/googletest.git 13 | GIT_TAG v1.12.0 14 | ) 15 | 16 | FetchContent_MakeAvailable(googletest) 17 | 18 | # add test files 19 | add_executable ( 20 | coros_test 21 | task_test.cpp 22 | thread_pool_test.cpp 23 | start_tasks_test.cpp 24 | wait_barrier_test.cpp 25 | wait_task_test.cpp 26 | deque_test.cpp 27 | enqueue_tasks_test.cpp 28 | chain_test.cpp 29 | ) 30 | 31 | target_include_directories(coros_test 32 | PUBLIC ${CMAKE_SOURCE_DIR}/include 33 | ) 34 | 35 | target_link_libraries(coros_test gtest_main) 36 | 37 | # Macro to enable additional testing functionalities and tests in code. 38 | # target_compile_definitions(coros_test PRIVATE COROS_TEST_ COROS_TEST_DEQUE_) 39 | # target_compile_definitions(coros_test PRIVATE COROS_TEST_ ) 40 | 41 | # Set default sanitizer to address if not provided 42 | if (NOT DEFINED DEQUE) 43 | set(DEQUE "NO") 44 | endif() 45 | 46 | # Whether to use testing deque. 47 | if (DEQUE STREQUAL "TEST") 48 | target_compile_definitions(coros_test PRIVATE COROS_TEST_ COROS_TEST_DEQUE_) 49 | else() 50 | # This needs to be set for the tests to work. 51 | target_compile_definitions(coros_test PRIVATE COROS_TEST_ ) 52 | endif() 53 | 54 | # Set default sanitizer to address if not provided 55 | if (NOT DEFINED SANITIZER) 56 | set(SANITIZER "address") 57 | endif() 58 | 59 | if (SANITIZER STREQUAL "thread") 60 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 61 | target_compile_options(coros_test PRIVATE -fsanitize=thread -Wno-interference-size -g) 62 | target_link_options(coros_test PRIVATE -fsanitize=thread -g) 63 | elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 64 | #target_include_directories(coros_test PRIVATE CLANG_INCLUDE_DIR) 65 | target_compile_options(coros_test PRIVATE 66 | -stdlib=libc++ 67 | -fsanitize=thread 68 | -g 69 | ) 70 | target_link_options(coros_test PRIVATE 71 | -stdlib=libc++ 72 | -fsanitize=thread 73 | -g 74 | ) 75 | endif() 76 | elseif (SANITIZER STREQUAL "address") 77 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 78 | target_compile_options(coros_test PRIVATE -fsanitize=address,leak,undefined -Wno-interference-size -g) 79 | target_link_options(coros_test PRIVATE -fsanitize=address,leak,undefined -g) 80 | elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 81 | #target_include_directories(coros_test PRIVATE CLANG_INCLUDE_DIR) 82 | target_compile_options(coros_test PRIVATE 83 | -stdlib=libc++ 84 | -fsanitize=address,leak,undefined 85 | -g 86 | ) 87 | target_link_options(coros_test PRIVATE 88 | -stdlib=libc++ 89 | -fsanitize=address,leak,undefined 90 | -Winterference-size 91 | -g 92 | ) 93 | endif() 94 | else() 95 | message(WARNING "Unknown sanitizer: ${SANITIZER}. No sanitizer will be applied.") 96 | endif() 97 | 98 | include(GoogleTest) 99 | gtest_discover_tests(coros_test) 100 | -------------------------------------------------------------------------------- /examples/waiting.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "start_tasks.h" 4 | #include "wait_tasks.h" 5 | 6 | 7 | // Helper function, just for demonstration. 8 | coros::Task multiply(int val, int mul) { 9 | co_return val * mul; 10 | } 11 | 12 | // This tasks waits for two other tasks to finish. 13 | coros::Task generate_answer() { 14 | 15 | coros::Task t1 = multiply(20, 2); 16 | 17 | // Suspends current execution and only resumes once the individual tasks are finished. 18 | // 19 | // Notice: with temporary task created with lambda function we cannot retrieve the value. 20 | co_await coros::wait_tasks(t1, 21 | [](int val, int mul) -> coros::Task{ 22 | // This task does the same thing as the 23 | // multiply task, however, we cannot retrieve 24 | // value from a temporary task. 25 | co_return val * mul; 26 | }(20, 2)); 27 | 28 | // We assume the value has been correctly stored, not checking with has_value(). 29 | co_return *t1 + 2; 30 | } 31 | 32 | // Same version as a task above, except this tasks is resumed 33 | // on another threadpool. 34 | coros::Task generate_answer_threadpool(coros::ThreadPool& tp2) { 35 | 36 | coros::Task t1 = multiply(20, 2); 37 | 38 | // When a threadpool is specified the suspended tasks is resumed on the 39 | // threadpool tp2. 40 | co_await coros::wait_tasks(tp2, 41 | t1, 42 | [](int val, int mul) -> coros::Task{ 43 | co_return val * mul; 44 | }(20, 2)); 45 | 46 | // This executed on the threadpool tp2. 47 | co_return *t1 + 2; 48 | } 49 | 50 | // Waits for the tasks asynchronously. 51 | coros::Task generate_answer_async() { 52 | 53 | coros::Task t1 = multiply(20, 2); 54 | 55 | // We store the awaitable so we can do some work and check if the tasks 56 | // are finished later. 57 | auto awaitable = coros::wait_tasks_async(t1, 58 | [](int val, int mul) -> coros::Task{ 59 | co_return val * mul; 60 | }(20, 2)); 61 | // 62 | // Do some work 63 | // 64 | 65 | // Either suspends current tasks and resumes it later, 66 | // or in case when tasks has already finished, the execution 67 | // simply continues without suspension. 68 | co_await awaitable; 69 | 70 | co_return *t1 + 2; 71 | } 72 | 73 | int main () { 74 | coros::ThreadPool tp{/*number_of_threads=*/2}; 75 | coros::ThreadPool tp2{/*number_of_threads=*/2}; 76 | 77 | coros::Task wait_task = generate_answer(); 78 | // In this tasks the suspended task and individual tasks 79 | // are executed on a different threadpool. 80 | coros::Task wait_task_pool = generate_answer_threadpool(tp2); 81 | // Waits for tasks asynchronously. 82 | coros::Task wait_task_async = generate_answer_async(); 83 | 84 | // start_async returns a start_task 85 | auto start_task = coros::start_async(tp, 86 | wait_task, 87 | wait_task_pool, 88 | wait_task_async); 89 | 90 | // Calling wait on the start_task, in case task are already finished 91 | // execution continues. Otherwise the call blocks until the tasks are finished. 92 | start_task.wait(); 93 | 94 | std::cout << "value of wait_task : " << *wait_task << std::endl; 95 | std::cout << "value of wait_task_pool : " << *wait_task_pool << std::endl; 96 | std::cout << "value of wait_task_async : " << *wait_task_async << std::endl; 97 | } 98 | -------------------------------------------------------------------------------- /examples/chaining.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "chain_tasks.h" 4 | #include "start_tasks.h" 5 | 6 | coros::Task add_two(int val) { 7 | co_return val + 2; 8 | } 9 | 10 | coros::Task multiply_by_six(double val) { 11 | co_return val * 6; 12 | } 13 | 14 | coros::Task accept_int(int val) { 15 | co_return; 16 | } 17 | 18 | coros::Task accept_nothing() { 19 | co_return; 20 | } 21 | 22 | coros::Task return_three() { 23 | co_return 3; 24 | } 25 | 26 | coros::Task compute_value() { 27 | 28 | // Each task must accept at most one argument 29 | // 30 | // Each subsequent task's parameter must be 31 | // constructible from the previous task return value. 32 | std::expected res = 33 | co_await coros::chain_tasks(3).and_then(add_two) 34 | .and_then(add_two) 35 | .and_then(multiply_by_six); 36 | if (res.has_value()) { 37 | // Return computed value. 38 | co_return *res; 39 | } else { 40 | // In case the chain did not sucessfuly finish. 41 | co_return -1; 42 | } 43 | } 44 | 45 | coros::Task compute_value_void() { 46 | 47 | // The value is "lost" in the chain and resulting 48 | // expected is of type void. 49 | std::expected res = 50 | co_await coros::chain_tasks(3).and_then(add_two) 51 | .and_then(add_two) 52 | .and_then(multiply_by_six) 53 | .and_then(accept_int) 54 | .and_then(accept_nothing) 55 | .and_then(accept_nothing); 56 | if (res.has_value()) { 57 | // Chain sucessfuly finished. 58 | co_return 1; 59 | } else { 60 | // Chain did not sucessfully finished. 61 | co_return -1; 62 | } 63 | } 64 | 65 | coros::Task compute_value_task() { 66 | // It is possible to pass a unfinished/unexecuted task 67 | // object into the chain_tasks. The starting task will 68 | // be executed and then all the folowing tasks. 69 | std::expected res = 70 | co_await coros::chain_tasks(return_three()).and_then(add_two) 71 | .and_then(add_two) 72 | .and_then(multiply_by_six); 73 | if (res.has_value()) { 74 | // Chain sucessfuly finished. 75 | co_return 1; 76 | } else { 77 | // Chain did not sucessfully finished. 78 | co_return -1; 79 | } 80 | } 81 | 82 | coros::Task compute_value_finished_task() { 83 | coros::Task task = return_three(); 84 | 85 | coros::wait_tasks(task); 86 | 87 | // Chain can also be started with a finished task. 88 | std::expected res = 89 | co_await coros::chain_tasks(*task).and_then(add_two) 90 | .and_then(add_two) 91 | .and_then(multiply_by_six); 92 | if (res.has_value()) { 93 | // Chain sucessfuly finished. 94 | co_return 1; 95 | } else { 96 | // Chain did not sucessfully finished. 97 | co_return -1; 98 | } 99 | } 100 | 101 | 102 | int main () { 103 | coros::ThreadPool tp(2); 104 | 105 | coros::Task t1 = compute_value(); 106 | coros::Task t2 = compute_value_void(); 107 | coros::Task t3 = compute_value_task(); 108 | coros::Task t4 = compute_value_finished_task(); 109 | 110 | coros::start_sync(tp, t1, t2, t3, t4); 111 | 112 | std::cout << "value stored in t1 : " << *t1 << std::endl; 113 | std::cout << "value stored in t2 : " << *t2 << std::endl; 114 | std::cout << "value stored in t3 : " << *t3 << std::endl; 115 | std::cout << "value stored in t4 : " << *t4 << std::endl; 116 | } 117 | -------------------------------------------------------------------------------- /include/wait_barrier.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_WAIT_BARRIER_H_ 2 | #define COROS_INCLUDE_WAIT_BARRIER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace coros { 9 | namespace detail { 10 | 11 | // Barrier is constructed with number of tasks that need to finish 12 | // and and a coroutine handle of the task that waits for them. Once 13 | // all tasks are finished the last task takes the handle and resumes 14 | // the waiting task(which is suspended). 15 | class WaitBarrier { 16 | public: 17 | 18 | std::coroutine_handle<> decrement_and_resume() noexcept { 19 | int remaining = remaining_tasks.fetch_sub(1, std::memory_order_acq_rel); 20 | //int remaining = remaining_tasks.fetch_sub(1, std::memory_order::seq_cst); 21 | if (remaining == 1) { 22 | return continuation; 23 | } else { 24 | return std::noop_coroutine(); 25 | } 26 | } 27 | 28 | uint_fast64_t get_counter() { 29 | return remaining_tasks; 30 | } 31 | 32 | void set_continuation(std::coroutine_handle<> handle) { 33 | continuation = handle; 34 | } 35 | 36 | std::coroutine_handle<> get_continuation () { 37 | return continuation; 38 | } 39 | 40 | std::atomic remaining_tasks; 41 | // coroutine to resume, when the all tasks are done 42 | std::coroutine_handle<> continuation; 43 | }; 44 | 45 | class WaitBarrierAsync { 46 | public: WaitBarrierAsync(int task_number) noexcept 47 | : remaining_tasks(task_number), continuation(nullptr), 48 | handle_ready_(false) {} 49 | 50 | // Each thread needs to make a copy of the shared_ptr to instance of this object. 51 | // This prevents a scenario where compare_exchange_strong is called on deleted 52 | // barrier. This can happen : 53 | // 54 | // 1. Each task decrements the barrier. This allows the awaiter in other 55 | // thread to resume the coroutine. There is not need to wait. When all tasks 56 | // decremented the barrier, signalling they are done. 57 | // 2. Last tasks tries to check wheter it should resume the coroutine or 58 | // not through compare_exchange_strong. However, the barrier is already 59 | // destroyed. 60 | // 61 | // Task cannot know if itself is the last task until it decrements the barrier. 62 | // Therefore every task needs to make the copy of the pointer before decrement, 63 | // keeping the barrier alive. 64 | std::coroutine_handle<> decrement_and_resume() noexcept { 65 | std::shared_ptr barrier = std::atomic_load(barrier_p_); 66 | int remaining = remaining_tasks.fetch_sub(1); 67 | if (remaining == 1) { 68 | // Last task. 69 | bool expected = true; 70 | if (handle_ready_.compare_exchange_strong(expected, false)) { 71 | // This tasks resumes the coroutine and sets flag for the awaiter, so it 72 | // doesn't resume the coroutine. 73 | return continuation.load(); 74 | } else { 75 | // Flag is not set. The awaiter will resume the coroutine. 76 | return std::noop_coroutine(); 77 | } 78 | } else { 79 | return std::noop_coroutine(); 80 | } 81 | } 82 | 83 | uint_fast64_t get_counter() { 84 | return remaining_tasks.load(); 85 | } 86 | 87 | void set_continuation(std::coroutine_handle<> handle) { 88 | continuation = handle; 89 | } 90 | 91 | void set_barrier_pointer(std::shared_ptr* barrier_p) { 92 | barrier_p_ = barrier_p; 93 | } 94 | 95 | std::coroutine_handle<> get_continuation () { 96 | return continuation; 97 | } 98 | 99 | std::atomic remaining_tasks; 100 | // Coroutine to resume, when the all tasks are done. Initialized to nullptr 101 | std::atomic> continuation; 102 | std::shared_ptr* barrier_p_; 103 | std::atomic handle_ready_; 104 | }; 105 | 106 | } // namespace detail 107 | } // namespace coros 108 | 109 | 110 | #endif // COROS_INCLUDE_WAIT_BARRIER_H_ 111 | -------------------------------------------------------------------------------- /tests/start_tasks_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "thread_pool.h" 3 | 4 | #include "start_tasks.h" 5 | 6 | TEST(StartingTaskTest, BasicAssertions) { 7 | coros::StartTask t = []() -> coros::StartTask { 8 | co_return; 9 | }(); 10 | } 11 | 12 | TEST(StartingTaskTest, BarrierSetReset) { 13 | coros::StartTask t = []() -> coros::StartTask { co_return; }(); 14 | 15 | bool v = t.get_handle().promise().get_barrier().is_set(); 16 | EXPECT_EQ(v, false); 17 | 18 | t.get_handle().resume(); 19 | 20 | // task is done here 21 | v = t.get_handle().promise().get_barrier().is_set(); 22 | EXPECT_EQ(v, true); 23 | } 24 | 25 | TEST(StartingTaskTest, SyncTask) { 26 | coros::ThreadPool tp{1}; 27 | coros::Task t = []() -> coros::Task { co_return 42; }(); 28 | 29 | coros::start_sync(tp, t); 30 | EXPECT_EQ(t.value(), 42); 31 | } 32 | 33 | 34 | TEST(StartingTaskTest, AsyncWait) { 35 | coros::ThreadPool tp(1); 36 | coros::Task t = []() -> coros::Task { co_return 42; }(); 37 | 38 | auto bt = coros::start_async(tp, t); 39 | 40 | bt.wait(); 41 | 42 | EXPECT_EQ(*t, 42); 43 | } 44 | 45 | // Test async_wait, when task2 completes before task1. 46 | TEST(StartingTaskTest, AsyncWaitTwoTasks) { 47 | 48 | for (int i = 0; i < 1000;i++) { 49 | coros::ThreadPool tp1(1); 50 | coros::ThreadPool tp2(1); 51 | 52 | std::atomic flag = false; 53 | 54 | coros::Task t1 = [](std::atomic& flag) -> coros::Task { 55 | while(flag == false); 56 | co_return 43; 57 | }(flag); 58 | coros::Task t2 = [](std::atomic& flag) -> coros::Task { 59 | flag = true; 60 | co_return 42; 61 | }(flag); 62 | 63 | auto bt1 = coros::start_async(tp1, t1); 64 | auto bt2 = coros::start_async(tp2, t2); 65 | 66 | bt1.wait(); 67 | bt2.wait(); 68 | 69 | EXPECT_EQ(t1.value(), 43); 70 | EXPECT_EQ(t2.value(), 42); 71 | } 72 | } 73 | 74 | TEST(StartingTaskTest, AsyncAwaitMultipleTasks) { 75 | coros::ThreadPool tp(2); 76 | 77 | coros::Task t1 = []() -> coros::Task {co_return 42;}(); 78 | coros::Task t2 = []() -> coros::Task {co_return 43;}(); 79 | coros::Task t3 = []() -> coros::Task {co_return 44;}(); 80 | 81 | auto bt1 = coros::start_async(tp, t1, t2, t3); 82 | 83 | bt1.wait(); 84 | 85 | EXPECT_EQ(*t1, 42); 86 | EXPECT_EQ(*t2, 43); 87 | EXPECT_EQ(*t3, 44); 88 | } 89 | 90 | TEST(StartingTaskTest, AsyncAwaitLambda) { 91 | coros::ThreadPool tp(2); 92 | 93 | std::atomic n1 = 0; 94 | std::atomic n2 = 0; 95 | std::atomic n3 = 0; 96 | 97 | auto bt = coros::start_async( 98 | tp, 99 | [](std::atomic& n1) -> coros::Task {n1 = 42; co_return;}(n1), 100 | [](std::atomic& n2) -> coros::Task {n2 = 43; co_return;}(n2), 101 | [](std::atomic& n3) -> coros::Task {n3 = 44; co_return;}(n3) 102 | ); 103 | 104 | bt.wait(); 105 | 106 | EXPECT_EQ(n1, 42); 107 | EXPECT_EQ(n2, 43); 108 | EXPECT_EQ(n3, 44); 109 | } 110 | 111 | 112 | TEST(StartingTaskTest, SyncWaitMultipleTasks) { 113 | coros::ThreadPool tp(2); 114 | 115 | coros::Task t1 = []() -> coros::Task {co_return 42;}(); 116 | coros::Task t2 = []() -> coros::Task {co_return 43;}(); 117 | coros::Task t3 = []() -> coros::Task {co_return 44;}(); 118 | 119 | coros::start_sync(tp, t1, t2, t3); 120 | 121 | EXPECT_EQ(t1.value(), 42); 122 | EXPECT_EQ(t2.value(), 43); 123 | EXPECT_EQ(t3.value(), 44); 124 | } 125 | 126 | TEST(StartingTaskTest, SyncWaitLambda) { 127 | coros::ThreadPool tp(2); 128 | 129 | std::atomic n1 = 0; 130 | std::atomic n2 = 0; 131 | std::atomic n3 = 0; 132 | 133 | coros::start_sync( 134 | tp, 135 | [](std::atomic& n1) -> coros::Task {n1 = 42; co_return;}(n1), 136 | [](std::atomic& n2) -> coros::Task {n2 = 43; co_return;}(n2), 137 | [](std::atomic& n3) -> coros::Task {n3 = 44; co_return;}(n3) 138 | ); 139 | 140 | EXPECT_EQ(n1, 42); 141 | EXPECT_EQ(n2, 43); 142 | EXPECT_EQ(n3, 44); 143 | } 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /include/start_tasks.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_START_TASKS_H_ 2 | #define COROS_INCLUDE_START_TASKS_H_ 3 | 4 | #include 5 | 6 | #include "start_barrier.h" 7 | #include "wait_tasks.h" 8 | #include "thread_pool.h" 9 | 10 | namespace coros { 11 | 12 | class StartTask; 13 | 14 | namespace detail { 15 | 16 | class StartTaskPromise; 17 | 18 | class NotifyBarrierAwaiter { 19 | public: 20 | NotifyBarrierAwaiter(StartingBarrier& barrier) : barrier_(barrier){}; 21 | ~NotifyBarrierAwaiter(){}; 22 | 23 | constexpr bool await_ready() const noexcept { return false; } 24 | 25 | void await_suspend([[maybe_unused]] std::coroutine_handle handle) noexcept { 26 | barrier_.notify(); 27 | } 28 | 29 | void await_resume() noexcept {} 30 | private: 31 | StartingBarrier& barrier_; 32 | }; 33 | 34 | class StartTaskPromise { 35 | public: 36 | friend class NotifyBarrierAwaiter; 37 | 38 | auto get_return_object(); 39 | 40 | std::suspend_always initial_suspend() noexcept { return {}; } 41 | 42 | NotifyBarrierAwaiter final_suspend() noexcept { 43 | return NotifyBarrierAwaiter{barrier_}; 44 | }; 45 | 46 | void return_void() {} 47 | 48 | void unhandled_exception() { std::rethrow_exception(std::current_exception()); } 49 | 50 | StartingBarrier& get_barrier() { return barrier_; } 51 | 52 | bool is_finished() { 53 | return barrier_.is_set(); 54 | } 55 | 56 | private: 57 | // barrier used to signal that the task is finished 58 | StartingBarrier barrier_; 59 | }; 60 | 61 | 62 | } // namespace detail 63 | 64 | // This tasks is used to wrap task created in main thread. 65 | // Once the wrapped task finishes its execution, control 66 | // is returned to this task, which notifies the barrier. 67 | class StartTask { 68 | public: 69 | using promise_type = detail::StartTaskPromise; 70 | 71 | 72 | StartTask(std::coroutine_handle handle) : handle_(handle){}; 73 | 74 | StartTask(const StartTask& other) = delete; 75 | 76 | StartTask(StartTask&& other) { 77 | handle_ = other.handle_; 78 | other.handle_ = nullptr; 79 | }; 80 | 81 | StartTask operator=(StartTask&& other) = delete; 82 | StartTask operator=(const StartTask& other) = delete; 83 | 84 | ~StartTask() { 85 | if (handle_) { 86 | handle_.destroy(); 87 | } 88 | }; 89 | 90 | // Wait for the barrier 91 | void wait() { 92 | handle_.promise().get_barrier().wait(); 93 | } 94 | 95 | std::coroutine_handle get_handle() { return handle_; } 96 | 97 | private: 98 | std::coroutine_handle handle_ = nullptr; 99 | }; 100 | 101 | namespace detail { 102 | 103 | inline auto StartTaskPromise::get_return_object() { 104 | return StartTask{std::coroutine_handle::from_promise(*this)}; 105 | } 106 | 107 | // Lifetime of the tasks is managed by the WaitForTasksAwaiter. 108 | // In case of an R-value the task object lives in the awaiter. 109 | template 110 | StartTask start_task_body(Args&&... tasks) { 111 | co_await wait_tasks(std::forward(tasks)...); 112 | co_return; 113 | } 114 | 115 | template 116 | StartTask start_task_body_async(ThreadPool& scheduler, Args&&... tasks) { 117 | co_await wait_tasks(scheduler, std::forward(tasks)...); 118 | co_return; 119 | } 120 | 121 | } // namespace detail 122 | 123 | // Allows for synchronous waiting, calls wait() on the barrier. 124 | // Schedules the task, and waits for completion of the wrapping barrier task. 125 | template 126 | void start_sync(ThreadPool& scheduler, Args&&... args) { 127 | // Wrap task into a barrier task. 128 | // Tasks can only be co_awaited inside an another coroutine. 129 | StartTask bt = detail::start_task_body(std::forward(args)...); 130 | scheduler.add_task_from_outside({bt.get_handle(), detail::TaskLifeTime::SCOPE_MANAGED}); 131 | bt.get_handle().promise().get_barrier().wait(); 132 | } 133 | 134 | // Return a barrier task, user must call wait. 135 | template 136 | [[nodiscard]] StartTask start_async(ThreadPool& scheduler, Args&&... args) { 137 | StartTask bt = detail::start_task_body_async(scheduler, std::forward(args)...); 138 | // At this point the R-value tasks are moved. 139 | // And we can sefaly move the task into a scheduler. 140 | bt.get_handle().resume(); 141 | // Put task into the scheduler. 142 | return bt; 143 | } 144 | 145 | } // namespace coros 146 | 147 | #endif // COROS_INCLUDE_START_TASKS_H_ 148 | -------------------------------------------------------------------------------- /tests/deque_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "deque.h" 8 | #include "task_life_time.h" 9 | #include "enqueue_tasks.h" 10 | 11 | 12 | TEST(DequeTest, PushAndPop) { 13 | 14 | std::unique_ptr q = std::make_unique(); 15 | //coros::detail::Dequeue q; 16 | q->pushBottom({std::noop_coroutine(), coros::detail::TaskLifeTime::NOOP}); 17 | 18 | EXPECT_TRUE(q->popBottom().has_value()); 19 | EXPECT_FALSE(q->popBottom().has_value()); 20 | } 21 | 22 | TEST(DequeTest, Steal) { 23 | std::unique_ptr q = std::make_unique(); 24 | q->pushBottom({std::noop_coroutine(), coros::detail::TaskLifeTime::NOOP}); 25 | 26 | EXPECT_TRUE(q->steal().has_value()); 27 | EXPECT_FALSE(q->steal().has_value()); 28 | EXPECT_FALSE(q->popBottom().has_value()); 29 | } 30 | 31 | TEST(DequeTest, ConcurrentPushSteal) { 32 | std::unique_ptr q = std::make_unique(); 33 | 34 | std::thread producer([&](){ 35 | for (int i = 0; i < 1'000; i++) { 36 | q->pushBottom({std::noop_coroutine(), coros::detail::TaskLifeTime::NOOP}); 37 | } 38 | }); 39 | 40 | std::thread consumer([&](){ 41 | int count = 0; 42 | while(count != 1'000) { 43 | if (q->steal().has_value()) { 44 | count++; 45 | } 46 | } 47 | }); 48 | 49 | producer.join(); 50 | consumer.join(); 51 | } 52 | 53 | TEST(DequeTest, BufferGrowTest) { 54 | std::unique_ptr q = std::make_unique(2,1); 55 | EXPECT_EQ(q->get_buffer_size(), 2); 56 | q->pushBottom({std::noop_coroutine(), coros::detail::TaskLifeTime::NOOP}); 57 | q->pushBottom({std::noop_coroutine(), coros::detail::TaskLifeTime::NOOP}); 58 | q->pushBottom({std::noop_coroutine(), coros::detail::TaskLifeTime::NOOP}); 59 | 60 | // we double in size 61 | EXPECT_EQ(q->get_buffer_size(), 8); 62 | 63 | EXPECT_TRUE(q->popBottom().has_value()); 64 | EXPECT_TRUE(q->popBottom().has_value()); 65 | EXPECT_TRUE(q->popBottom().has_value()); 66 | EXPECT_FALSE(q->popBottom().has_value()); 67 | 68 | EXPECT_EQ(q->get_buffer_size(), 8); 69 | } 70 | 71 | TEST(DequeTest, DestructionNoWaitTask) { 72 | { 73 | EXPECT_EQ(coros::Task::instance_count(), 0); 74 | EXPECT_EQ(coros::NoWaitTaskPromise::instance_count(), 0); 75 | std::unique_ptr q = std::make_unique(2,1); 76 | 77 | auto t1 = []() -> coros::NoWaitTask{ co_return; }(); 78 | auto t2 = []() -> coros::NoWaitTask{ co_return; }(); 79 | auto t3 = []() -> coros::NoWaitTask{ co_return; }(); 80 | auto t4 = []() -> coros::NoWaitTask{ co_return; }(); 81 | 82 | EXPECT_EQ(q->get_buffer_size(), 2); 83 | EXPECT_EQ(coros::NoWaitTaskPromise::instance_count(), 4); 84 | 85 | q->pushBottom({t1.get_handle(), coros::detail::TaskLifeTime::THREAD_POOL_MANAGED}); 86 | q->pushBottom({t2.get_handle(), coros::detail::TaskLifeTime::THREAD_POOL_MANAGED}); 87 | q->pushBottom({t3.get_handle(), coros::detail::TaskLifeTime::THREAD_POOL_MANAGED}); 88 | q->pushBottom({t4.get_handle(), coros::detail::TaskLifeTime::THREAD_POOL_MANAGED}); 89 | 90 | 91 | EXPECT_EQ(q->get_buffer_size(), 8); 92 | EXPECT_EQ(coros::NoWaitTaskPromise::instance_count(), 4); 93 | } 94 | EXPECT_EQ(coros::NoWaitTaskPromise::instance_count(), 0); 95 | } 96 | 97 | TEST(DequeTest, DestructionTask) { 98 | EXPECT_EQ(coros::detail::SimplePromise::instance_count(), 0); 99 | { 100 | auto t1 = []() -> coros::Task{ co_return 42; }(); 101 | auto t2 = []() -> coros::Task{ co_return 42; }(); 102 | auto t3 = []() -> coros::Task{ co_return 42; }(); 103 | auto t4 = []() -> coros::Task{ co_return 42; }(); 104 | { 105 | std::unique_ptr q = std::make_unique(2,1); 106 | 107 | EXPECT_EQ(q->get_buffer_size(), 2); 108 | EXPECT_EQ(coros::detail::SimplePromise::instance_count(), 4); 109 | 110 | q->pushBottom({t1.get_handle(), coros::detail::TaskLifeTime::SCOPE_MANAGED}); 111 | q->pushBottom({t2.get_handle(), coros::detail::TaskLifeTime::SCOPE_MANAGED}); 112 | q->pushBottom({t3.get_handle(), coros::detail::TaskLifeTime::SCOPE_MANAGED}); 113 | q->pushBottom({t4.get_handle(), coros::detail::TaskLifeTime::SCOPE_MANAGED}); 114 | 115 | EXPECT_EQ(q->get_buffer_size(), 8); 116 | } 117 | EXPECT_EQ(coros::detail::SimplePromise::instance_count(), 4); 118 | } 119 | EXPECT_EQ(coros::detail::SimplePromise::instance_count(), 0); 120 | } 121 | 122 | -------------------------------------------------------------------------------- /include/enqueue_tasks.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_ENQUEUE_TASK_H_ 2 | #define COROS_INCLUDE_ENQUEUE_TASK_H_ 3 | 4 | #include 5 | 6 | #include "task.h" 7 | #include "thread_pool.h" 8 | 9 | namespace coros { 10 | 11 | class NoWaitTaskPromise; 12 | 13 | // Destroys tasks that is suspended through this awaiter. 14 | // Used in final_suspend in NoWaitTask. 15 | class DestroyTaskAwaiter { 16 | public: 17 | DestroyTaskAwaiter() {}; 18 | ~DestroyTaskAwaiter() {}; 19 | 20 | constexpr bool await_ready() const noexcept { return false; } 21 | 22 | void await_suspend(std::coroutine_handle handle) noexcept { 23 | handle.destroy(); 24 | } 25 | 26 | void await_resume() noexcept {} 27 | 28 | private: 29 | }; 30 | 31 | // Wrapper around task, that destroys its own coroutine state once completed. 32 | // Also destroyes the task it wraps, if passed as value parameter. 33 | class NoWaitTask { 34 | public: 35 | using promise_type = NoWaitTaskPromise; 36 | 37 | NoWaitTask() : handle(nullptr){}; 38 | 39 | NoWaitTask(std::coroutine_handle handle) : handle(handle){}; 40 | 41 | NoWaitTask(const NoWaitTask& other) = delete; 42 | 43 | NoWaitTask(NoWaitTask&& other) noexcept { 44 | if (this != &other) { 45 | handle = other.handle; 46 | other.handle = nullptr; 47 | } 48 | } 49 | 50 | NoWaitTask& operator=(const NoWaitTask& other) = delete; 51 | 52 | NoWaitTask& operator=(NoWaitTask&& other) noexcept { 53 | if (this != &other) { 54 | handle = other.handle; 55 | other.handle = nullptr; 56 | } 57 | return *this; 58 | } 59 | 60 | ~NoWaitTask() { } 61 | 62 | std::coroutine_handle get_handle() { return handle; } 63 | 64 | private: 65 | std::coroutine_handle handle; 66 | }; 67 | 68 | #ifdef COROS_TEST_ 69 | #define PROMISE_INSTANCE_COUNTER_ : private coros::test::InstanceCounter 70 | #else 71 | #define PROMISE_INSTANCE_COUNTER_ 72 | #endif 73 | 74 | class NoWaitTaskPromise PROMISE_INSTANCE_COUNTER_ { 75 | public: 76 | 77 | auto get_return_object() { 78 | return NoWaitTask{std::coroutine_handle::from_promise(*this)}; 79 | } 80 | 81 | template 82 | static std::enable_if_t, T>, std::size_t> instance_count() { 83 | return coros::test::InstanceCounter::instance_count(); 84 | } 85 | 86 | 87 | std::suspend_always initial_suspend() noexcept { return {}; } 88 | 89 | DestroyTaskAwaiter final_suspend() noexcept { 90 | return DestroyTaskAwaiter{}; 91 | }; 92 | 93 | // This type of task does not return anything. 94 | void return_void() {} 95 | 96 | void unhandled_exception() { std::rethrow_exception(std::current_exception()); } 97 | 98 | private: 99 | }; 100 | 101 | // Accept task by value, because it will be destroyed once completed. 102 | template 103 | inline NoWaitTask create_NoWaitTask(Task t) { 104 | co_await t; 105 | co_return; 106 | } 107 | 108 | inline NoWaitTask create_NoWaitTask() { 109 | co_return; 110 | } 111 | 112 | // Accept an r-value reference for the task, so we can move it/construct it in 113 | // the NoWaitTask argument, so it can be destroyed with the NoWaitTask. 114 | template 115 | requires (std::is_rvalue_reference_v && ...) 116 | inline void enqueue_tasks(ThreadPool& tp, Args... args) { 117 | ((tp).add_task_from_outside( 118 | {create_NoWaitTask(std::move(args)).get_handle(), 119 | detail::TaskLifeTime::THREAD_POOL_MANAGED}), ...); 120 | } 121 | 122 | template 123 | requires (std::is_rvalue_reference_v && ...) 124 | inline void enqueue_tasks(Args&&... args) { 125 | ((*thread_my_pool).add_task_from_outside( 126 | {create_NoWaitTask(std::move(args)).get_handle(), 127 | detail::TaskLifeTime::THREAD_POOL_MANAGED}), ...); 128 | } 129 | 130 | template 131 | inline void enqueue_tasks(std::vector>&& vec) { 132 | for(auto&& task : vec) { 133 | (*thread_my_pool).add_task_from_outside({create_NoWaitTask(std::move(task)).get_handle(), 134 | detail::TaskLifeTime::THREAD_POOL_MANAGED}); 135 | } 136 | } 137 | 138 | template 139 | inline void enqueue_tasks(coros::ThreadPool& tp, std::vector>&& vec) { 140 | for(auto&& task : vec) { 141 | tp.add_task_from_outside({create_NoWaitTask(std::move(task)).get_handle(), 142 | detail::TaskLifeTime::THREAD_POOL_MANAGED}); 143 | } 144 | } 145 | 146 | 147 | } // namespace coros 148 | 149 | 150 | #endif // COROS_INCLUDE_ENQUEUE_TASK_H_ 151 | -------------------------------------------------------------------------------- /tests/enqueue_tasks_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "start_tasks.h" 4 | #include "thread_pool.h" 5 | #include "enqueue_tasks.h" 6 | 7 | TEST(NoWaitTaskTest, FireTask) { 8 | EXPECT_EQ(coros::Task::instance_count(), 0); 9 | { 10 | coros::ThreadPool pool(1); 11 | 12 | EXPECT_EQ(coros::Task::instance_count(), 0); 13 | coros::Task t = []() -> coros::Task { 14 | co_return; 15 | }(); 16 | 17 | EXPECT_EQ(coros::Task::instance_count(), 1); 18 | enqueue_tasks(pool,std::move(t)); 19 | EXPECT_EQ(coros::Task::instance_count(), 2); 20 | } 21 | EXPECT_EQ(coros::Task::instance_count(), 0); 22 | } 23 | 24 | 25 | TEST(NoWaitTaskTest, NotExecutingTask) { 26 | { 27 | coros::ThreadPool tp(0); 28 | 29 | // No task created. 30 | EXPECT_EQ(coros::Task::instance_count(), 0 ); 31 | 32 | coros::Task t = []() -> coros::Task { 33 | co_return "Hello"; 34 | }(); 35 | 36 | EXPECT_EQ(coros::Task::instance_count(), 1); 37 | coros::enqueue_tasks(tp, std::move(t)); 38 | 39 | // Two tasks should exist one in the fire_task stored with contains 40 | // data from t and our current t in invalid state, since we moved it. 41 | EXPECT_EQ(coros::Task::instance_count(), 2); 42 | } 43 | // Task was not executed by the thread pool. Task t is destroyed when exiting scope. 44 | // Task inside threadpool should be destroyed in ThreadPool's destrcutor. 45 | EXPECT_EQ(coros::Task::instance_count(), 0); 46 | } 47 | 48 | TEST(NoWaitTaskTest, NoThreadPoolParam) { 49 | coros::ThreadPool tp(2); 50 | 51 | std::atomic flag = false; 52 | 53 | coros::start_sync(tp, 54 | [](std::atomic& flag) -> coros::Task{ 55 | coros::enqueue_tasks([](std::atomic& flag) -> coros::Task{ 56 | flag.store(true); 57 | co_return; 58 | }(flag)); 59 | 60 | co_return; 61 | }(flag)); 62 | 63 | 64 | while(!flag) { 65 | // busy waiting for the flag to be set 66 | } 67 | EXPECT_EQ(flag, true); 68 | } 69 | 70 | TEST(NoWaitTaskTest, MultipleTasks) { 71 | coros::ThreadPool tp{2}; 72 | std::atomic counter = 0; 73 | 74 | coros::Task task = [](std::atomic& counter) -> coros::Task { 75 | auto t1 = [](std::atomic& counter) -> coros::Task { 76 | counter++; 77 | co_return; 78 | }(counter); 79 | auto t2 = [](std::atomic& counter) -> coros::Task { 80 | counter++; 81 | co_return; 82 | }(counter); 83 | 84 | coros::enqueue_tasks(std::move(t1), std::move(t2)); 85 | 86 | co_return; 87 | }(counter); 88 | 89 | coros::start_sync(tp, task); 90 | 91 | // Busy wait 92 | while(counter != 2){} 93 | 94 | EXPECT_EQ(counter, 2); 95 | } 96 | 97 | TEST(NoWaitTaskTest, MultipleTasksThreadPool) { 98 | coros::ThreadPool tp2{2}; 99 | coros::ThreadPool tp{2}; 100 | std::atomic counter = 0; 101 | 102 | coros::Task task = [](std::atomic& counter, coros::ThreadPool& tp) -> coros::Task { 103 | auto t1 = [](std::atomic& counter) -> coros::Task { 104 | counter++; 105 | co_return; 106 | }(counter); 107 | auto t2 = [](std::atomic& counter) -> coros::Task { 108 | counter++; 109 | co_return; 110 | }(counter); 111 | 112 | coros::enqueue_tasks(tp, std::move(t1), std::move(t2)); 113 | 114 | co_return; 115 | }(counter, tp2); 116 | 117 | coros::start_sync(tp, task); 118 | 119 | // Busy wait 120 | while(counter != 2){} 121 | 122 | EXPECT_EQ(counter, 2); 123 | } 124 | 125 | TEST(NoWaitTaskTest, Vector) { 126 | coros::ThreadPool tp{2}; 127 | std::atomic counter = 0; 128 | 129 | coros::Task task = [](std::atomic& counter) -> coros::Task { 130 | 131 | std::vector> vec; 132 | vec.push_back([](std::atomic& counter) -> coros::Task { 133 | counter++; 134 | co_return; 135 | }(counter)); 136 | vec.push_back([](std::atomic& counter) -> coros::Task { 137 | counter++; 138 | co_return; 139 | }(counter)); 140 | 141 | coros::enqueue_tasks(std::move(vec)); 142 | 143 | co_return; 144 | }(counter); 145 | 146 | coros::start_sync(tp, task); 147 | 148 | // Busy wait 149 | while(counter != 2){} 150 | 151 | EXPECT_EQ(counter, 2); 152 | } 153 | 154 | TEST(NoWaitTaskTest, VectorThradpool) { 155 | coros::ThreadPool tp2{2}; 156 | coros::ThreadPool tp{2}; 157 | std::atomic counter = 0; 158 | 159 | coros::Task task = [](std::atomic& counter, coros::ThreadPool& tp) -> coros::Task { 160 | 161 | std::vector> vec; 162 | vec.push_back([](std::atomic& counter) -> coros::Task { 163 | counter++; 164 | co_return; 165 | }(counter)); 166 | vec.push_back([](std::atomic& counter) -> coros::Task { 167 | counter++; 168 | co_return; 169 | }(counter)); 170 | 171 | coros::enqueue_tasks(tp, std::move(vec)); 172 | 173 | co_return; 174 | }(counter, tp2); 175 | 176 | coros::start_sync(tp, task); 177 | 178 | // Busy wait 179 | while(counter != 2){} 180 | 181 | EXPECT_EQ(counter, 2); 182 | } 183 | -------------------------------------------------------------------------------- /include/thread_pool.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_THREAD_POOL_H_ 2 | #define COROS_INCLUDE_THREAD_POOL_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | #include "deque.h" 11 | #include "concurrentqueue.h" 12 | 13 | #ifdef COROS_TEST_DEQUE_ 14 | #include "test_deque.h" 15 | #endif 16 | 17 | 18 | namespace coros { 19 | 20 | class ThreadPool; 21 | 22 | // If this macro is defined the test Deque, using mutex is used 23 | #ifdef COROS_TEST_DEQUE_ 24 | #define DEQUE_ test::TestDeque 25 | #else 26 | #define DEQUE_ coros::detail::Dequeue 27 | #endif // DEBUG 28 | 29 | // Pointer to thread's own task deque. 30 | inline thread_local DEQUE_* thread_my_tasks; 31 | // inline thread_local Dequeue* thread_my_tasks; 32 | // inline thread_local TestDeque* thread_my_tasks; 33 | // Each thread has a pointer to its own generator for generating 34 | // random index from which other threads are checked for stealing a task. 35 | inline thread_local std::mt19937* thread_gen; 36 | // Pointer to a thread pool to which the thread belongs to. 37 | inline thread_local ThreadPool* thread_my_pool; 38 | 39 | // Holds individual threads and their task queues. 40 | // TODO : delete constructors (move and copy) 41 | class ThreadPool { 42 | public: 43 | ThreadPool(int thread_count); 44 | 45 | void add_task(std::pair, detail::TaskLifeTime>&& task); 46 | 47 | void add_task_from_outside(std::pair, detail::TaskLifeTime>&& handle); 48 | 49 | std::coroutine_handle<> get_task(); 50 | 51 | void stop_threads(); 52 | 53 | auto schedule_this_task(); 54 | 55 | ~ThreadPool(); 56 | 57 | private: 58 | void run(); 59 | 60 | std::atomic threads_stop_executing_ = false; 61 | std::vector workers_; 62 | 63 | // Deques for each worker thread. 64 | std::vector worker_queues_; 65 | // Used to get tasks to queue task from "outside" of ThreadPool. 66 | moodycamel::ConcurrentQueue, detail::TaskLifeTime>> new_tasks_; 67 | 68 | std::random_device rd_; 69 | std::vector gens_; 70 | std::uniform_int_distribution<> dists_; 71 | }; 72 | 73 | inline ThreadPool::ThreadPool(int thread_count) 74 | : worker_queues_(std::vector(thread_count)), 75 | gens_(thread_count), 76 | dists_(0, thread_count - 1) { 77 | workers_.reserve(thread_count); 78 | for (int i = 0; i < thread_count; i++) { 79 | DEQUE_* deque = &worker_queues_[i]; 80 | std::mt19937* gen = &gens_[i]; 81 | workers_.emplace_back([this, deque, gen]() { 82 | thread_my_tasks = deque; 83 | thread_gen = gen; 84 | thread_my_pool = this; 85 | this->run(); 86 | }); 87 | } 88 | } 89 | 90 | 91 | // Individual threads use this method to add tasks into their own deque. 92 | inline void ThreadPool::add_task(std::pair, detail::TaskLifeTime>&& handle) { 93 | thread_my_tasks->pushBottom(std::move(handle)); 94 | } 95 | 96 | // Used to add tasks from "outside"(not from within of the threadpool) not from individual threads. Tasks are pushed 97 | // into a different queue from which individual threads can take them as a new work. 98 | // New tasks cannot be pushed directly into the work-stealing dequeue. 99 | inline void ThreadPool::add_task_from_outside(std::pair, detail::TaskLifeTime>&& handle) { 100 | new_tasks_.enqueue(std::move(handle)); 101 | } 102 | 103 | // Main method run by each thread. Individual threads check for available work. 104 | // New tasks are started through coroutine handle, by calling resume() on 105 | // the handle. 106 | inline std::coroutine_handle<> ThreadPool::get_task() { 107 | // Worker tries to get task from its own queue. If there is a tasks 108 | // in its own deuque, the handle is returned. 109 | auto task = thread_my_tasks->popBottom(); 110 | if (task.has_value()) [[likely]] { 111 | return task.value(); 112 | } 113 | 114 | // In case the thread does not have tasks in its own deque, it 115 | // tries to steal from other threads. 116 | // Random index is generate for even distribution. 117 | int random_index = dists_(*thread_gen); 118 | for (size_t i = 0; i < worker_queues_.size(); i++) { 119 | int index = (random_index + i) % worker_queues_.size(); 120 | // Skip my own queue (can't steal from myself) 121 | if (&worker_queues_[index] == thread_my_tasks) [[unlikely]] continue; 122 | task = worker_queues_[index].steal(); 123 | if (task.has_value()) { 124 | return task.value(); 125 | } 126 | } 127 | 128 | // In case a thread does not have a task in its own deque nor steals 129 | // a task from another thread, we checked new_task qeueu, which is shared among 130 | // all threads, for new work. 131 | std::pair, detail::TaskLifeTime> new_task; 132 | if (new_tasks_.try_dequeue(new_task)) { 133 | return new_task.first; 134 | } 135 | 136 | // If there is no work, the std::noop_coroutine_handle is returned. Resuming this 137 | // handle does nothing, which repeats the loop inside the run() method. 138 | return std::noop_coroutine(); 139 | } 140 | 141 | // TODO: Implement thread yielding in case there is no work for an extended duration to 142 | // not waste CPU. 143 | // Main loop run by each thread. 144 | inline void ThreadPool::run() { 145 | while (!threads_stop_executing_.load(std::memory_order::acquire)) [[likely]] { 146 | // Takes tasks and resumes it. If the qeuue is empty, 147 | // it will return noop_coroutine. 148 | this->get_task().resume(); 149 | } 150 | } 151 | 152 | inline ThreadPool::~ThreadPool() { 153 | stop_threads(); 154 | 155 | // Should be safe to use size_approx here, because 156 | // all thread are stopped. 157 | size_t size = new_tasks_.size_approx(); 158 | std::vector, detail::TaskLifeTime>> tasks(size); 159 | new_tasks_.try_dequeue_bulk(tasks.data(), size); 160 | 161 | for (size_t i = 0; i < size; i++) { 162 | if (tasks[i].second == detail::TaskLifeTime::THREAD_POOL_MANAGED) { 163 | tasks[i].first.destroy(); 164 | } 165 | } 166 | } 167 | 168 | // request stop for all threads 169 | // TODO : destruction of tasks 170 | inline void ThreadPool::stop_threads() { 171 | // TODO: Check for weaker synchronizatoin 172 | threads_stop_executing_.store(true, std::memory_order::release); 173 | for (auto& t : workers_) { 174 | t.join(); 175 | } 176 | } 177 | 178 | // Accept an rvalue reference for the task, so we can move it/construct it in 179 | // the NoWaitTask argument, so it can be destroyed with the NoWaitTask. 180 | /*template 181 | inline void fire_task(Task&& task, ThreadPool& tp) { 182 | auto t = create_NoWaitTask(std::move(task)); 183 | tp.add_task_from_outside({t.get_handle(), detail::TaskLifeTime::THREAD_POOL_MANAGED}); 184 | }*/ 185 | 186 | 187 | } // namespace coros 188 | 189 | #endif // COROS_INCLUDE_THREAD_POOL_H_ 190 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignArrayOfStructures: None 7 | AlignConsecutiveMacros: None 8 | AlignConsecutiveAssignments: None 9 | AlignConsecutiveBitFields: None 10 | AlignConsecutiveDeclarations: None 11 | AlignEscapedNewlines: Left 12 | AlignOperands: Align 13 | AlignTrailingComments: true 14 | AllowAllArgumentsOnNextLine: true 15 | AllowAllParametersOfDeclarationOnNextLine: true 16 | AllowShortEnumsOnASingleLine: true 17 | AllowShortBlocksOnASingleLine: Never 18 | AllowShortCaseLabelsOnASingleLine: false 19 | AllowShortFunctionsOnASingleLine: All 20 | AllowShortLambdasOnASingleLine: All 21 | AllowShortIfStatementsOnASingleLine: WithoutElse 22 | AllowShortLoopsOnASingleLine: true 23 | AlwaysBreakAfterDefinitionReturnType: None 24 | AlwaysBreakAfterReturnType: None 25 | AlwaysBreakBeforeMultilineStrings: true 26 | AlwaysBreakTemplateDeclarations: Yes 27 | AttributeMacros: 28 | - __capability 29 | BinPackArguments: true 30 | BinPackParameters: true 31 | BraceWrapping: 32 | AfterCaseLabel: false 33 | AfterClass: false 34 | AfterControlStatement: Never 35 | AfterEnum: false 36 | AfterFunction: false 37 | AfterNamespace: false 38 | AfterObjCDeclaration: false 39 | AfterStruct: false 40 | AfterUnion: false 41 | AfterExternBlock: false 42 | BeforeCatch: false 43 | BeforeElse: false 44 | BeforeLambdaBody: false 45 | BeforeWhile: false 46 | IndentBraces: false 47 | SplitEmptyFunction: true 48 | SplitEmptyRecord: true 49 | SplitEmptyNamespace: true 50 | BreakBeforeBinaryOperators: None 51 | BreakBeforeConceptDeclarations: true 52 | BreakBeforeBraces: Attach 53 | BreakBeforeInheritanceComma: false 54 | BreakInheritanceList: BeforeColon 55 | BreakBeforeTernaryOperators: true 56 | BreakConstructorInitializersBeforeComma: false 57 | BreakConstructorInitializers: BeforeColon 58 | BreakAfterJavaFieldAnnotations: false 59 | BreakStringLiterals: true 60 | ColumnLimit: 100 61 | CommentPragmas: '^ IWYU pragma:' 62 | QualifierAlignment: Leave 63 | CompactNamespaces: false 64 | ConstructorInitializerIndentWidth: 4 65 | ContinuationIndentWidth: 4 66 | Cpp11BracedListStyle: true 67 | DeriveLineEnding: true 68 | DerivePointerAlignment: true 69 | DisableFormat: false 70 | EmptyLineAfterAccessModifier: Never 71 | EmptyLineBeforeAccessModifier: LogicalBlock 72 | ExperimentalAutoDetectBinPacking: false 73 | PackConstructorInitializers: NextLine 74 | BasedOnStyle: '' 75 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 76 | AllowAllConstructorInitializersOnNextLine: true 77 | FixNamespaceComments: true 78 | ForEachMacros: 79 | - foreach 80 | - Q_FOREACH 81 | - BOOST_FOREACH 82 | IfMacros: 83 | - KJ_IF_MAYBE 84 | IncludeBlocks: Regroup 85 | IncludeCategories: 86 | - Regex: '^' 87 | Priority: 2 88 | SortPriority: 0 89 | CaseSensitive: false 90 | - Regex: '^<.*\.h>' 91 | Priority: 1 92 | SortPriority: 0 93 | CaseSensitive: false 94 | - Regex: '^<.*' 95 | Priority: 2 96 | SortPriority: 0 97 | CaseSensitive: false 98 | - Regex: '.*' 99 | Priority: 3 100 | SortPriority: 0 101 | CaseSensitive: false 102 | IncludeIsMainRegex: '([-_](test|unittest))?$' 103 | IncludeIsMainSourceRegex: '' 104 | IndentAccessModifiers: false 105 | IndentCaseLabels: true 106 | IndentCaseBlocks: false 107 | IndentGotoLabels: true 108 | IndentPPDirectives: None 109 | IndentExternBlock: AfterExternBlock 110 | IndentRequires: false 111 | IndentWidth: 2 112 | IndentWrappedFunctionNames: false 113 | InsertTrailingCommas: None 114 | JavaScriptQuotes: Leave 115 | JavaScriptWrapImports: true 116 | KeepEmptyLinesAtTheStartOfBlocks: false 117 | LambdaBodyIndentation: Signature 118 | MacroBlockBegin: '' 119 | MacroBlockEnd: '' 120 | MaxEmptyLinesToKeep: 1 121 | NamespaceIndentation: None 122 | ObjCBinPackProtocolList: Never 123 | ObjCBlockIndentWidth: 2 124 | ObjCBreakBeforeNestedBlockParam: true 125 | ObjCSpaceAfterProperty: false 126 | ObjCSpaceBeforeProtocolList: true 127 | PenaltyBreakAssignment: 2 128 | PenaltyBreakBeforeFirstCallParameter: 1 129 | PenaltyBreakComment: 300 130 | PenaltyBreakFirstLessLess: 120 131 | PenaltyBreakOpenParenthesis: 0 132 | PenaltyBreakString: 1000 133 | PenaltyBreakTemplateDeclaration: 10 134 | PenaltyExcessCharacter: 1000000 135 | PenaltyReturnTypeOnItsOwnLine: 200 136 | PenaltyIndentedWhitespace: 0 137 | PointerAlignment: Left 138 | PPIndentWidth: -1 139 | RawStringFormats: 140 | - Language: Cpp 141 | Delimiters: 142 | - cc 143 | - CC 144 | - cpp 145 | - Cpp 146 | - CPP 147 | - 'c++' 148 | - 'C++' 149 | CanonicalDelimiter: '' 150 | BasedOnStyle: google 151 | - Language: TextProto 152 | Delimiters: 153 | - pb 154 | - PB 155 | - proto 156 | - PROTO 157 | EnclosingFunctions: 158 | - EqualsProto 159 | - EquivToProto 160 | - PARSE_PARTIAL_TEXT_PROTO 161 | - PARSE_TEST_PROTO 162 | - PARSE_TEXT_PROTO 163 | - ParseTextOrDie 164 | - ParseTextProtoOrDie 165 | - ParseTestProto 166 | - ParsePartialTestProto 167 | CanonicalDelimiter: pb 168 | BasedOnStyle: google 169 | ReferenceAlignment: Pointer 170 | ReflowComments: true 171 | RemoveBracesLLVM: false 172 | SeparateDefinitionBlocks: Leave 173 | ShortNamespaceLines: 1 174 | SortIncludes: CaseSensitive 175 | SortJavaStaticImport: Before 176 | SortUsingDeclarations: true 177 | SpaceAfterCStyleCast: false 178 | SpaceAfterLogicalNot: false 179 | SpaceAfterTemplateKeyword: true 180 | SpaceBeforeAssignmentOperators: true 181 | SpaceBeforeCaseColon: false 182 | SpaceBeforeCpp11BracedList: false 183 | SpaceBeforeCtorInitializerColon: true 184 | SpaceBeforeInheritanceColon: true 185 | SpaceBeforeParens: ControlStatements 186 | SpaceBeforeParensOptions: 187 | AfterControlStatements: true 188 | AfterForeachMacros: true 189 | AfterFunctionDefinitionName: false 190 | AfterFunctionDeclarationName: false 191 | AfterIfMacros: true 192 | AfterOverloadedOperator: false 193 | BeforeNonEmptyParentheses: false 194 | SpaceAroundPointerQualifiers: Default 195 | SpaceBeforeRangeBasedForLoopColon: true 196 | SpaceInEmptyBlock: false 197 | SpaceInEmptyParentheses: false 198 | SpacesBeforeTrailingComments: 2 199 | SpacesInAngles: Never 200 | SpacesInConditionalStatement: false 201 | SpacesInContainerLiterals: true 202 | SpacesInCStyleCastParentheses: false 203 | SpacesInLineCommentPrefix: 204 | Minimum: 1 205 | Maximum: -1 206 | SpacesInParentheses: false 207 | SpacesInSquareBrackets: false 208 | SpaceBeforeSquareBrackets: false 209 | BitFieldColonSpacing: Both 210 | Standard: Auto 211 | StatementAttributeLikeMacros: 212 | - Q_EMIT 213 | StatementMacros: 214 | - Q_UNUSED 215 | - QT_REQUIRE_VERSION 216 | TabWidth: 8 217 | UseCRLF: false 218 | UseTab: Never 219 | WhitespaceSensitiveMacros: 220 | - STRINGIZE 221 | - PP_STRINGIZE 222 | - BOOST_PP_STRINGIZE 223 | - NS_SWIFT_NAME 224 | - CF_SWIFT_NAME 225 | ... 226 | 227 | -------------------------------------------------------------------------------- /include/deque.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_DEQUE_H_ 2 | #define COROS_INCLUDE_DEQUE_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "task_life_time.h" 13 | 14 | namespace coros { 15 | namespace detail { 16 | 17 | #ifdef __cpp_lib_hardware_interference_size 18 | using std::hardware_constructive_interference_size; 19 | using std::hardware_destructive_interference_size; 20 | #else 21 | constexpr std::size_t hardware_constructive_interference_size = sizeof(uint_fast64_t); 22 | constexpr std::size_t hardware_destructive_interference_size = sizeof(uint_fast64_t); 23 | #endif 24 | 25 | inline constexpr size_t kDefaultBufferSize = 512; 26 | 27 | template 28 | class CircularBuffer { 29 | public: 30 | CircularBuffer(uint_fast64_t init_size) 31 | : size_(init_size), modulo_(size_ - 1), array_(std::make_unique(size_)){}; 32 | 33 | CircularBuffer(const CircularBuffer&) = delete; 34 | CircularBuffer(CircularBuffer&&) = delete; 35 | 36 | CircularBuffer& operator=(const CircularBuffer&) = delete; 37 | CircularBuffer& operator=(CircularBuffer&&) = delete; 38 | 39 | // Keeping the size of the buffer a power of 2 40 | // allows us to use modulo 41 | void put(uint_fast64_t pos, T&& elem) noexcept { 42 | array_[pos & modulo_] = elem; 43 | } 44 | 45 | T get(uint_fast64_t pos) noexcept { 46 | return array_[pos & modulo_]; 47 | } 48 | 49 | CircularBuffer* grow(uint_fast64_t bottom, uint_fast64_t top) { 50 | CircularBuffer* new_buffer = new CircularBuffer{4 * size_}; 51 | for (uint_fast64_t i = top; i < bottom; i++) { 52 | new_buffer->put(i, get(i)); 53 | } 54 | return new_buffer; 55 | } 56 | 57 | uint_fast64_t size() { return size_; } 58 | 59 | T& operator[](uint_fast64_t pos) { return array_[pos]; } 60 | 61 | private: 62 | uint_fast64_t size_; 63 | uint_fast64_t modulo_; 64 | std::unique_ptr array_; 65 | }; 66 | 67 | 68 | class Dequeue { 69 | public: 70 | Dequeue() 71 | : top_(1), 72 | bottom_(1), 73 | buffer_(new CircularBuffer, TaskLifeTime>>(kDefaultBufferSize)), 74 | old_buffers_(64) {}; 75 | 76 | explicit Dequeue(uint_fast64_t init_buffer_size) 77 | : top_(1), 78 | bottom_(1), buffer_(new CircularBuffer, TaskLifeTime>>(init_buffer_size)), 79 | old_buffers_(64) {}; 80 | 81 | explicit Dequeue(uint_fast64_t init_buffer_size, uint_fast64_t old_buffers_size) 82 | : top_(1), 83 | bottom_(1), buffer_(new CircularBuffer, TaskLifeTime>>(init_buffer_size)), 84 | old_buffers_(old_buffers_size) {}; 85 | 86 | // Deletes coroutine states, which lifetimes are manged by ThreadPool. 87 | ~Dequeue() { 88 | // Calling delete for each old buffer, which release the memory 89 | // allocated in unique_ptr. 90 | for (auto& buffer : old_buffers_) { 91 | delete buffer; 92 | } 93 | 94 | // Destroy tasks in the current buffer. The current buffer 95 | // has all the coroutine handles. That were not processed. 96 | auto buffer = buffer_.load(); 97 | for(uint_fast64_t i = top_; i < bottom_;i++) { 98 | auto task = buffer->get(i); 99 | // If there are any tasks that are managed by threadpool, they should 100 | // be destroyed. Otherwise the tasks are destroyed when they go 101 | // out of scope. 102 | if (task.second == TaskLifeTime::THREAD_POOL_MANAGED) 103 | task.first.destroy(); 104 | } 105 | 106 | // Deletes the buffer, freed by the unique_ptr that holds it in the 107 | // CircularBuffer. 108 | delete buffer_; 109 | } 110 | 111 | // Overload for R-values. 112 | void pushBottom(std::pair, TaskLifeTime>&& handle) { 113 | uint_fast64_t bottom = bottom_.load(std::memory_order_relaxed); 114 | uint_fast64_t top = top_.load(std::memory_order_acquire); 115 | CircularBuffer, TaskLifeTime>>* cb = 116 | buffer_.load(std::memory_order_relaxed); 117 | 118 | // Check if queue is full 119 | if (bottom - top > cb->size() - 1) { 120 | old_buffers_.push_back(cb); 121 | cb = cb->grow(bottom, top); 122 | buffer_.store(cb, std::memory_order_relaxed); 123 | } 124 | 125 | cb->put(bottom, std::move(handle)); 126 | // std::atomic_thread_fence(std::memory_order_release); 127 | // __tsan_release(&bottom_); 128 | // bottom_.store(bottom + 1, std::memory_order_relaxed); 129 | bottom_.store(bottom + 1, std::memory_order::release); 130 | }; 131 | 132 | std::optional> steal() { 133 | std::uint_fast64_t top = top_.load(std::memory_order_acquire); 134 | std::atomic_thread_fence(std::memory_order_seq_cst); 135 | std::uint_fast64_t bottom = bottom_.load(std::memory_order_acquire); 136 | 137 | if (top < bottom) { 138 | std::pair, TaskLifeTime> return_handle = buffer_.load(std::memory_order_acquire)->get(top); 139 | 140 | if (!top_.compare_exchange_strong(top, top + 1, std::memory_order_seq_cst, 141 | std::memory_order_relaxed)) { 142 | // failsed to steal the item 143 | return {}; 144 | } 145 | // return stolen item 146 | return return_handle.first; 147 | } 148 | // return empty value queue is empty 149 | return {}; 150 | }; 151 | 152 | std::optional> popBottom() { 153 | uint_fast64_t bottom = bottom_.load(std::memory_order_relaxed) - 1; 154 | CircularBuffer, TaskLifeTime>>* cb = 155 | buffer_.load(std::memory_order_relaxed); 156 | 157 | 158 | bottom_.store(bottom, std::memory_order_relaxed); 159 | //__tsan_release(&bottom_); 160 | std::atomic_thread_fence(std::memory_order_seq_cst); 161 | 162 | uint_fast64_t top = top_.load(std::memory_order_relaxed); 163 | //__tsan_acquire(&top_); 164 | std::pair, TaskLifeTime> return_handle; 165 | 166 | if (top <= bottom) { 167 | // queue is not empty 168 | return_handle = cb->get(bottom); 169 | if (top == bottom) { 170 | // last element in the qeueu, top == bottom means queue is empty and we are trying to 171 | // steal the last element 172 | if (!top_.compare_exchange_strong(top, top + 1, std::memory_order_seq_cst, 173 | std::memory_order_relaxed)) { 174 | // failed to pop last element, other thread stole it 175 | // by incrementing top 176 | bottom_.store(bottom + 1, std::memory_order_relaxed); 177 | return {}; 178 | } 179 | bottom_.store(bottom + 1, std::memory_order_relaxed); 180 | } 181 | } else { 182 | // queue is empty 183 | bottom_.store(bottom + 1, std::memory_order_relaxed); 184 | return {}; 185 | } 186 | 187 | // qeueu was not empty and we managed to pop the element 188 | return return_handle.first; 189 | } 190 | 191 | // does not guarantee that the value is correct 192 | uint_fast64_t get_buffer_size() { 193 | return buffer_.load(std::memory_order_relaxed)->size(); 194 | // return std::atomic_load_explicit(&buffer_, std::memory_order_relaxed)->size(); 195 | } 196 | 197 | // Returns current underlying buffer. 198 | // Do not use this in multithreaded environment. 199 | CircularBuffer, TaskLifeTime>>& get_underlying_buffer() { 200 | return *buffer_.load(); 201 | } 202 | 203 | private: 204 | alignas(hardware_destructive_interference_size) std::atomic top_; 205 | alignas(hardware_destructive_interference_size) std::atomic bottom_; 206 | 207 | //std::atomic top_; 208 | //std::atomic bottom_; 209 | 210 | std::atomic, TaskLifeTime>>*> buffer_; 211 | std::vector, TaskLifeTime>>*> old_buffers_; 212 | }; 213 | 214 | } // namespace detail 215 | } // namespace coros 216 | 217 | #endif // COROS_INCLUDE_DEQUE_H_ 218 | -------------------------------------------------------------------------------- /tests/chain_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "chain_tasks.h" 5 | #include "wait_tasks.h" 6 | #include "start_tasks.h" 7 | 8 | 9 | // Helper functions. 10 | namespace { 11 | 12 | coros::Task add_one(int val) { 13 | co_return val + 1; 14 | } 15 | 16 | coros::Task return_void([[maybe_unused]] int value) { 17 | co_return; 18 | } 19 | 20 | coros::Task add_one_exception(int val) { 21 | throw std::bad_alloc(); 22 | co_return val + 1; 23 | } 24 | 25 | coros::Task accept_void() { 26 | co_return; 27 | } 28 | 29 | coros::Task accept_void_throw() { 30 | throw std::bad_alloc(); 31 | co_return; 32 | } 33 | 34 | coros::Task return_double(int value) { 35 | co_return value; 36 | } 37 | 38 | coros::Task fib(int index) { 39 | if (index == 0) co_return 0; 40 | if (index == 1) co_return 1; 41 | 42 | coros::Task a = fib(index - 1); 43 | coros::Task b = fib(index - 2); 44 | 45 | co_await coros::wait_tasks(a, b); 46 | 47 | co_return *a + *b; 48 | } 49 | 50 | } // anonymous namespace 51 | 52 | TEST(ChainTest, SimpleChain) { 53 | std::expected ex{40}; 54 | auto t = [](std::expected& ex) -> coros::Task { 55 | auto awaitable = coros::chain_tasks(ex).and_then(add_one); 56 | auto res = co_await awaitable; 57 | co_return *res + 1; 58 | }(ex); 59 | t.get_handle().resume(); 60 | EXPECT_EQ(*t, 42); 61 | } 62 | 63 | TEST(ChainTest, MultipleFunctions) { 64 | std::expected ex{38}; 65 | auto t = [](std::expected& ex) -> coros::Task { 66 | auto awaitable = coros::chain_tasks(ex).and_then(add_one) 67 | .and_then(add_one) 68 | .and_then(add_one); 69 | auto res = co_await awaitable; 70 | co_return *res + 1; 71 | }(ex); 72 | t.get_handle().resume(); 73 | EXPECT_EQ(*t, 42); 74 | } 75 | 76 | TEST(ChainTest, ReturnVoid) { 77 | std::expected ex{38}; 78 | auto t = [](std::expected& ex) -> coros::Task { 79 | auto awaitable = coros::chain_tasks(ex).and_then(add_one) 80 | .and_then(add_one) 81 | .and_then(add_one) 82 | .and_then(return_void); 83 | auto res = co_await awaitable; 84 | co_return; 85 | }(ex); 86 | t.get_handle().resume(); 87 | } 88 | 89 | TEST(ChainTest, ChainVoid) { 90 | std::expected ex{41}; 91 | auto t = [](std::expected& ex) -> coros::Task { 92 | auto awaitable = coros::chain_tasks(ex).and_then(add_one) 93 | .and_then(return_void) 94 | .and_then(accept_void) 95 | .and_then(accept_void) 96 | .and_then(accept_void); 97 | 98 | auto res = co_await awaitable; 99 | co_return; 100 | }(ex); 101 | t.get_handle().resume(); 102 | } 103 | 104 | TEST(ChainTest, PropagateException) { 105 | std::expected ex{41}; 106 | auto t = [](std::expected& ex) -> coros::Task { 107 | auto awaitable = coros::chain_tasks(ex).and_then(add_one) 108 | .and_then(add_one_exception) 109 | .and_then(return_void); 110 | 111 | auto res = co_await awaitable; 112 | EXPECT_EQ(res.has_value(), false); 113 | EXPECT_THROW(std::rethrow_exception(res.error()), std::bad_alloc); 114 | co_return; 115 | }(ex); 116 | t.get_handle().resume(); 117 | } 118 | 119 | TEST(ChainTest, TypeConversion) { 120 | std::expected ex{41}; 121 | auto t = [](std::expected& ex) -> coros::Task { 122 | auto awaitable = coros::chain_tasks(ex).and_then(add_one) 123 | .and_then(return_double); 124 | 125 | auto res = co_await awaitable; 126 | static_assert( 127 | std::is_same_v, 128 | std::remove_reference_t>); 129 | EXPECT_EQ(res.has_value(), true); 130 | EXPECT_EQ(*res, 42); 131 | static_assert( 132 | std::is_same_v>, 133 | "Type is not double : "); 134 | co_return *res; 135 | }(ex); 136 | t.get_handle().resume(); 137 | EXPECT_EQ(*t, 42); 138 | } 139 | 140 | TEST(ChainTest, ChainTaskStart) { 141 | auto t = []() -> coros::Task { 142 | auto t = []() -> coros::Task {co_return 41;}; 143 | auto awaitable = coros::chain_tasks(t()).and_then(add_one); 144 | 145 | auto res = co_await awaitable; 146 | EXPECT_EQ(res.has_value(), true); 147 | static_assert( 148 | std::is_same_v, 149 | int>, 150 | "NO" 151 | ); 152 | co_return *res; 153 | }(); 154 | t.get_handle().resume(); 155 | EXPECT_EQ(*t, 42); 156 | } 157 | 158 | TEST(ChainTest, Fib) { 159 | coros::ThreadPool tp{4}; 160 | auto t = []() -> coros::Task { 161 | auto res = co_await coros::chain_tasks(fib(20)).and_then(add_one) 162 | .and_then(add_one) 163 | .and_then(add_one); 164 | co_return *res; 165 | }(); 166 | coros::start_sync(tp, t); 167 | EXPECT_EQ(*t, 6768); 168 | } 169 | 170 | TEST(ChainTest, FibVoid) { 171 | coros::ThreadPool tp{4}; 172 | auto t = []() -> coros::Task { 173 | auto res = co_await coros::chain_tasks(fib(20)).and_then(add_one) 174 | .and_then(add_one) 175 | .and_then(add_one) 176 | .and_then(return_void) 177 | .and_then(accept_void) 178 | .and_then(accept_void); 179 | EXPECT_TRUE(res.has_value()); 180 | co_return; 181 | }(); 182 | coros::start_sync(tp, t); 183 | } 184 | 185 | TEST(ChainTest, FibVoidThrow) { 186 | coros::ThreadPool tp{4}; 187 | auto t = []() -> coros::Task { 188 | auto res = co_await coros::chain_tasks(fib(20)).and_then(add_one) 189 | .and_then(add_one) 190 | .and_then(add_one) 191 | .and_then(return_void) 192 | .and_then(accept_void_throw) 193 | .and_then(accept_void); 194 | EXPECT_FALSE(res.has_value()); 195 | EXPECT_THROW(std::rethrow_exception(res.error()), std::bad_alloc); 196 | co_return; 197 | }(); 198 | coros::start_sync(tp, t); 199 | } 200 | 201 | TEST(ChainTest, Lambda) { 202 | coros::ThreadPool tp{4}; 203 | auto t = []() -> coros::Task { 204 | auto res = co_await coros::chain_tasks(fib(20)).and_then(add_one) 205 | .and_then(add_one) 206 | .and_then(add_one) 207 | .and_then(return_void) 208 | .and_then(accept_void_throw) 209 | .and_then(accept_void); 210 | EXPECT_FALSE(res.has_value()); 211 | EXPECT_THROW(std::rethrow_exception(res.error()), std::bad_alloc); 212 | co_return; 213 | }(); 214 | coros::start_sync(tp, t); 215 | } 216 | 217 | class MoveOnlyInt { 218 | public: 219 | MoveOnlyInt(int value) : value_(value) {} 220 | MoveOnlyInt(MoveOnlyInt&& other) noexcept : value_(other.value_) { 221 | other.value_ = 0; // Optional: reset the source object 222 | } 223 | MoveOnlyInt& operator=(MoveOnlyInt&& other) noexcept { 224 | if (this != &other) { 225 | value_ = other.value_; 226 | other.value_ = 0; 227 | } 228 | return *this; 229 | } 230 | MoveOnlyInt(const MoveOnlyInt&) = delete; 231 | MoveOnlyInt& operator=(const MoveOnlyInt&) = delete; 232 | int getValue() const { return value_; } 233 | private: 234 | int value_; 235 | }; 236 | 237 | class CopyOnlyInt { 238 | public: 239 | CopyOnlyInt(int value) : value_(value) {} 240 | CopyOnlyInt(const CopyOnlyInt& other) : value_(other.value_) {} 241 | CopyOnlyInt& operator=(const CopyOnlyInt& other) { 242 | if (this != &other) { 243 | value_ = other.value_; 244 | } 245 | return *this; 246 | } 247 | ~CopyOnlyInt(){}; 248 | int getValue() const { return value_; } 249 | private: 250 | int value_; 251 | }; 252 | 253 | coros::Task add_one_move(MoveOnlyInt val) { 254 | co_return MoveOnlyInt(val.getValue() + 1); 255 | } 256 | 257 | coros::Task add_one_copy(CopyOnlyInt val) { 258 | co_return CopyOnlyInt(val.getValue() + 1); 259 | } 260 | 261 | 262 | TEST(ChainTest, MoveOnlyInt) { 263 | auto t = []() -> coros::Task { 264 | std::expected ex{40}; 265 | auto res = co_await coros::chain_tasks(std::move(ex)).and_then(add_one_move) 266 | .and_then(add_one_move); 267 | co_return *res; 268 | }(); 269 | t.get_handle().resume(); 270 | EXPECT_EQ((*t).getValue(), 42); 271 | } 272 | 273 | TEST(ChainTest, CopyOnlyInt) { 274 | auto t = []() -> coros::Task { 275 | std::expected ex{40}; 276 | auto res = co_await coros::chain_tasks(ex).and_then(add_one_copy) 277 | .and_then(add_one_copy); 278 | co_return *res; 279 | }(); 280 | t.get_handle().resume(); 281 | EXPECT_EQ((*t).getValue(), 42); 282 | } 283 | 284 | TEST(ChainTest, InitValue){ 285 | coros::ThreadPool tp{4}; 286 | auto t = []() -> coros::Task { 287 | auto res = co_await coros::chain_tasks(40).and_then(add_one) 288 | .and_then(add_one); 289 | EXPECT_TRUE(res.has_value()); 290 | co_return *res; 291 | }(); 292 | coros::start_sync(tp, t); 293 | EXPECT_EQ(*t, 42); 294 | } 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | -------------------------------------------------------------------------------- /tests/wait_task_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "wait_tasks.h" 5 | #include "start_tasks.h" 6 | #include "thread_pool.h" 7 | 8 | 9 | TEST(WaitTaskTest, SettingPromiseBarrier) { 10 | coros::detail::WaitTaskPromise promise = 11 | coros::detail::WaitTaskPromise{}; 12 | coros::detail::WaitBarrier bar{10, nullptr}; 13 | promise.set_barrier(&bar); 14 | EXPECT_EQ(promise.get_barrier().get_counter(), 10); 15 | } 16 | 17 | TEST(WaitTaskTest, MoveConstructor) { 18 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 0); 19 | { 20 | coros::detail::WaitTask task = []() -> coros::detail::WaitTask { 21 | co_return; 22 | }(); 23 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 1); 24 | 25 | std::coroutine_handle<> old_handle = task.get_handle(); 26 | coros::detail::WaitTask task2 = std::move(task); 27 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 2); 28 | 29 | EXPECT_EQ(task.get_handle(), nullptr); 30 | EXPECT_EQ(task2.get_handle(), old_handle); 31 | } 32 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 0); 33 | } 34 | 35 | TEST(WaitTaskTest, WaitTaskDestructor) { 36 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 0); 37 | { 38 | coros::Task t = []() -> coros::Task { 39 | co_return 42; 40 | }(); 41 | 42 | coros::detail::WaitTask wt = coros::detail::create_wait_task(t); 43 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 1); 44 | EXPECT_EQ(coros::Task::instance_count(), 1); 45 | } 46 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 0); 47 | EXPECT_EQ(coros::Task::instance_count(), 0); 48 | } 49 | 50 | 51 | TEST(WaitTaskTest, Construction) { 52 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 0); 53 | { 54 | coros::detail::WaitTask t1 = []() -> coros::detail::WaitTask { 55 | co_return; 56 | }(); 57 | 58 | coros::detail::WaitTask t2 = []() -> coros::detail::WaitTask { 59 | co_return; 60 | }(); 61 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 2); 62 | 63 | std::coroutine_handle<> old_t1_h = t1.get_handle(); 64 | std::coroutine_handle<> old_t2_h = t2.get_handle(); 65 | 66 | EXPECT_NE(t1.get_handle(), nullptr); 67 | EXPECT_NE(t2.get_handle(), nullptr); 68 | 69 | coros::ThreadPool tp{1}; 70 | 71 | coros::WaitTasksAwaitable awaiter = 72 | coros::WaitTasksAwaitable, 2>>{ 73 | tp, 74 | {std::move(t1), std::move(t2)}, 75 | {2,nullptr}}; 76 | 77 | std::array, 2>& moved_tasks = awaiter.get_tasks(); 78 | 79 | EXPECT_EQ(moved_tasks.size(), 2); 80 | // moved tasks are equal to the old one created 81 | EXPECT_EQ(moved_tasks[0].get_handle(), old_t1_h); 82 | EXPECT_EQ(moved_tasks[1].get_handle(), old_t2_h); 83 | // tasks are not suspended at final suspension point 84 | EXPECT_EQ(moved_tasks[0].get_handle().done(), false); 85 | EXPECT_EQ(moved_tasks[1].get_handle().done(), false); 86 | } 87 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 0); 88 | } 89 | 90 | 91 | TEST(WaitTaskTest, WaitForTasks) { 92 | coros::ThreadPool tp{1}; 93 | 94 | coros::Task t = [&]() -> coros::Task { 95 | coros::Task a = [](int num) -> coros::Task { 96 | co_return num; 97 | }(1); 98 | coros::Task b = [](int num) -> coros::Task { 99 | co_return num; 100 | }(2); 101 | 102 | co_await coros::wait_tasks(a, b); 103 | 104 | co_return *a + *b; 105 | }(); 106 | 107 | coros::start_sync(tp, t); 108 | 109 | EXPECT_EQ(t.value(), 3); 110 | } 111 | 112 | TEST(WaitTaskTest, WaitForTasksVector) { 113 | coros::ThreadPool tp{2}; 114 | 115 | coros::Task t = [&]() -> coros::Task { 116 | std::vector> vec; 117 | for (size_t i = 1; i <= 10;i++) { 118 | vec.push_back( 119 | [](int num) -> coros::Task { 120 | co_return num; 121 | }(i) 122 | ); 123 | } 124 | 125 | co_await coros::wait_tasks(vec); 126 | 127 | int result_sum = 0; 128 | for (auto& task : vec) { 129 | result_sum += *task; 130 | } 131 | 132 | co_return result_sum; 133 | }(); 134 | 135 | coros::start_sync(tp, t); 136 | 137 | EXPECT_EQ(t.value(), 55); 138 | } 139 | 140 | coros::Task fib2(int index) { 141 | if (index == 0) co_return 0; 142 | if (index == 1) co_return 1; 143 | 144 | coros::Task a = fib2(index - 1); 145 | coros::Task b = fib2(index - 2); 146 | 147 | co_await coros::wait_tasks(a, b); 148 | 149 | co_return *a + *b; 150 | } 151 | 152 | TEST(WaitTaskTest, WaitTaskFibonacci) { 153 | coros::ThreadPool tp{8}; 154 | coros::Task t = fib2(20); 155 | coros::start_sync(tp, t); 156 | EXPECT_EQ(t.value(), 6765); 157 | } 158 | 159 | coros::Task moveTaskExecution(coros::ThreadPool& tp, int index) { 160 | auto t1 = fib2(index); 161 | auto b = coros::wait_tasks(tp, t1); 162 | 163 | co_await b; 164 | 165 | co_return *t1; 166 | } 167 | 168 | // TODO : Could check the moving of the execution through thread ids. 169 | TEST(WaitTaskTest, WaitTaskFibonacciDifferentPool) { 170 | coros::ThreadPool tp1{1}; 171 | coros::ThreadPool tp2{1}; 172 | 173 | coros::Task t = moveTaskExecution(tp2, 20); 174 | coros::start_sync(tp1, t); 175 | EXPECT_EQ(t.value(), 6765); 176 | } 177 | 178 | 179 | TEST(WaitTaskTest, Lambda) { 180 | coros::ThreadPool tp1{2}; 181 | 182 | std::atomic n = 0; 183 | 184 | coros::start_sync( 185 | tp1, 186 | [&]() -> coros::Task{ 187 | auto t = fib2(20); 188 | co_await t; 189 | n = *t; 190 | }() 191 | ); 192 | 193 | EXPECT_EQ(n, 6765); 194 | } 195 | 196 | TEST(WaitTaskTest, RValueWaitTask) { 197 | auto t1 = []() -> coros::Task{ 198 | co_return; 199 | }(); 200 | auto wt = coros::detail::create_wait_task(std::move(t1)); 201 | } 202 | 203 | TEST(WaitTaskTest, DifferentTaskTypes) { 204 | EXPECT_EQ(coros::detail::WaitTask::instance_count(), 0); 205 | { 206 | coros::Task a = [](int num) -> coros::Task { 207 | co_return num; 208 | }(2); 209 | 210 | coros::Task b = []() -> coros::Task { 211 | co_return; 212 | }(); 213 | 214 | coros::Task c = [](coros::Task& a, coros::Task& b) -> coros::Task { 215 | co_await coros::wait_tasks(a, b); 216 | co_return 42; 217 | }(a, b); 218 | EXPECT_EQ(coros::Task::instance_count(), 2); 219 | EXPECT_EQ(coros::Task::instance_count(), 1); 220 | 221 | coros::ThreadPool tp(1); 222 | coros::start_sync(tp, c); 223 | EXPECT_EQ(*c, 42); 224 | } 225 | EXPECT_EQ(coros::Task::instance_count(), 0); 226 | EXPECT_EQ(coros::Task::instance_count(), 0); 227 | } 228 | 229 | TEST(WaitTaskTest, Async) { 230 | coros::ThreadPool tp{1}; 231 | 232 | coros::Task t = [&]() -> coros::Task { 233 | coros::Task a = [](int num) -> coros::Task { 234 | co_return num; 235 | }(1); 236 | coros::Task b = [](int num) -> coros::Task { 237 | co_return num; 238 | }(2); 239 | 240 | co_await coros::wait_tasks_async(a, b); 241 | 242 | co_return *a + *b; 243 | }(); 244 | 245 | coros::start_sync(tp, t); 246 | 247 | EXPECT_EQ(t.value(), 3); 248 | } 249 | 250 | TEST(WaitTaskTest, AsyncAwaitable) { 251 | coros::ThreadPool tp{1}; 252 | 253 | coros::Task t = [&]() -> coros::Task { 254 | coros::Task a = [](int num) -> coros::Task { 255 | co_return num; 256 | }(1); 257 | coros::Task b = [](int num) -> coros::Task { 258 | co_return num; 259 | }(2); 260 | 261 | auto awaitable = coros::wait_tasks_async(a, b); 262 | 263 | co_await awaitable; 264 | 265 | co_return *a + *b; 266 | }(); 267 | 268 | coros::start_sync(tp, t); 269 | 270 | EXPECT_EQ(t.value(), 3); 271 | } 272 | 273 | 274 | thread_local std::mt19937 gen(std::random_device{}()); 275 | 276 | int generate_random_numbers() { 277 | std::uniform_int_distribution<> distrib(1, 20); 278 | return distrib(gen); 279 | } 280 | 281 | 282 | coros::Task fib_async(int index) { 283 | if (index == 0) co_return 0; 284 | if (index == 1) co_return 1; 285 | 286 | coros::Task a = fib_async(index - 1); 287 | coros::Task b = fib_async(index - 2); 288 | 289 | auto awaitable = coros::wait_tasks_async(a, b); 290 | // std::chrono::milliseconds duration(generate_random_numbers()); 291 | // std::this_thread::sleep_for(duration); 292 | co_await awaitable; 293 | 294 | co_return *a + *b; 295 | } 296 | 297 | TEST(WaitTaskTest, WaitTaskFibonacciAsync) { 298 | coros::ThreadPool tp{8}; 299 | coros::Task t = fib_async(20); 300 | coros::start_sync(tp, t); 301 | 302 | EXPECT_EQ(t.value(), 6765); 303 | } 304 | 305 | 306 | coros::Task foo() { 307 | auto t1 = []() -> coros::Task { 308 | std::chrono::milliseconds duration(generate_random_numbers()); 309 | std::this_thread::sleep_for(duration); 310 | co_return; 311 | }(); 312 | 313 | auto t2 = []() -> coros::Task { 314 | std::chrono::milliseconds duration(generate_random_numbers()); 315 | std::this_thread::sleep_for(duration); 316 | co_return; 317 | }(); 318 | 319 | auto awaitable = coros::wait_tasks_async(t1, t2); 320 | std::chrono::milliseconds duration(generate_random_numbers()); 321 | std::this_thread::sleep_for(duration); 322 | co_await awaitable; 323 | 324 | co_return; 325 | } 326 | 327 | // Mainly for the sanitizers. 328 | TEST(WaitTaskTest, WaitTaskAsyncRun) { 329 | coros::ThreadPool tp{4}; 330 | 331 | for (size_t i = 0; i < 1000; i++) { 332 | coros::Task t = foo(); 333 | coros::start_sync(tp, t); 334 | } 335 | } 336 | 337 | TEST(WaitTaskTest, WaitTaskPoolVector) { 338 | coros::ThreadPool tp{2}; 339 | coros::ThreadPool tp2{2}; 340 | 341 | coros::Task t = [](coros::ThreadPool& tp) -> coros::Task { 342 | std::vector> vec; 343 | for (size_t i = 1; i <= 10;i++) { 344 | vec.push_back( 345 | [](int num) -> coros::Task { 346 | co_return num; 347 | }(i) 348 | ); 349 | } 350 | 351 | co_await coros::wait_tasks(tp, vec); 352 | 353 | int result_sum = 0; 354 | for (auto& task : vec) { 355 | result_sum += *task; 356 | } 357 | 358 | co_return result_sum; 359 | }(tp2); 360 | 361 | coros::start_sync(tp, t); 362 | 363 | EXPECT_EQ(t.value(), 55); 364 | } 365 | 366 | 367 | TEST(WaitTaskTest, WaitTaskAsyncVector) { 368 | 369 | coros::ThreadPool tp{2}; 370 | 371 | coros::Task t = []() -> coros::Task { 372 | std::vector> vec; 373 | for (size_t i = 1; i <= 10;i++) { 374 | vec.push_back( 375 | [](int num) -> coros::Task { 376 | co_return num; 377 | }(i) 378 | ); 379 | } 380 | 381 | auto awaitable = coros::wait_tasks_async(vec); 382 | 383 | co_await awaitable; 384 | 385 | int result_sum = 0; 386 | for (auto& task : vec) { 387 | result_sum += *task; 388 | } 389 | 390 | co_return result_sum; 391 | }(); 392 | 393 | coros::start_sync(tp, t); 394 | 395 | EXPECT_EQ(t.value(), 55); 396 | } 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | -------------------------------------------------------------------------------- /tests/task_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "constructor_counter.hpp" 5 | 6 | #include "task.h" 7 | 8 | TEST(TaskTest, TaskTrivialType) { 9 | EXPECT_EQ(coros::Task::instance_count(), 0); 10 | { 11 | coros::Task t = []() -> coros::Task { 12 | co_return 42; 13 | }(); 14 | EXPECT_EQ(coros::Task::instance_count(), 1); 15 | t.get_handle().resume(); 16 | EXPECT_EQ(t.value(), 42); 17 | } 18 | EXPECT_EQ(coros::Task::instance_count(), 0); 19 | } 20 | 21 | TEST(TaskTest, TaskVoid) { 22 | EXPECT_EQ(coros::Task::instance_count(), 0); 23 | { 24 | coros::Task t = []() -> coros::Task { 25 | co_return; 26 | }(); 27 | EXPECT_EQ(coros::Task::instance_count(), 1); 28 | t.get_handle().resume(); 29 | EXPECT_NO_THROW(t.value()); 30 | } 31 | EXPECT_EQ(coros::Task::instance_count(), 0); 32 | } 33 | 34 | TEST(TaskTest, TaskAccessResultBeforeSet) { 35 | EXPECT_EQ(coros::Task::instance_count(), 0); 36 | { 37 | coros::Task t = []() -> coros::Task { 38 | co_return 42; 39 | }(); 40 | EXPECT_EQ(coros::Task::instance_count(), 1); 41 | EXPECT_THROW(t.value(), std::bad_expected_access); 42 | } 43 | EXPECT_EQ(coros::Task::instance_count(), 0); 44 | } 45 | 46 | TEST(TaskTest, TaskException) { 47 | EXPECT_EQ(coros::Task::instance_count(), 0); 48 | { 49 | coros::Task t = []() -> coros::Task { 50 | throw std::bad_alloc(); 51 | co_return 42; 52 | }(); 53 | EXPECT_EQ(coros::Task::instance_count(), 1); 54 | t.get_handle().resume(); 55 | EXPECT_THROW(std::rethrow_exception(t.error()), std::bad_alloc); 56 | } 57 | EXPECT_EQ(coros::Task::instance_count(), 0); 58 | } 59 | 60 | // This test also covers no_throw_constructible type for return_value(). 61 | TEST(TaskTest, TaskNonTrivialType) { 62 | EXPECT_EQ(coros::Task::instance_count(), 0); 63 | { 64 | coros::Task t = []() -> coros::Task { 65 | co_return "Hello"; 66 | }(); 67 | EXPECT_EQ(coros::Task::instance_count(), 1); 68 | t.get_handle().resume(); 69 | EXPECT_EQ(t.value(), "Hello"); 70 | } 71 | EXPECT_EQ(coros::Task::instance_count(), 0); 72 | } 73 | 74 | TEST(TaskTest, TaskNonTrivialTypeNotInitialized) { 75 | EXPECT_EQ(coros::Task::instance_count(), 0); 76 | { 77 | coros::Task t = []() -> coros::Task { 78 | co_return "Hello"; 79 | }(); 80 | EXPECT_EQ(coros::Task::instance_count(), 1); 81 | } 82 | EXPECT_EQ(coros::Task::instance_count(), 0); 83 | } 84 | 85 | TEST(TaskTest, MoveConstructor) { 86 | { 87 | EXPECT_EQ(coros::Task::instance_count(), 0); 88 | coros::Task t1 = []() -> coros::Task { 89 | co_return "Hello"; 90 | }(); 91 | EXPECT_EQ(coros::Task::instance_count(), 1); 92 | 93 | std::coroutine_handle<> original_handle = t1.get_handle(); 94 | coros::Task t2 = std::move(t1); 95 | 96 | // A new task is constructed, now two instances of the task live. 97 | // One should have nullptr handle (moved from task, t1) and t2 should 98 | // have the original handle. 99 | EXPECT_EQ(coros::Task::instance_count(), 2); EXPECT_EQ(t1.get_handle(), nullptr); 100 | EXPECT_EQ(t2.get_handle(), original_handle); 101 | 102 | t2.get_handle().resume(); 103 | std::string result = t2.value(); 104 | EXPECT_EQ(result, "Hello"); 105 | } 106 | EXPECT_EQ(coros::Task::instance_count(), 0); 107 | } 108 | 109 | TEST(TaskTest, ControlTransfer) { 110 | { 111 | EXPECT_EQ(coros::Task::instance_count(), 0); 112 | coros::Task t1 = []() -> coros::Task { 113 | co_return 42; 114 | }(); 115 | coros::Task t2 = [&](coros::Task t) -> coros::Task { 116 | co_await t; 117 | co_return t.value() + 1; 118 | }(std::move(t1)); 119 | // 3 instances live, t1, t2, and t in the t2. 120 | EXPECT_EQ(coros::Task::instance_count(), 3); 121 | EXPECT_EQ(t1.get_handle(), nullptr); 122 | 123 | t2.get_handle().resume(); 124 | int result = t2.value(); 125 | EXPECT_EQ(result, 43); 126 | EXPECT_EQ(coros::Task::instance_count(), 3); 127 | } 128 | EXPECT_EQ(coros::Task::instance_count(), 0); 129 | } 130 | 131 | TEST(TaskTest, OperatorAsterisk) { 132 | EXPECT_EQ(coros::Task::instance_count(), 0); 133 | { 134 | coros::Task t = []() -> coros::Task { 135 | co_return "Hello"; 136 | }(); 137 | EXPECT_EQ(coros::Task::instance_count(), 1); 138 | t.get_handle().resume(); 139 | EXPECT_EQ(*t, "Hello"); 140 | } 141 | EXPECT_EQ(coros::Task::instance_count(), 0); 142 | } 143 | 144 | TEST(TaskTest, OperatorAsteriskRvalue) { 145 | EXPECT_EQ(coros::Task::instance_count(), 0); 146 | { 147 | coros::Task t = []() -> coros::Task { 148 | co_return "Hello"; 149 | }(); 150 | EXPECT_EQ(coros::Task::instance_count(), 1); 151 | t.get_handle().resume(); 152 | std::string result = *std::move(t); 153 | EXPECT_EQ(result, "Hello"); 154 | } 155 | EXPECT_EQ(coros::Task::instance_count(), 0); 156 | } 157 | 158 | TEST(TaskTest, OperatorAsteriskRvalueCounter) { 159 | coros::test::ConstructorCounter::clear_count(); 160 | coros::Task t = []() -> coros::Task { 161 | co_return coros::test::ConstructorCounter(); 162 | }(); 163 | t.get_handle().resume(); 164 | EXPECT_EQ(coros::test::ConstructorCounter::move_constructed, 1); 165 | [[maybe_unused]] coros::test::ConstructorCounter result = *std::move(t); 166 | EXPECT_EQ(coros::test::ConstructorCounter::move_constructed, 2); 167 | } 168 | 169 | TEST(TaskTest, OperatorAsteriskVoid) { 170 | EXPECT_EQ(coros::Task::instance_count(), 0); 171 | { 172 | coros::Task t = []() -> coros::Task { 173 | co_return; 174 | }(); 175 | EXPECT_EQ(coros::Task::instance_count(), 1); 176 | t.get_handle().resume(); 177 | EXPECT_NO_THROW(*t); 178 | } 179 | EXPECT_EQ(coros::Task::instance_count(), 0); 180 | } 181 | 182 | // TODO : Need to implement operator->(). 183 | 184 | TEST(TaskTest, ValueOrSet) { 185 | EXPECT_EQ(coros::Task::instance_count(), 0); 186 | { 187 | coros::Task t = []() -> coros::Task { 188 | co_return "Hello"; 189 | }(); 190 | EXPECT_EQ(coros::Task::instance_count(), 1); 191 | t.get_handle().resume(); 192 | EXPECT_EQ(t.value_or("Bye"), "Hello"); 193 | } 194 | EXPECT_EQ(coros::Task::instance_count(), 0); 195 | } 196 | 197 | TEST(TaskTest, ValueOrNotSet) { 198 | EXPECT_EQ(coros::Task::instance_count(), 0); 199 | { 200 | coros::Task t = []() -> coros::Task { 201 | co_return "Hello"; 202 | }(); 203 | EXPECT_EQ(coros::Task::instance_count(), 1); 204 | EXPECT_EQ(t.value_or("Bye"), "Bye"); 205 | } 206 | EXPECT_EQ(coros::Task::instance_count(), 0); 207 | } 208 | 209 | TEST(TaskTest, ValueOrMove) { 210 | EXPECT_EQ(coros::Task::instance_count(), 0); 211 | { 212 | coros::Task t = []() -> coros::Task { 213 | co_return "Hello"; 214 | }(); 215 | EXPECT_EQ(coros::Task::instance_count(), 1); 216 | t.get_handle().resume(); 217 | std::string result = std::move(t).value_or("Bye"); 218 | EXPECT_EQ(result, "Hello"); 219 | } 220 | EXPECT_EQ(coros::Task::instance_count(), 0); 221 | } 222 | 223 | TEST(TaskTest, ValueOrMoveCounter) { 224 | coros::test::ConstructorCounter::clear_count(); 225 | EXPECT_EQ(coros::Task::instance_count(), 0); 226 | { 227 | coros::Task t = []() -> coros::Task { 228 | co_return coros::test::ConstructorCounter(); 229 | }(); 230 | EXPECT_EQ(coros::Task::instance_count(), 1); 231 | t.get_handle().resume(); 232 | EXPECT_EQ(coros::test::ConstructorCounter::move_constructed, 1); 233 | [[maybe_unused]] coros::test::ConstructorCounter result = 234 | std::move(t).value_or(coros::test::ConstructorCounter()); 235 | // Check whether instnace of the ConstructorCounter was constructed 236 | // in the value_or. 237 | EXPECT_EQ(coros::test::ConstructorCounter::move_constructed, 2); 238 | } 239 | EXPECT_EQ(coros::Task::instance_count(), 0); 240 | } 241 | 242 | TEST(TaskTest, OperatorBoolSet) { 243 | coros::Task t = []() -> coros::Task { 244 | co_return "Hello"; 245 | }(); 246 | t.get_handle().resume(); 247 | 248 | EXPECT_TRUE(t); 249 | } 250 | TEST(TaskTest, OperatorBoolNotSet) { 251 | coros::Task t = []() -> coros::Task { 252 | co_return "Hello"; 253 | }(); 254 | 255 | EXPECT_FALSE(t); 256 | } 257 | 258 | TEST(TaskTest, HasValueSet) { 259 | coros::Task t = []() -> coros::Task { 260 | co_return "Hello"; 261 | }(); 262 | t.get_handle().resume(); 263 | 264 | EXPECT_TRUE(t.has_value()); 265 | } 266 | 267 | TEST(TaskTest, HasValueNotSet) { 268 | coros::Task t = []() -> coros::Task { 269 | co_return "Hello"; 270 | }(); 271 | 272 | EXPECT_FALSE(t.has_value()); 273 | } 274 | 275 | TEST(TaskTest, VoidSetValue) { 276 | EXPECT_EQ(coros::Task::instance_count(), 0); 277 | { 278 | auto t = []() -> coros::Task{ 279 | co_return; 280 | }(); 281 | EXPECT_EQ(t.has_value(), false); 282 | t.get_handle().resume(); 283 | EXPECT_EQ(t.has_value(), true); 284 | } 285 | EXPECT_EQ(coros::Task::instance_count(), 0); 286 | } 287 | 288 | TEST(TaskTest, VoidGetValue) { 289 | EXPECT_EQ(coros::Task::instance_count(), 0); 290 | { 291 | auto t = []() -> coros::Task{ 292 | co_return; 293 | }(); 294 | t.get_handle().resume(); 295 | EXPECT_EQ(t.has_value(), true); 296 | EXPECT_NO_THROW(*t); 297 | } 298 | EXPECT_EQ(coros::Task::instance_count(), 0); 299 | } 300 | 301 | TEST(TaskTest, VoidThrowException) { 302 | EXPECT_EQ(coros::Task::instance_count(), 0); 303 | { 304 | auto t = []() -> coros::Task{ 305 | throw std::bad_typeid(); 306 | co_return; 307 | }(); 308 | t.get_handle().resume(); 309 | EXPECT_EQ(t.has_value(), false); 310 | EXPECT_THROW(std::rethrow_exception(t.error()), std::bad_typeid); 311 | } 312 | EXPECT_EQ(coros::Task::instance_count(), 0); 313 | } 314 | 315 | namespace { 316 | 317 | struct ListInitialized { 318 | ListInitialized(std::initializer_list list) { 319 | std::size_t i = 0; 320 | for (auto elem : list) { 321 | arr_[i++] = elem; 322 | } 323 | }; 324 | 325 | std::array arr_; 326 | }; 327 | 328 | struct ListInitializedNoExcept { 329 | ListInitializedNoExcept(std::initializer_list list) noexcept { 330 | std::size_t i = 0; 331 | for (auto elem : list) { 332 | arr_[i++] = elem; 333 | } 334 | }; 335 | 336 | std::array arr_; 337 | }; 338 | 339 | } 340 | 341 | TEST(TaskTest, InitializerList) { 342 | EXPECT_EQ(coros::Task::instance_count(), 0); 343 | { 344 | auto t = []() -> coros::Task{ 345 | co_return {41, 42}; 346 | }(); 347 | t.get_handle().resume(); 348 | EXPECT_EQ(t.has_value(), true); 349 | EXPECT_EQ(t.value().arr_[0], 41); 350 | EXPECT_EQ(t.value().arr_[1], 42); 351 | } 352 | EXPECT_EQ(coros::Task::instance_count(), 0); 353 | } 354 | 355 | TEST(TaskTest, InitializerListNoThrow) { 356 | EXPECT_EQ(coros::Task::instance_count(), 0); 357 | { 358 | auto t = []() -> coros::Task{ 359 | co_return {41, 42}; 360 | }(); 361 | t.get_handle().resume(); 362 | EXPECT_EQ(t.has_value(), true); 363 | EXPECT_EQ(t.value().arr_[0], 41); 364 | EXPECT_EQ(t.value().arr_[1], 42); 365 | } 366 | EXPECT_EQ(coros::Task::instance_count(), 0); 367 | } 368 | 369 | -------------------------------------------------------------------------------- /include/task.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_TASK_H_ 2 | #define COROS_INCLUDE_TASK_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "constructor_counter.hpp" 10 | 11 | namespace coros { 12 | 13 | 14 | // C++ coroutines cannot accept parameters which do not 15 | // satify this concept. Coroutine parameter is passed as an xvalue 16 | // into a coroutine state to be copied/moved. Therefore, coros::Task only 17 | // works with such types. 18 | template 19 | concept TaskRetunType = std::is_constructible_v 20 | || std::is_void_v; 21 | 22 | // Forward declaration for Task, so it can be used in the Promise object. 23 | template 24 | class Task; 25 | 26 | namespace detail { 27 | 28 | // This awaiter resumes a coroutine by passing it a handle. 29 | // 30 | // task foo() { 31 | // co_await BasicAwaiter{bar_handle}; 32 | // } 33 | // 34 | // Pauses foo and resumes bar. 35 | class ResumeAwaiter { 36 | public: 37 | constexpr bool await_ready() noexcept { return false; } 38 | 39 | std::coroutine_handle<> await_suspend ( 40 | [[maybe_unused]] std::coroutine_handle<> currently_suspended) noexcept { 41 | return to_resume_; 42 | } 43 | 44 | void await_resume() noexcept {} 45 | 46 | std::coroutine_handle<> to_resume_; 47 | }; 48 | 49 | // Needs to be defined when running tests. Allows for checking the number 50 | // of task instances alive. 51 | #ifdef COROS_TEST_ 52 | #define INSTANCE_COUNTER_SIMPLE_PROMISE_ : private coros::test::InstanceCounter> 53 | #else 54 | #define INSTANCE_COUNTER_SIMPLE_PROMISE_ 55 | #endif 56 | 57 | // Promise object assosiacted with Task. 58 | template 59 | class SimplePromise INSTANCE_COUNTER_SIMPLE_PROMISE_ { 60 | public: 61 | 62 | using ResultType = std::expected; 63 | 64 | template> 65 | static std::enable_if_t>, T>, std::size_t> 66 | instance_count() { 67 | return coros::test::InstanceCounter>::instance_count(); 68 | } 69 | 70 | // Constructs a Task object that is returned to the user, when creating a task. 71 | // Coroutine handle (pointing to the coroutine state) is stored in Task object. This 72 | // allows for manipulating coroutine state and promise object through task. 73 | // 74 | // Task foo() {co_return 42;} 75 | // 76 | // Task object is constructed and returned to user. 77 | Task get_return_object() noexcept { 78 | return Task{std::coroutine_handle::from_promise(*this)}; 79 | }; 80 | 81 | // Lazily evaluated coroutine, suspend on initial_suspend. 82 | std::suspend_always initial_suspend() noexcept { return {}; } 83 | 84 | // If any task awaits thsi task, it is resumed. Otherwise default 85 | // value of noop_coroutine is passed to ResumeAwaiter, returning 86 | // control back to caller. 87 | ResumeAwaiter final_suspend() noexcept { 88 | return ResumeAwaiter{continuation_}; 89 | } 90 | 91 | template 92 | requires (std::is_nothrow_constructible_v> 93 | || std::is_nothrow_constructible_v) 94 | void return_value(U&& val) noexcept { 95 | result_.emplace(std::move(val)); 96 | } 97 | 98 | // Construct temporary object that can be moved/copied inot the result_; 99 | // Used in case the return type can throw in construction. Temporary is 100 | // created and moved/copied into return value. 101 | template 102 | requires (!std::is_nothrow_constructible_v> 103 | && (std::is_constructible_v> 104 | || std::is_constructible_v)) 105 | void return_value(U&& val) { 106 | result_ = T{std::move(val)}; 107 | } 108 | 109 | template 110 | requires (std::is_nothrow_constructible_v>) 111 | void return_value(std::initializer_list list) { 112 | result_.emplace(list); 113 | } 114 | 115 | template 116 | requires (!std::is_nothrow_constructible_v> 117 | && std::is_constructible_v>) 118 | void return_value(std::initializer_list list) { 119 | result_ = T{list}; 120 | } 121 | 122 | // Exception thrown in task body are stored in expected. 123 | void unhandled_exception() { 124 | result_ = std::unexpected(std::current_exception()); 125 | } 126 | 127 | ReturnValue&& value() && { 128 | return std::move(result_).value(); 129 | } 130 | 131 | // Return a reference to stored value. 132 | // If the value is not set throwd std::bad_expected_acesss<>. 133 | ReturnValue& value() & { 134 | return result_.value(); 135 | } 136 | 137 | std::exception_ptr& error() & noexcept { 138 | return result_.error(); 139 | } 140 | 141 | ReturnValue& operator*() & noexcept { 142 | return *result_; 143 | } 144 | 145 | ReturnValue&& operator*() && noexcept { 146 | return *std::move(result_); 147 | } 148 | 149 | template 150 | ReturnValue value_or(T&& default_value) & { 151 | return result_.value_or(std::forward(default_value)); 152 | } 153 | 154 | template 155 | ReturnValue value_or(T&& default_value) && { 156 | return std::move(result_).value_or(std::forward(default_value)); 157 | } 158 | 159 | ResultType& expected() & noexcept { 160 | return result_; 161 | } 162 | 163 | ResultType&& expected() && noexcept { 164 | return std::move(result_); 165 | } 166 | 167 | explicit operator bool() noexcept { 168 | return (bool)result_; 169 | } 170 | 171 | bool has_value() noexcept { 172 | return result_.has_value(); 173 | } 174 | 175 | void set_continuation(std::coroutine_handle<> cont) noexcept { continuation_ = cont; } 176 | 177 | private: 178 | // When we co_await task, we need to store a coroutine handle of a coroutine we want to resume 179 | // when this tasks finishes. Allows for coroutine to coroutine transfer of control. 180 | // std::coroutine_handle<> continuation_ = nullptr; 181 | std::coroutine_handle<> continuation_ = std::noop_coroutine(); 182 | // Stores either value or a exception that was thrown during coroutine's execution. 183 | std::expected result_{std::unexpect_t{}}; 184 | }; 185 | 186 | template <> 187 | class SimplePromise { 188 | public: 189 | 190 | using ResultType = std::expected; 191 | 192 | Task get_return_object() noexcept; 193 | 194 | std::suspend_always initial_suspend() noexcept { return {}; } 195 | 196 | ResumeAwaiter final_suspend() noexcept { 197 | return ResumeAwaiter{continuation_}; 198 | } 199 | 200 | // If the method returns void, and does not throw exception 201 | // we want the expected to has expected type void. 202 | void return_void() noexcept { 203 | result_.emplace(); 204 | } 205 | 206 | void value() && { 207 | return; 208 | } 209 | 210 | // Return a reference to stored value. 211 | // If the value is not set throwd std::bad_expected_acesss<>. 212 | void value() const& { 213 | return; 214 | } 215 | 216 | void operator*() noexcept { 217 | return ; 218 | } 219 | 220 | // Exception thrown in task body are stored in expected. 221 | void unhandled_exception() { 222 | result_ = std::unexpected(std::current_exception()); 223 | } 224 | 225 | std::exception_ptr& error() & noexcept { 226 | return result_.error(); 227 | } 228 | 229 | ResultType& expected() & noexcept { 230 | return result_; 231 | } 232 | 233 | ResultType&& expected() && noexcept { 234 | return std::move(result_); 235 | } 236 | 237 | explicit operator bool() noexcept { 238 | return (bool)result_; 239 | } 240 | 241 | bool has_value() noexcept { 242 | return result_.has_value(); 243 | } 244 | 245 | void set_continuation(std::coroutine_handle<> cont) noexcept { continuation_ = cont; } 246 | 247 | private: 248 | // When we co_await task, we need to store a coroutine handle of a coroutine we want to resume 249 | // when this tasks finishes. Allows for coroutine to coroutine transfer of control. 250 | // std::coroutine_handle<> continuation_ = nullptr; 251 | std::coroutine_handle<> continuation_ = std::noop_coroutine(); 252 | // Stores either value or a exception that was thrown during coroutine's execution. 253 | std::expected result_{std::unexpect_t{}}; 254 | }; 255 | 256 | // This awaiter suspends current coroutine and sets contination to a coroutine, we passed 257 | // to the constructor. This way we can resume the task when it finishes. 258 | // 259 | // task foo() { 260 | // co_await bar(); 261 | // } 262 | // 263 | // Suspends foo, sets foo as a contination to bar and resumes bar. 264 | template 265 | class ContinuationSetAwaiter { 266 | public: 267 | 268 | constexpr bool await_ready() noexcept { return false; } 269 | 270 | std::coroutine_handle<> await_suspend(std::coroutine_handle<> currently_suspended) noexcept { 271 | to_resume_.promise().set_continuation(currently_suspended); 272 | return to_resume_; 273 | } 274 | 275 | // returns result of the whole co_await expresion 276 | void await_resume() noexcept {} 277 | 278 | std::coroutine_handle to_resume_; 279 | }; 280 | 281 | 282 | // Needs to be defined when running tests. Allows for checking the number 283 | // of task instances alive. 284 | #ifdef COROS_TEST_ 285 | #define INSTANCE_COUNTER_ : private coros::test::InstanceCounter> 286 | #else 287 | #define INSTANCE_COUNTER_ 288 | #endif 289 | 290 | 291 | } // namespace detail 292 | 293 | // Task is an object returned to the user when a coroutine is created. Allows users 294 | // to get return value from the promise object. 295 | template 296 | class Task INSTANCE_COUNTER_ { 297 | public: 298 | // Coroutines traits looks for promise_type. 299 | using promise_type = detail::SimplePromise; 300 | 301 | template> 302 | static std::enable_if_t>, T>, std::size_t> 303 | instance_count() { 304 | return coros::test::InstanceCounter>::instance_count(); 305 | } 306 | 307 | // using InstanceCounter>::instance_count; 308 | 309 | Task() : handle_(nullptr) {}; 310 | 311 | Task(std::coroutine_handle handle) noexcept : handle_(handle){ }; 312 | 313 | Task(Task& other) = delete; 314 | 315 | Task(Task&& other) noexcept { 316 | handle_ = other.handle_; 317 | other.handle_ = nullptr; 318 | } 319 | 320 | Task& operator=(Task& other) = delete; 321 | 322 | Task& operator=(Task&& other) noexcept { 323 | handle_ = other.handle_; 324 | other.handle_ = nullptr; 325 | return *this; } 326 | 327 | ~Task() { 328 | if (handle_ != nullptr) { 329 | handle_.destroy(); 330 | } 331 | } 332 | 333 | // When Task is co_awaited it is suspended and continuation to the calling 334 | // task is set, allowing for control transfer once the co_await-ed task is done. 335 | detail::ContinuationSetAwaiter operator co_await() noexcept { 336 | return detail::ContinuationSetAwaiter{handle_}; 337 | } 338 | 339 | // Returns void. 340 | template 341 | std::enable_if_t> 342 | value() & { } 343 | 344 | // Return a value stored in expected. Can potentionally throw. 345 | template 346 | std::enable_if_t, T>& 347 | value() & { 348 | return handle_.promise().value(); 349 | } 350 | 351 | std::exception_ptr& error() & noexcept { 352 | return handle_.promise().error(); 353 | } 354 | 355 | template 356 | std::enable_if_t, T>& 357 | operator*() & noexcept { 358 | return *handle_.promise(); 359 | } 360 | 361 | template 362 | std::enable_if_t> 363 | operator*() & noexcept { } 364 | 365 | template 366 | std::enable_if_t, T>&& 367 | operator*() && noexcept { 368 | return std::move(*handle_.promise()); 369 | } 370 | 371 | template 372 | std::enable_if_t> 373 | operator*() && noexcept { } 374 | 375 | template 376 | ReturnValue value_or(T&& default_value) & { 377 | return handle_.promise().value_or(std::forward(default_value)); 378 | } 379 | 380 | template 381 | ReturnValue value_or(T&& default_value) && { 382 | return std::move(handle_.promise()).value_or(std::forward(default_value)); 383 | } 384 | 385 | std::expected& expected() & noexcept { 386 | return handle_.promise().expected(); 387 | } 388 | 389 | std::expected&& expected() && noexcept { 390 | return std::move(handle_.promise()).expected(); 391 | } 392 | 393 | explicit operator bool() const noexcept { 394 | return static_cast(handle_.promise().expected()); 395 | } 396 | 397 | bool has_value() const noexcept { 398 | return handle_.promise().has_value(); 399 | } 400 | 401 | std::coroutine_handle get_handle() const noexcept { return handle_; } 402 | 403 | private: 404 | std::coroutine_handle handle_ = nullptr; 405 | }; 406 | 407 | // Placed here, because of forward declaration. 408 | inline Task detail::SimplePromise::get_return_object() noexcept { 409 | return Task{std::coroutine_handle>::from_promise(*this)}; 410 | }; 411 | 412 | } // namespace coros 413 | 414 | #endif // COROS_INCLUDE_TASK_H_ 415 | -------------------------------------------------------------------------------- /include/chain_tasks.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_CHAIN_TASKS_H_ 2 | #define COROS_INCLUDE_CHAIN_TASKS_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "task.h" 13 | 14 | namespace coros { 15 | namespace detail { 16 | 17 | template 18 | struct extract_return_type; 19 | 20 | template 21 | struct extract_return_type(*)(U)> { 22 | using type = R; 23 | }; 24 | 25 | template 26 | using extract_return_type_t = typename extract_return_type::type; 27 | 28 | template 29 | concept FunctionNoParams = requires { 30 | requires std::is_function_v; 31 | requires std::is_same_v || std::is_same_v; 32 | }; 33 | 34 | template 35 | struct is_Task : std::false_type{}; 36 | 37 | template 38 | struct is_Task> : std::true_type{}; 39 | 40 | template 41 | concept IsTask = is_Task::value; 42 | 43 | template 44 | concept IsNotTask = !is_Task::value; 45 | 46 | // 47 | // Forward declarations. 48 | // 49 | 50 | template 51 | requires (!FunctionNoParams && IsNotTask) 52 | coros::Task process_tasks(U val, F f, Funcs... functions); 53 | 54 | template 55 | requires (!FunctionNoParams && IsNotTask) 56 | coros::Task process_tasks(U val, F f); 57 | 58 | template 59 | coros::Task process_tasks(coros::Task(*f)(), Funcs... functions); 60 | 61 | inline coros::Task process_tasks(coros::Task(*f)()); 62 | 63 | template 64 | coros::Task process_tasks(coros::Task(*f)()); 65 | 66 | template 67 | coros::Task process_tasks(coros::Task& starting_task, Funcs... functions); 68 | 69 | template 70 | coros::Task process_tasks(coros::Task& starting_task); 71 | 72 | // 73 | // Function definitions. 74 | // 75 | 76 | template 77 | requires (!FunctionNoParams && IsNotTask) 78 | coros::Task process_tasks(U val, F f, Funcs... functions) { 79 | 80 | // Extract the return type from the pointer 81 | using return_type = extract_return_type_t; 82 | 83 | // Construct the promise object and task object 84 | coros::Task t = f(std::move(val)); 85 | 86 | // Execute the function 87 | co_await t; 88 | 89 | // An error occured. Should return unexpected 90 | // Rethrown exception will be caught, propagation 91 | // of the exception. 92 | if (!t.has_value()) { 93 | std::rethrow_exception(t.error()); 94 | } 95 | 96 | // TODO : Do not like this, should be more readable. 97 | if constexpr (std::is_void_v) { 98 | auto next_task = coros::detail::process_tasks(functions...); 99 | co_await next_task; 100 | 101 | if (!next_task.has_value()) { 102 | std::rethrow_exception(next_task.error()); 103 | } 104 | 105 | co_return; 106 | } else { 107 | auto next_task = coros::detail::process_tasks(*std::move(t), functions...); 108 | co_await next_task; 109 | 110 | if (!next_task.has_value()) { 111 | std::rethrow_exception(next_task.error()); 112 | } 113 | 114 | if constexpr (std::is_void_v) { 115 | co_return; 116 | } else{ 117 | co_return *std::move(next_task); 118 | } 119 | } 120 | } 121 | 122 | // Base case 123 | template 124 | requires (!FunctionNoParams && IsNotTask) 125 | coros::Task process_tasks(U val, F f) { 126 | // Extract the return type from the pointer 127 | using return_type = extract_return_type_t; 128 | 129 | // Construct the promise object and task object 130 | coros::Task t = f(std::move(val)); 131 | 132 | // Execute the function 133 | co_await t; 134 | 135 | // An error occurred. Should return unexpected 136 | // Rethrown exception will be caught, propagation 137 | // of the exception. 138 | if (!t.has_value()) { 139 | std::rethrow_exception(t.error()); 140 | } 141 | 142 | if constexpr (std::is_void_v) { 143 | co_return; 144 | } else{ 145 | co_return *std::move(t); 146 | } 147 | } 148 | 149 | // TODO : Maybe fix the concept. 150 | template 151 | coros::Task process_tasks(coros::Task(*f)(), Funcs... functions) { 152 | coros::Task t = f(); 153 | co_await t; 154 | 155 | if (!t.has_value()) { 156 | std::rethrow_exception(t.error()); 157 | } 158 | 159 | auto next_task = coros::detail::process_tasks(functions...); 160 | co_await next_task; 161 | 162 | if (!next_task.has_value()) { 163 | std::rethrow_exception(next_task.error()); 164 | } 165 | 166 | co_return; 167 | } 168 | 169 | inline coros::Task process_tasks(coros::Task(*f)()) { 170 | coros::Task t = f(); 171 | co_await t; 172 | 173 | 174 | if (!t.has_value()) { 175 | std::rethrow_exception(t.error()); 176 | } 177 | 178 | co_return; 179 | } 180 | 181 | template 182 | coros::Task process_tasks(coros::Task& starting_task, Funcs... functions) { 183 | // Execute the first task. 184 | co_await starting_task; 185 | 186 | if (!starting_task.has_value()) { 187 | std::rethrow_exception(starting_task.error()); 188 | } 189 | 190 | if constexpr (std::is_void_v) { 191 | auto next_task = coros::detail::process_tasks(functions...); 192 | co_await next_task; 193 | 194 | if (!next_task.has_value()) { 195 | std::rethrow_exception(next_task.error()); 196 | } 197 | 198 | co_return; 199 | } else { 200 | auto next_task = coros::detail::process_tasks(*std::move(starting_task), functions...); 201 | co_await next_task; 202 | 203 | if (!next_task.has_value()) { 204 | std::rethrow_exception(next_task.error()); 205 | } 206 | 207 | if constexpr (std::is_void_v) { 208 | co_return; 209 | } else{ 210 | co_return *next_task; 211 | } 212 | } 213 | } 214 | 215 | // Base case 216 | template 217 | coros::Task process_tasks(coros::Task& starting_task) { 218 | 219 | // Execute the function 220 | co_await starting_task; 221 | 222 | // An error occurred. Should return unexpected 223 | // Rethrown exception will be caught, propagation 224 | // of the exception. 225 | if (!starting_task.has_value()) { 226 | std::rethrow_exception(starting_task.error()); 227 | } 228 | 229 | if constexpr (std::is_void_v) { 230 | co_return; 231 | } else{ 232 | co_return *starting_task; 233 | } 234 | } 235 | 236 | } // namespace detail 237 | 238 | 239 | // Template arguments: 240 | // 241 | // StartingType - Type that is passed to the constructor of the awaitable. 242 | // EndingType - Type that is returned to the user, final expected returned to 243 | // the user is std::expected. 244 | // TaskStart - Set to true if the awaitable is passed a task to execute 245 | // rather than a value. 246 | // Funcs - Individual accumulated functions that should be executed on the value 247 | // when the awaitable is co_await-ed. 248 | template< 249 | typename StartingType, 250 | typename EndingType = StartingType, 251 | bool TaskStart = false, 252 | typename... Funcs> 253 | class ChainAwaitable { 254 | public: 255 | 256 | ChainAwaitable(std::expected&& expected) 257 | : expected_(std::move(expected)) {} 258 | 259 | ChainAwaitable(std::expected& expected) 260 | : expected_(expected) {} 261 | 262 | ChainAwaitable(coros::Task&& starting_task) 263 | : starting_task_(std::move(starting_task)) {} 264 | 265 | template 266 | ChainAwaitable(U&& val) 267 | : expected_({std::forward(val)}) {} 268 | 269 | 270 | explicit ChainAwaitable( 271 | std::expected&& ex, 272 | std::tuple&& tuple) 273 | : expected_(std::move(ex)), 274 | functions_(std::move(tuple)) {} 275 | 276 | explicit ChainAwaitable( 277 | std::expected&& ex, 278 | std::tuple&& tuple, 279 | coros::Task&& starting_task) 280 | : expected_(std::move(ex)), 281 | functions_(std::move(tuple)), 282 | starting_task_(std::move(starting_task)) {} 283 | 284 | // TODO : implement later 285 | // HACK : Getting some weird behavior with parsing the template, 286 | // therefor I use AwaiterVoid. 287 | auto operator co_await() { 288 | if constexpr (std::is_void_v) { 289 | return AwaiterVoid{this}; 290 | } else { 291 | return Awaiter{this}; 292 | } 293 | } 294 | 295 | 296 | template 297 | struct Awaiter { 298 | 299 | constexpr bool await_ready() noexcept {return false;} 300 | 301 | // Here I should suspend, and run a task that runs individaul functions. 302 | std::coroutine_handle<> 303 | await_suspend(std::coroutine_handle<> currently_suspended) noexcept { 304 | loop_task_.get_handle().promise().set_continuation(currently_suspended); 305 | return loop_task_.get_handle(); 306 | } 307 | 308 | 309 | [[nodiscard]] std::expected&& await_resume() noexcept { 310 | return std::move(loop_task_).expected(); 311 | } 312 | 313 | ChainAwaitable* awaitable_; 314 | // Needed to use lambda to make it work. Not sure why. 315 | coros::Task loop_task_ = 316 | std::apply( 317 | [](auto&&... args) { 318 | return coros::detail::process_tasks(std::forward(args)...); 319 | }, 320 | [this]() -> auto { 321 | if constexpr (!TaskStart) { 322 | return std::tuple_cat( 323 | std::make_tuple(std::move(awaitable_->expected_).value()), 324 | awaitable_->functions_); 325 | } else { 326 | return std::tuple_cat( 327 | std::tie(awaitable_->starting_task_), 328 | awaitable_->functions_); 329 | } 330 | }() 331 | ); 332 | }; 333 | 334 | struct AwaiterVoid { 335 | 336 | constexpr bool await_ready() noexcept {return false;} 337 | 338 | // Here I should suspend, and run a task that runs individaul functions. 339 | std::coroutine_handle<> 340 | await_suspend(std::coroutine_handle<> currently_suspended) noexcept { 341 | loop_task_.get_handle().promise().set_continuation(currently_suspended); 342 | return loop_task_.get_handle(); 343 | } 344 | 345 | [[nodiscard]] std::expected&& await_resume() noexcept { 346 | return std::move(loop_task_).expected(); 347 | } 348 | 349 | ChainAwaitable* awaitable_; 350 | // Needed to use lambda to make it work. Not sure why. 351 | coros::Task loop_task_ = 352 | std::apply( 353 | [](auto&&... args) { 354 | return coros::detail::process_tasks(std::forward(args)...); 355 | }, 356 | [this]() -> auto { 357 | if constexpr (!TaskStart) { 358 | return std::tuple_cat( 359 | std::make_tuple(std::move(awaitable_->expected_).value()), 360 | awaitable_->functions_); 361 | } else { 362 | return std::tuple_cat( 363 | std::tie(awaitable_->starting_task_), 364 | awaitable_->functions_); 365 | } 366 | }() 367 | ); 368 | }; 369 | 370 | // Should create a new ChainAwaitable and add the chaining function into the list. 371 | // We do not stop here but in chaining function. This function checks whether the 372 | // types are valid. Meaning, the function takes the input type currently stored in ChainAwaitable. 373 | // Could this be done in constant time. I think so ! 374 | template 375 | constexpr auto and_then(coros::Task(*fun)(U)) { 376 | // TODO: Should remove the std::function ? 377 | static_assert( 378 | std::is_invocable_v(U)>, EndingType>, 379 | "Task in and_then() needs to accept corret type"); 380 | 381 | // Add and_then into a tuple 382 | // functions.tuple_cat(std::make_tuple(fun)); 383 | auto new_tuple = std::tuple_cat(functions_, std::make_tuple(fun)); 384 | 385 | // Need to create a new expected, with a new type. 386 | if constexpr (TaskStart) { 387 | return ChainAwaitable(*)(U)>{ 388 | std::move(expected_), 389 | std::move(new_tuple), 390 | std::move(starting_task_)}; 391 | } else { 392 | return ChainAwaitable(*)(U)>{ 393 | std::move(expected_), 394 | std::move(new_tuple)}; 395 | } 396 | } 397 | 398 | constexpr auto and_then(coros::Task(*fun)()) { 399 | // Add and_then into a tuple 400 | // functions.tuple_cat(std::make_tuple(fun)); 401 | auto new_tuple = std::tuple_cat(functions_, std::make_tuple(fun)); 402 | 403 | // Need to create a new expected, with a new type. 404 | if constexpr (TaskStart) { 405 | return ChainAwaitable(*)()>{ 406 | std::move(expected_), 407 | std::move(new_tuple), 408 | std::move(starting_task_)}; 409 | } else { 410 | return ChainAwaitable(*)()>{ 411 | std::move(expected_), 412 | std::move(new_tuple)}; 413 | } 414 | } 415 | 416 | private: 417 | std::expected expected_; 418 | std::tuple functions_; 419 | coros::Task starting_task_; 420 | }; 421 | 422 | 423 | template 424 | ChainAwaitable chain_tasks(std::expected& ex) { 425 | return ChainAwaitable{ex}; 426 | } 427 | 428 | template 429 | ChainAwaitable chain_tasks(std::expected&& ex) { 430 | return ChainAwaitable{std::move(ex)}; 431 | } 432 | 433 | template 434 | ChainAwaitable chain_tasks(coros::Task&& task) { 435 | return ChainAwaitable{std::move(task)}; 436 | } 437 | 438 | template 439 | requires( 440 | !std::is_same_v, std::exception_ptr>, 441 | std::remove_reference_t> 442 | &&std::is_constructible_v, 443 | std::exception_ptr>, std::remove_reference_t>) 444 | ChainAwaitable, std::remove_reference_t, false> chain_tasks(T&& val) { 445 | return ChainAwaitable, 446 | std::remove_reference_t, 447 | false> 448 | {std::forward(val)}; 449 | } 450 | 451 | } // namespace coros 452 | 453 | #endif // COROS_INCLUDE_CHAIN_TASKS_H_ 454 | -------------------------------------------------------------------------------- /include/wait_tasks.h: -------------------------------------------------------------------------------- 1 | #ifndef COROS_INCLUDE_WAIT_TASKS_H_ 2 | #define COROS_INCLUDE_WAIT_TASKS_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include "task_life_time.h" 8 | #include "wait_barrier.h" 9 | #include "task.h" 10 | #include "thread_pool.h" 11 | 12 | #include "constructor_counter.hpp" 13 | 14 | namespace coros { 15 | namespace detail { 16 | 17 | 18 | // TODO: can do some concept for awaitable types 19 | template 20 | class WaitTask; 21 | 22 | template 23 | class WaitTaskPromise { 24 | public: 25 | // TODO: look at exceptions 26 | WaitTask get_return_object(); 27 | 28 | std::suspend_always initial_suspend() noexcept { return {}; } 29 | 30 | // when tasks finishes we need to decrease the counter 31 | // on the barrier so we can resume the awaiting task 32 | auto final_suspend() noexcept { 33 | class DecreaseCounterAwaiter { 34 | public: 35 | // TODO : remove it so it can be may be trivially constructible ? 36 | // DecreaseCounterAwaiter(WaitTaskPromise* p) noexcept : finished_task_promise(p) {} 37 | 38 | constexpr bool await_ready() noexcept { return false; } 39 | 40 | std::coroutine_handle<> await_suspend( 41 | [[maybe_unused]] std::coroutine_handle<> currently_suspended) noexcept { 42 | // TODO: make decrement_and_resume noexcept 43 | return finished_task_promise->barrier_->decrement_and_resume(); 44 | } 45 | 46 | void await_resume() noexcept {} 47 | 48 | WaitTaskPromise* finished_task_promise; 49 | }; 50 | 51 | return DecreaseCounterAwaiter{this}; 52 | } 53 | 54 | void return_void() {} 55 | 56 | // TODO: maybe change approach 57 | void unhandled_exception() const { std::rethrow_exception(std::current_exception()); } 58 | 59 | void set_barrier(BarrierType* bar) noexcept { barrier_ = bar; } 60 | 61 | BarrierType& get_barrier() const noexcept { return *barrier_; } 62 | 63 | private: 64 | BarrierType* barrier_; 65 | }; 66 | 67 | // Needs to be defined when running tests. Allows for checking the number 68 | // of task instances alive. 69 | #ifdef COROS_TEST_ 70 | #define INSTANCE_COUNTER_WAIT_TASK_ : private coros::test::InstanceCounter> 71 | #else 72 | #define INSTANCE_COUNTER_WAIT_TASK_ 73 | #endif 74 | 75 | // Object representing a wait WaitTask, similar to Task object. This object 76 | // is used to manipulate WaitTask and the promise/coroutine state associated 77 | // with it. 78 | // RAII object. 79 | template 80 | class WaitTask INSTANCE_COUNTER_WAIT_TASK_ { 81 | public: 82 | // need a public coroutine::promise_type for coroutine traits 83 | // to deduce a promsie type 84 | using promise_type = WaitTaskPromise; 85 | 86 | template 87 | static std::enable_if_t, T>, std::size_t> 88 | instance_count() { 89 | return coros::test::InstanceCounter::instance_count(); 90 | } 91 | 92 | WaitTask(std::coroutine_handle handle) noexcept : task_handle_(handle){}; 93 | 94 | WaitTask(const WaitTask& other) = delete; 95 | WaitTask& operator=(const WaitTask& other) = delete; 96 | 97 | WaitTask(WaitTask&& other) noexcept { 98 | task_handle_ = other.task_handle_; 99 | other.task_handle_ = nullptr; 100 | } 101 | 102 | WaitTask& operator=(WaitTask&& other) noexcept { 103 | task_handle_ = other.task_handle_; 104 | other.task_handle_ = nullptr; 105 | return *this; 106 | } 107 | 108 | // Need to keep the if statement in case the task is moved, 109 | // we do not want to destroy the state. RAII 110 | ~WaitTask() { 111 | if (task_handle_) { 112 | task_handle_.destroy(); 113 | } 114 | } 115 | 116 | std::coroutine_handle get_handle() const noexcept { return task_handle_; } 117 | 118 | void set_barrier(BarrierType* barrier) const noexcept { task_handle_.promise().set_barrier(barrier); } 119 | 120 | private: 121 | std::coroutine_handle task_handle_; 122 | }; 123 | 124 | } // namespace detail 125 | 126 | // An awaitable that is used that pauses current task, schedules individual tasks 127 | // for execution into and threadpool. 128 | // 129 | // task foo() { 130 | // auto t1 = task1(); 131 | // auto t2 = task2(); 132 | // 133 | // co_await wait_for_tasks(t1, t2); // Here awaitable is constructed 134 | // 135 | // co_return; 136 | // } 137 | // 138 | // Once all tasks are done the parent task foo, is resumed. It is resumed on the 139 | // pool where t1 and t2 were executed. 140 | template 141 | class WaitTasksAwaitable { 142 | public: 143 | 144 | using TaskType = typename Container::value_type; 145 | 146 | auto operator co_await() { 147 | struct Awaiter { 148 | public: 149 | 150 | // This should be changed because task can be finished before we start executing. 151 | // TODO : Changed only if we do async waiting 152 | constexpr bool await_ready() const noexcept { return false; } 153 | 154 | // Suspends current coroutine, sets barrier for all tasks, 155 | // sets continuation for the barrier(currently suspended coroutine) and 156 | // adds tasks to thread pool for execution. 157 | void await_suspend(std::coroutine_handle<> currently_suspended) noexcept { 158 | awaitable_->barrier_.set_continuation(currently_suspended); 159 | 160 | for (auto& task : awaitable_->tasks_) { 161 | task.set_barrier(&(awaitable_->barrier_)); 162 | awaitable_->tp_.add_task({task.get_handle(), detail::TaskLifeTime::SCOPE_MANAGED}); 163 | } 164 | } 165 | 166 | void await_resume() noexcept {} 167 | 168 | WaitTasksAwaitable* awaitable_; 169 | }; 170 | return Awaiter{this}; 171 | } 172 | 173 | Container& get_tasks() noexcept { return tasks_; } 174 | 175 | ThreadPool& tp_; 176 | Container tasks_; 177 | detail::WaitBarrier barrier_; 178 | }; 179 | 180 | template 181 | class WaitTasksAwaitableVector { 182 | public: 183 | 184 | auto operator co_await() { 185 | struct Awaiter { 186 | public: 187 | 188 | // This should be changed because task can be finished before we start executing. 189 | // TODO : Changed only if we do async waiting 190 | constexpr bool await_ready() const noexcept { return false; } 191 | 192 | // Suspends current coroutine, sets barrier for all tasks, 193 | // sets continuation for the barrier(currently suspended coroutine) and 194 | // adds tasks to thread pool for execution. 195 | void await_suspend(std::coroutine_handle<> currently_suspended) noexcept { 196 | awaitable_->barrier_.set_continuation(currently_suspended); 197 | 198 | for (auto& task : awaitable_->tasks_) { 199 | task.set_barrier(&(awaitable_->barrier_)); 200 | awaitable_->tp_.add_task({task.get_handle(), detail::TaskLifeTime::SCOPE_MANAGED}); 201 | } 202 | } 203 | 204 | void await_resume() noexcept {} 205 | 206 | WaitTasksAwaitableVector* awaitable_; 207 | }; 208 | return Awaiter{this}; 209 | } 210 | 211 | std::vector>& get_tasks() noexcept { return tasks_; } 212 | 213 | ThreadPool& tp_; 214 | std::vector> tasks_; 215 | detail::WaitBarrier barrier_; 216 | }; 217 | 218 | 219 | template 220 | class WaitTasksAwaitableAsync { 221 | public: 222 | 223 | WaitTasksAwaitableAsync(coros::ThreadPool& tp, 224 | Container&& tasks) 225 | : tp_(tp), 226 | tasks_(std::move(tasks)), 227 | barrier_{std::make_shared(tasks_.size())} { 228 | // Set barrier in tasks and schedule individual tasks. 229 | barrier_->set_barrier_pointer(&barrier_); 230 | for(auto& task : tasks_) { 231 | task.set_barrier(barrier_.get()); 232 | tp_.add_task({task.get_handle(), detail::TaskLifeTime::SCOPE_MANAGED}); 233 | } 234 | } 235 | 236 | auto operator co_await() { 237 | struct Awaiter { 238 | public: 239 | 240 | // Optimization when all tasks are finished, we do not need 241 | // to suspend. 242 | constexpr bool await_ready() const noexcept { 243 | if (this->awaitable_->barrier_->remaining_tasks.load() == 0) { 244 | // All tasks are finished 245 | return true; 246 | } else { 247 | return false; 248 | } 249 | } 250 | 251 | std::coroutine_handle<> await_suspend(std::coroutine_handle<> currently_suspended) noexcept { 252 | std::shared_ptr barrier = std::atomic_load(&(awaitable_->barrier_)); 253 | // Once we store the continuation into the barrier, 254 | // there is no guarantee that the awaiter is still alive, 255 | // so we need to store the pointer to the barrier in function stack. 256 | barrier->continuation.store(currently_suspended); 257 | barrier->handle_ready_.store(true); 258 | int remaining_tasks = barrier->remaining_tasks.load(); 259 | 260 | if (remaining_tasks >= 1) { 261 | // At least on tasks hasn't decremented the barrier. We can be 262 | // sure that the last tasks reads the set handle, given the 263 | // total order, sequential consistency. 264 | return std::noop_coroutine(); 265 | } else { 266 | bool expected = true; 267 | if (barrier->handle_ready_.compare_exchange_strong( 268 | expected, false)) { 269 | // If continuation contains a nullptr, it means that the last task 270 | // took the continuation and set the nullptr. We can be sure 271 | // that the coroutine will be resumed from the last task. 272 | // return std::noop_coroutine(); 273 | return currently_suspended; 274 | } else { 275 | // Last task did not manage to get the stored handle, we need to resume 276 | // the current coroutine. 277 | // return currently_suspended; 278 | return std::noop_coroutine(); 279 | } 280 | } 281 | } 282 | 283 | void await_resume() noexcept {} 284 | 285 | WaitTasksAwaitableAsync* awaitable_; 286 | }; 287 | return Awaiter{this}; 288 | } 289 | 290 | Container& get_tasks() noexcept { return tasks_; } 291 | 292 | ThreadPool& tp_; 293 | Container tasks_; 294 | // With shared pointer we have guarantee that the barrier is destroyed 295 | // only once the coroutine is resumed and the awaiter is destroyed and 296 | // when the await_suspend function ends. 297 | // This ensures that the barrier lives long enough for the synchronizatoin 298 | // between tasks and that are awaited and the parent coroutine. 299 | // std::atomic> barrier_; 300 | std::shared_ptr barrier_; 301 | }; 302 | 303 | 304 | // Suspends the current coroutine and waits for tasks from different pool. 305 | template 306 | class WaitTasksPoolAwaitable { 307 | public: 308 | 309 | auto operator co_await() { 310 | struct Awaiter { 311 | public: 312 | Awaiter(WaitTasksPoolAwaitable* awaitable) : awaitable_(awaitable) {} 313 | 314 | constexpr bool await_ready() const noexcept { return false; } 315 | 316 | // Suspends current coroutine, sets barrier for all tasks, 317 | // sets continuation for the barrier(currently suspended coroutine) and 318 | // adds tasks to thread pool for execution. 319 | void await_suspend(std::coroutine_handle<> currently_suspended) { 320 | awaitable_->barrier_.set_continuation(currently_suspended); 321 | 322 | for (auto& task : awaitable_->tasks_) { 323 | task.set_barrier(&(awaitable_->barrier_)); 324 | awaitable_->tp_.add_task_from_outside({task.get_handle(), detail::TaskLifeTime::SCOPE_MANAGED}); 325 | } 326 | } 327 | 328 | // TODO : returning multiple values has to be done here. 329 | void await_resume() {} 330 | 331 | private: 332 | WaitTasksPoolAwaitable* awaitable_; 333 | }; 334 | return Awaiter(this); 335 | } 336 | 337 | Container& get_tasks() noexcept { return tasks_; } 338 | 339 | ThreadPool& tp_; 340 | Container tasks_; 341 | detail::WaitBarrier barrier_; 342 | }; 343 | 344 | namespace detail{ 345 | 346 | template 347 | inline WaitTask WaitTaskPromise::get_return_object() { 348 | return WaitTask(std::coroutine_handle>::from_promise(*this)); 349 | } 350 | 351 | // Used for lvalues when the task is passed by lvalue reference. 352 | template 353 | WaitTask wait_task_body_ref(T& t) { 354 | co_await t; 355 | } 356 | 357 | // Used for rvalues, once this WaitTask is destroyed the task 358 | // stored in parameter list in coroutine state is also destroyed. 359 | // Task t lifetime is tied to this task. 360 | template 361 | WaitTask wait_task_body_val(T t) { 362 | co_await t; 363 | } 364 | 365 | template 366 | inline WaitTask create_wait_task(T&& t) { 367 | if constexpr (std::is_reference_v) { 368 | return wait_task_body_ref(std::forward(t)); 369 | } else { 370 | return wait_task_body_val(std::forward(t)); 371 | } 372 | } 373 | 374 | } // namespace detail 375 | 376 | // TODO: maybe add a concept whether the parameter is an awaitable. So it can be 377 | // co_awaited. 378 | // Constructs a vector of wait tasks and returns an awaitable 379 | template 380 | inline auto wait_tasks(Args&&... args) { 381 | constexpr size_t arr_size = sizeof...(Args); 382 | return WaitTasksAwaitable, arr_size>>{ 383 | *thread_my_pool, 384 | {detail::create_wait_task(std::forward(args))...}, 385 | {arr_size, nullptr}}; 386 | } 387 | 388 | template 389 | inline auto wait_tasks(std::vector>& tasks) { 390 | size_t vec_size = tasks.size(); 391 | std::vector> wait_task_vec; 392 | for (auto& task : tasks) { 393 | wait_task_vec.push_back(detail::create_wait_task(task)); 394 | } 395 | return WaitTasksAwaitable>>{ 396 | *thread_my_pool, 397 | std::move(wait_task_vec), 398 | {vec_size, nullptr}}; 399 | } 400 | 401 | template 402 | inline auto wait_tasks_async(Args&&... args) { 403 | constexpr size_t arr_size = sizeof...(Args); 404 | return WaitTasksAwaitableAsync< 405 | std::array, arr_size>>( 406 | *thread_my_pool, 407 | {detail::create_wait_task(std::forward(args))...}); 408 | } 409 | 410 | template 411 | inline auto wait_tasks_async(std::vector>& tasks) { 412 | std::vector> wait_task_vec; 413 | for (auto& task : tasks) { 414 | wait_task_vec.push_back(detail::create_wait_task(task)); 415 | } 416 | return WaitTasksAwaitableAsync< 417 | std::vector>>( 418 | *thread_my_pool, 419 | std::move(wait_task_vec)); 420 | } 421 | 422 | // Moves tasks between thread pools. 423 | template 424 | inline auto wait_tasks(coros::ThreadPool& pool, Args&&... args) { 425 | constexpr size_t arr_size = sizeof...(Args); 426 | return WaitTasksPoolAwaitable, arr_size>>{ 427 | pool, 428 | {detail::create_wait_task(std::forward(args))...}, 429 | {arr_size, nullptr}}; 430 | } 431 | 432 | template 433 | inline auto wait_tasks(coros::ThreadPool& pool, 434 | std::vector>& tasks) { 435 | size_t vec_size = tasks.size(); 436 | std::vector> wait_task_vec; 437 | for (auto& task : tasks) { 438 | wait_task_vec.push_back(detail::create_wait_task(task)); 439 | } 440 | return WaitTasksPoolAwaitable>>{ 441 | pool, 442 | std::move(wait_task_vec), 443 | {vec_size, nullptr}}; 444 | } 445 | 446 | } // namespace coros 447 | 448 | 449 | #endif // COROS_INCLUDE_WAIT_TASKS_H_ 450 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Coros

2 | 3 |
4 | 5 | [![GCC](https://github.com/mtmucha/coros/actions/workflows/gcc_test.yml/badge.svg?branch=main)](https://github.com/mtmucha/coros/actions/workflows/gcc_test.yml) 6 | [![Clang](https://github.com/mtmucha/coros/actions/workflows/clang_test.yml/badge.svg?branch=main)](https://github.com/mtmucha/coros/actions/workflows/clang_test.yml) 7 | [![tests](https://github.com/mtmucha/coros/actions/workflows/test_build.yml/badge.svg)](https://github.com/mtmucha/coros/actions/workflows/test_build.yml) 8 | [![address sanitizer](https://github.com/mtmucha/coros/actions/workflows/add_build.yml/badge.svg?branch=main)](https://github.com/mtmucha/coros/actions/workflows/add_build.yml) 9 | [![thread sanitizer](https://github.com/mtmucha/coros/actions/workflows/tsan_build.yml/badge.svg)](https://github.com/mtmucha/coros/actions/workflows/tsan_build.yml) 10 | 11 |
12 | 13 | Coros is a header-only C++23 library designed for task-based parallelism, 14 | that utilizes coroutines and the new expected type. Key features include: 15 | 16 | - **Ease of use**: Straightforward interface and header-only installation. 17 | - **Performance**: Optimized to ensure you don't miss out on performance ([see benchmarks below](#benchmarks)). 18 | - **Exception handling**: Utilizes `std::expected` for error management. 19 | - **Monadic operations**: Supports easy chaining of tasks with `and_then` method. 20 | 21 | Transforming a standard sequential function into a parallel task with Coros is as simple as: 22 | 23 |

24 | 25 | 26 | 27 | Shows an illustrated sun in light mode and a moon with stars in dark mode. 28 | 29 |

30 | 31 | # Benchmarks 32 | 33 | We have conducted two benchmarks to evaluate the performance of our library: 34 | 35 | - **Fibonacci Calculation**: Calculating the 30th Fibonacci number to assess the scheduling overhead. 36 | - **Matrix Multiplication**: A standard workload that tests overall performance. 37 | 38 | ### Calculating Fibonacci number 39 | 40 |

41 | 42 | Code Screenshot 1 43 | 44 | 45 | Code Screenshot 2 46 | 47 |

48 | 49 | ### Matrix multiplication 50 | 51 |

52 | 53 | Code Screenshot 1 54 | 55 | 56 | Code Screenshot 2 57 | 58 |

59 | 60 | 61 | # Documentation 62 | 63 | Using the Coros library is straightforward, there are three main components: 64 | 65 | - `coros::ThreadPool`: As the name suggests, this is a thread pool that manages a desired number of worker 66 | threads and executes individual tasks. 67 | - `coros::Task`: This object serves as the task container, which must be used as the return value from tasks. 68 | The T parameter specifies the type the task returns. For a simple example, refer to the accompanying image. 69 | - **Awaiters**: These are objects that support the `co_await` operator, allowing them to be awaited within tasks. 70 | They are typically used for control flow, for example, waiting for other tasks to finish. 71 | 72 | For additional details about the library, refer to the documentation provided below and check out the examples in the example folder. 73 | 74 | - [Documentation](#documentation) 75 | - [Installation](#installation) 76 | - [Creating tasks and starting execution](#creating-tasks-and-starting-execution) 77 | - [Waiting for other tasks](#waiting-for-other-tasks) 78 | - [Enqueueing tasks](#enqueueing-tasks) 79 | - [Chaining tasks](#chaining-tasks) 80 | 81 | # Installation 82 | 83 | To install the Coros library, simply place the include folder into your project directory and set the include path. 84 | There are four key headers you can include: 85 | 86 | - `#include "start_tasks.h"`: Provides the functionality to set up tasks, thread pool object, and launch task execution from the main thread. 87 | - `#include "wait_tasks.h"`: Enables suspension of individual tasks while waiting for others to complete. 88 | - `#include "enqueue_tasks.h"`: Allows for the enqueuing of tasks into a thread pool without awaiting their completion. 89 | - `#include "chain_tasks.h"`: Supports chaining of tasks, this chain is then executed on a thread pool. 90 | 91 | To compile the library, ensure your compiler supports C++23 feature std::expected. Compatible compilers: 92 | 93 | - GCC 13 or newer 94 | - Clang 17 or newer 95 | - MSVC (not yet supported) 96 | 97 | Do not forget to enable coroutines for given compiler, for example `-fcoroutines` for GCC. 98 | 99 | > [!NOTE] 100 | > The library uses `std::hardware_destructive_interference_size` if supported by the compiler. 101 | > You can also set this value manually by passing a flag to the compiler, or you may choose to ignore it. 102 | > This is used as an optimization to avoid false sharing. 103 | 104 | # Creating tasks and starting execution 105 | 106 | To set up a task and start parallel execution the necessary steps are: 107 | 108 | - Construct a `coros::ThreadPool`: This will be the execution environment for your tasks. 109 | - Construct tasks using `coros::Task`: Define the tasks and create a task object. These tasks can be run 110 | on the thread pool object. 111 | - Start execution: Use `coros::start_sync` or `coros::start_async` to initiate execution from the main thread. 112 | 113 | 114 | `coros::start_sync`/`coros::start_async` are functions designed to start parallel execution from the main thread. 115 | 116 | **To create `coros::Task`, `coros::ThreadPool` or use `coros::start_sync`/`coros::start_async` 117 | include the `#include "start_tasks"` header.** 118 | 119 | ## `coros::Task` 120 | 121 | Task is a coroutine(added in C++20) that returns a `coros::Task` object. To 122 | transform a regular function into a coroutine, instead of `return` keyword a `co_return` 123 | keyword must be used. And to transform a coroutine into a task a `coros::Task` must 124 | be a return type of the coroutine. Tasks support two keywords : 125 | 126 | A `coros::Task` is a coroutine object, a feature introduced in C++20. To convert a standard function into a coroutine, replace the `return` keyword with `co_return`. 127 | Additionally, the function must specify `coros::Task` as its return type to function as a task. Tasks support two keywords: 128 | 129 | - `co_return`: Use this keyword instead of return in your return statements. 130 | - `co_await`: Use this for flow control with awaitable objects(objects that support `co_await` operator). 131 | 132 | The return type `T` **must satisfy** constraint `std::is_constructible`. 133 | This requirement ensures that return values can be constructed from an r-value reference, utilizing either a move or a copy constructor. 134 | This constraint arises because coroutine parameters themselves must also satisfy the std::is_constructible condition. 135 | 136 | ```Cpp 137 | coros::Task add_one(int val) { 138 | co_return val + 1; 139 | } 140 | 141 | int main() { 142 | coros::ThreadPool tp{/*number_of_threads=*/2}; 143 | // Create the task object by calling the coroutine. 144 | coros::Task task = add_one(41); 145 | // Wait until the task is finished, this call blocks. 146 | coros::start_sync(tp, task); 147 | // After this point, task is completed. 148 | // Optional check for stored value. 149 | if (task.has_value()) { 150 | std::cout << "Result : " << *task << std::endl; 151 | } else { 152 | // It is possible to process the caught exception. 153 | std::cout << "Task failed" << std::endl; 154 | } 155 | } 156 | ``` 157 | 158 | 159 | > [!WARNING] 160 | > While it's possible to use lambda coroutines to construct tasks, be cautious with captures and references. 161 | Best practice is passing values and references through coroutine parameters rather than captures to ensure safety and avoid unexpected behavior. 162 | For more detail [see the C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#SScp-coro). 163 | 164 | Under the hood, `coros::Task` employs `std::expected` to store the outcome of the coroutine/task. 165 | This structure holds either a value, indicating successful completion, or an `std::exception_ptr` if an exception occurred. 166 | For convenience, `coros::Task` offers methods analogous to those of std::expected: 167 | 168 | - `T value()`: Accesses the stored value directly. 169 | - `std::exception_ptr error()`: Retrieves the stored `std::exception_ptr`. 170 | - `operator*()`: Provides direct access to the result. 171 | - `T value_or(T defaultValue)`: Returns the stored value if the task contains a value; otherwise, it returns the specified default value. 172 | - `std::expected expected()`: Returns the underlying std::expected object. 173 | - `operator bool()`: Returns true if the task contains a successfully stored value. 174 | - `bool has_value()`: Returns true if the task contains a successfully stored value. 175 | 176 | > [!NOTE] 177 | > Methods `value()` and `operator*()` **are not supported** for specialization `coros::Task`. 178 | 179 | `coros::Task` supports the `co_await` operator, making it an awaitable object. 180 | When a task is awaited using `co_await`, it behaves similarly to a regular function call: the awaited task executes and, upon completion, control returns to the calling task. 181 | The difference is that this operation typically does not consume additional stack space, thanks to the coroutine-to-coroutine control transfer. 182 | 183 | ```Cpp 184 | coros::Task add_one(int val) { 185 | co_return val + 1; 186 | } 187 | 188 | coros::Task add_value(int val) { 189 | coros::Task another_task = add_one(val); 190 | // Once the another task finishes, control is returned, 191 | // this works like a regular function. 192 | co_await another_task; 193 | // Accesses the another_task's result and increments it by one. 194 | // NOTE : check for the value is omitted. 195 | co_return *another_task + 1; 196 | } 197 | ``` 198 | 199 | ## `coros::start_sync(coros::ThreadPool&, Tasks&&...)` 200 | 201 | To start tasks on a thread pool, you specify the desired thread pool and the tasks to be executed. 202 | It is crucial that the `coros::ThreadPool` object outlives the execution of the tasks. 203 | 204 |
205 | 206 | code example 207 | 208 | ```Cpp 209 | coros::Task add_one(int val) { 210 | co_return val + 1; 211 | } 212 | 213 | int main() { 214 | coros::ThreadPool tp{/*number_of_threads=*/2}; 215 | coros::Task task = add_one(41); 216 | coros::start_sync( 217 | tp, 218 | // Lambda function that does the same ad add_one. Creates a temporary task object, which means 219 | // we cannot access its value. 220 | [](int val) -> coros::Task {co_return val + 1;}(41), 221 | task 222 | ); 223 | // Cannot retrieve value from the lambda function, but can retrieve 224 | // value from the task. 225 | std::cout << *task << std::endl; // prints : 42 226 | } 227 | ``` 228 | 229 |
230 | 231 | 232 | ## `coros::start_async(coros::ThreadPool&, Tasks&&...)` 233 | 234 | A task can be started asynchronously from the main thread, which allows the main thread to continue working while other tasks execute on the thread pool. 235 | 236 |
237 | 238 | code example 239 | 240 | ```Cpp 241 | coros::Task add_one(int val) { 242 | co_return val + 1; 243 | } 244 | 245 | int main() { 246 | coros::ThreadPool tp{/*number_of_threads=*/2}; 247 | coros::Task task = add_one(41); 248 | auto start_task = coros::start_async( 249 | tp, 250 | [](int val) -> coros::Task {co_return val + 1;}(41), 251 | task 252 | ); 253 | // 254 | // Main thread can do some work. 255 | // 256 | // Call wait, blocks if tasks hasn't finished 257 | start_task.wait() 258 | std::cout << *task << std::endl; // prints : 42 259 | } 260 | ``` 261 | 262 |
263 | 264 | # Waiting for other tasks 265 | 266 | The Coros library provides mechanisms to wait for other tasks to complete. 267 | This is achieved by suspending the current task (if necessary) and resuming it later. 268 | There are two main methods and their overloads for handling task waiting: 269 | 270 | - `coros::wait_tasks(Tasks&&...)` 271 | - `coros::wait_tasks(coros::ThreadPool&, Tasks&&...)` 272 | - `coros::wait_tasks(std::vector>&)` 273 | - `coros::wait_tasks(coros::ThreadPool&, std::vector>&)` 274 | - `coors::wait_tasks_async(Tasks&&...)` 275 | - `coors::wait_tasks_async(std::vector>&)` 276 | 277 | The main difference between the synchronous and asynchronous versions is in how they schedule tasks into a thread pool: 278 | 279 | - The **asynchronous version** schedules tasks into a thread pool upon creation. 280 | - The **synchronous version** schedules tasks into a thread pool only when they are explicitly `co_await`-ed. 281 | 282 | Each of these function returns an awaitable object which can be `co_await`-ed by 283 | calling the `co_await` operator. 284 | 285 | **To use `coros::wait_tasks` include the `#include "wait_tasks"` header.** 286 | 287 | ## `coros::wait_tasks(Tasks&&...)` 288 | 289 | Calling `coros::wait_tasks()` generates an awaitable object that supports the `co_await` operator. 290 | This can be awaited to suspend the current task, which resumes only after the specified tasks have completed. 291 | This approach allows individual threads to perform useful work without blocking. 292 | 293 |
294 | 295 | code example 296 | 297 | ```Cpp 298 | coros::Task add_one(int val) { 299 | co_return val + 1; 300 | } 301 | 302 | coros::Task add_to_number(int val) { 303 | coros::Task task = add_one(val); 304 | // The current task is suspended until task add_one completes. 305 | co_await coros::wait_tasks(task); 306 | co_return *task; 307 | } 308 | ``` 309 | 310 |
311 | 312 | ## `coros::wait_tasks(coros::ThreadPool&, Tasks&&...)` 313 | 314 | `coros::wait_task()` accepts variable number of tasks and it is also possible to specify 315 | the `coros::ThreadPool&` parameter, which moves tasks between thread pools. 316 | Once the awaiting tasks are finished, the task is resumed on the specified thread pool. 317 | 318 | The `coros::wait_tasks()` function can accept a variable number of tasks, and it also allows for specifying a `coros::ThreadPool&`. 319 | This makes it possible to move tasks between thread pools. 320 | Once the tasks being awaited are completed, the awaiting task resumes on the specified thread pool. 321 | 322 |
323 | 324 | code example 325 | 326 | ```Cpp 327 | coros::ThreadPool tp{/*number_of_threads=*/2}; 328 | 329 | coros::Task add_one(int val) { 330 | co_return val + 1; 331 | } 332 | 333 | coros::Task add_to_number(int val) { 334 | 335 | coros::Task task = add_one(val); 336 | // The awaitable returned from the function can be directly co_awaited or 337 | // stored into a variable and co_awaited later. 338 | auto awaitable = coros::wait_tasks(tp, task); 339 | // Store awaitable into a variable and suspend later. 340 | co_await awaitable; 341 | // This part is resumed on the thread pool specified by the 342 | // parameter. 343 | co_return *task; 344 | } 345 | ``` 346 | 347 |
348 | 349 | 350 | ## `coros::wait_tasks(std::vector>&)` 351 | 352 | It's possible to pass a vector of `coros::Task` into the `coros::wait_tasks()` function to await the completion of multiple tasks. 353 | This version **can also move tasks between thread pools** if a thread pool parameter is specified. 354 | 355 |
356 | 357 | code example 358 | 359 | ```Cpp 360 | coros::Task add_one_and_sum(int n) { 361 | std::vector> vec; 362 | for (size_t i = 1; i <= n;i++) { 363 | // Constructs tasks with lambda function. 364 | // Simple task that adds one to passed parameter. 365 | vec.push_back( 366 | [](int num) -> coros::Task { 367 | co_return num + 1; 368 | }(i) 369 | ); 370 | } 371 | // Suspends the current task and is resumed once all tasks are finished. 372 | co_await coros::wait_tasks(vec); 373 | 374 | int result_sum = 0; 375 | for (auto& task : vec) { 376 | result_sum += *task; 377 | } 378 | 379 | co_return result_sum; 380 | }; 381 | ``` 382 | 383 |
384 | 385 | ## `coros::wait_tasks_async(Tasks&&...)` 386 | 387 | This function operates similarly to `coros::wait_tasks`, with the primary distinction being that the async version schedules the tasks as soon as the 388 | awaitable is created(when `coros::wait_tasks_async` is called). 389 | Note that the async version **does not support moving tasks between thread pools**. When the awaitable is `co_await`-ed, one of two scenarios may occur: 390 | 391 | - If all tasks are already completed, the task is not suspended and continues execution immediately. 392 | - If at least one task has not yet finished, the task is suspended and will resume once all tasks have completed. 393 | 394 | 395 |
396 | 397 | code example 398 | 399 | ```Cpp 400 | coros::Task add_one(int val) { 401 | co_return val + 1; 402 | } 403 | 404 | coros::Task add_to_number(int val) { 405 | coros::Task task = add_one(val); 406 | auto awaitable = coros::wait_tasks_async(task); 407 | // Store awaitable into a variable and suspend later. 408 | // 409 | // Do some work. 410 | // 411 | // Checks if the task have already finished, if not it suspends the task. 412 | co_await awaitable; 413 | co_return *task; 414 | } 415 | ``` 416 | 417 |
418 | 419 | ## `coros::wait_tasks_async(std::vector>&)` 420 | 421 | A `std::vector>` can be passed into `coros::wait_tasks_async`, similar to its non-async counterpart. 422 | 423 |
424 | 425 | code example 426 | 427 | ```Cpp 428 | coros::Task add_one_and_sum(int n) { 429 | std::vector> vec; 430 | for (size_t i = 1; i <= n;i++) { 431 | // Constructs tasks with lambda function. 432 | vec.push_back( 433 | [](int num) -> coros::Task { 434 | co_return num + 1; 435 | }(i) 436 | ); 437 | } 438 | // Async version can also be directly co_await-ed. 439 | co_await coros::wait_tasks_async(vec); 440 | 441 | int result_sum = 0; 442 | for (auto& task : vec) { 443 | result_sum += *task; 444 | } 445 | 446 | co_return result_sum; 447 | }; 448 | ``` 449 | 450 |
451 | 452 | # Enqueueing tasks 453 | 454 | Contrary to to awaiting tasks with `coros::wait_tasks` or `coros::wait_tasks_async` 455 | is `coros::enqueue_tasks`, which schedules tasks into a threadpool without waiting for them. 456 | Given that these tasks are not awaited, their results cannot be retrieved and 457 | any exception thrown inside these tasks is immediately rethrown. 458 | 459 | Unlike `coros::wait_tasks` or `coros::wait_tasks_async`, which suspend the current task until others are completed, `coros::enqueue_tasks` 460 | schedules tasks into a threadpool without awaiting their completion. 461 | Since these tasks are not awaited, their results cannot be retrieved directly, and any exceptions thrown within these tasks are immediately rethrown. 462 | 463 | Overloads for the `coros::enqueue_tasks` are : 464 | 465 | - `enqueue_tasks(Tasks&&...)` 466 | - `enqueue_tasks(coros::ThreadPool&, Tasks&&...)` 467 | - `enqueue_tasks(std::vector>&&)` 468 | - `enqueue_tasks(coros::ThreadPool&, std::vector>&&)` 469 | 470 | All these functions are constrained to **only accept r-value references** 471 | because the tasks are enqueued and not awaited, which means their `coros::Task` objects are temporary(destroyed when finished) and cannot be used to retrieve values. 472 | 473 | **To use `coros::enqueue_tasks` include the `#include "enqueue_tasks"` header.** 474 | 475 | ## `coros::enqueue_tasks(Tasks&&...)` 476 | 477 |
478 | 479 | code example 480 | 481 | ```Cpp 482 | std::atomic counter = 0; 483 | 484 | coros::Task add_one() { 485 | counter++; // Atomic operation to increase the counter. 486 | co_return; 487 | } 488 | 489 | coros::Task increase_counter() { 490 | coros::enqueue_tasks(add_one(), add_one()); 491 | co_return; 492 | } 493 | 494 | int main() { 495 | coros::ThreadPool tp{2}; 496 | coros::Task t = increase_counter(); 497 | coros::start_sync(tp, t); 498 | // The resulting counter value can be 0, 1, or 2. The increase_counter 499 | // task is finished at this point; however, this does not guarantee 500 | // that the add_one tasks have also completed their execution. 501 | std::cout << counter.load() << std::endl; 502 | } 503 | ``` 504 | 505 |
506 | 507 | ## `coros::enqueue_tasks(coros::ThreadPool&, Tasks&&...)` 508 | 509 | 510 |
511 | 512 | 513 | code example 514 | 515 | ```Cpp 516 | std::atomic counter = 0; 517 | 518 | coros::Task add_one() { 519 | counter++; 520 | co_return; 521 | } 522 | 523 | coros::Task increase_counter(coros::ThreadPool& tp) { 524 | // Both tasks will be executed on the specified thread pool. 525 | coros::enqueue_tasks(tp, add_one(), add_one()); 526 | co_return; 527 | } 528 | 529 | int main() { 530 | coros::ThreadPool tp{2}; 531 | coros::ThreadPool tp2{2}; 532 | coros::Task t = increase_counter(tp2); 533 | coros::start_sync(tp, t); 534 | // The resulting counter value can be 0, 1, or 2. The increase_counter 535 | // task is finished at this point; however, this does not guarantee 536 | // that the add_one tasks have also completed their execution. 537 | std::cout << counter.load() << std::endl; 538 | } 539 | ``` 540 | 541 |
542 | 543 | ## `coros::enqueue_tasks(std::vector>&&)` 544 | 545 | 546 |
547 | 548 | code example 549 | 550 | ```Cpp 551 | std::atomic counter = 0; 552 | 553 | coros::Task add_one() { 554 | counter++; 555 | co_return; 556 | } 557 | 558 | coros::Task increase_counter() { 559 | std::vector> vec; 560 | vec.push_back(add_one()); 561 | vec.push_back(add_one()); 562 | coros::enqueue_tasks(std::move(vec)); 563 | co_return; 564 | } 565 | 566 | int main() { 567 | coros::ThreadPool tp{2}; 568 | coros::Task t = increase_counter(); 569 | coros::start_sync(tp, t); 570 | // The resulting counter value can be 0, 1, or 2. The increase_counter 571 | // task is finished at this point; however, this does not guarantee 572 | // that the add_one tasks have also completed their execution. 573 | std::cout << counter.load() << std::endl; 574 | } 575 | ``` 576 | 577 |
578 | 579 | 580 | 581 | # Chaining tasks 582 | 583 | The Coros library supports a monadic operation with the `and_then` method, similar to `std::expected`, but with a key difference: 584 | the Coros version executes the chain of tasks on a thread pool. 585 | If an exception occurs during the chain, it is captured into the result, and the chain is halted. The resulting type of this operation is `std::expected`. 586 | There are three overloads for this function: 587 | 588 | - `coros::chain_tasks(T&&)`: Starts a chain of tasks with a value. 589 | - `coros::chain_tasks(coros::Task&&)`: Starts a chain of tasks with a 590 | **unstarted** task 591 | - `coros::chain_tasks(std::expected&&)`: Starts a chain 592 | of tasks with an expected value 593 | 594 | **All of these overloads accept r-value references**. 595 | Given that this operation mimics monadic behavior, each task in the chain must accept exactly one argument. 596 | Additionally, it must be possible to construct the argument of each subsequent task with the result from the previous task. 597 | 598 | **To use `coros::chain_tasks` include the `#include "chain_tasks"` header.** 599 | 600 | ## `coros::chain_tasks(T&&)` 601 | 602 | One way to start a task chain is by passing a starting value. The function requires an `r-value reference`, indicating that the starting value should be movable, 603 | or copyable through r-value reference. 604 | 605 |
606 | 607 | code example 608 | 609 | ```Cpp 610 | coros::Task add_two(int val) { 611 | co_return val + 2; 612 | } 613 | 614 | coros::Task multiply_by_six(int val) { 615 | co_return val * 6; 616 | } 617 | 618 | coros::Task compute_value() { 619 | // Each task must accept exactly one argument. 620 | // Each subsequent task's parameter must be 621 | // constructible from the previous task return value. 622 | std::expected res = 623 | co_await coros::chain_tasks(3).and_then(add_two) 624 | .and_then(add_two) 625 | .and_then(multiply_by_six); 626 | if (res.has_value()) { 627 | // Return the computed value if the chain completes successfully. 628 | co_return *res; 629 | } else { 630 | // In case the chain did not successfully finish, an exception occurred. 631 | co_return -1; 632 | } 633 | } 634 | ``` 635 | 636 |
637 | 638 | ## `coros::chain_tasks(coros::Task&&)` 639 | 640 | It is also possible to start a chain with **unstarted** `coros::task` object. 641 | The unstarted task will be executed first and then each subsequent task. 642 | 643 | It is possible to initiate a task chain with an unstarted `coros::Task`. 644 | The chain will start by executing this unstarted task, followed by each subsequent task in the sequence. 645 | 646 |
647 | 648 | code example 649 | 650 | ```Cpp 651 | coros::Task add_two(int val) { 652 | co_return val + 2; 653 | } 654 | 655 | coros::Task multiply_by_six(int val) { 656 | co_return val * 6; 657 | } 658 | 659 | coros::Task return_three() { 660 | co_return 3; 661 | } 662 | 663 | coros::Task compute_value() { 664 | // Each task must accept exactly most one argument. 665 | // Each subsequent task's parameter must be 666 | // constructible from the previous task return value. 667 | std::expected res = 668 | co_await coros::chain_tasks(return_three()) // Initializes the chain with a task that returns 3. 669 | .and_then(add_two) // First subsequent operation, adds two to the value 3. 670 | .and_then(add_two) 671 | .and_then(multiply_by_six); 672 | 673 | if (res.has_value()) { 674 | // Return the computed value if the chain completes successfully. 675 | co_return *res; 676 | } else { 677 | // In case the chain did not successfully finish, an exception occurred. 678 | co_return -1; 679 | } 680 | } 681 | ``` 682 | 683 |
684 | 685 | ## `coros::chain_tasks(std::expected&&)` 686 | 687 | This overload is particularly useful when you need to start a new chain from the result of a previous chain, 688 | potentially extending the computation or handling with new tasks. 689 | 690 |
691 | 692 | code example 693 | 694 | ```Cpp 695 | coros::Task add_two(int val) { 696 | co_return val + 2; 697 | } 698 | 699 | coros::Task multiply_by_six(int val) { 700 | co_return val * 6; 701 | } 702 | 703 | coros::Task return_three() { 704 | co_return 3; 705 | } 706 | 707 | coros::Task compute_value() { 708 | std::expected res = 709 | co_await coros::chain_tasks(return_three()).and_then(add_two) 710 | .and_then(add_two) 711 | .and_then(multiply_by_six); 712 | if (res.has_value()) { 713 | // Return the computed value if the chain completes successfully. 714 | auto another_res = co_await coros::chain_tasks(std::move(res)).and_then(add_two); 715 | co_return *another_res; // returns 44 716 | } else { 717 | // In case the chain did not successfully finish, an exception occurred. 718 | co_return -1; 719 | } 720 | } 721 | ``` 722 | 723 |
724 | 725 | # References 726 | 727 | - [Concurrent deque](https://github.com/cameron314/concurrentqueue?tab=readme-ov-file): Used in the project as part of the scheduling algorithm. 728 | - [Deque implementation](https://inria.hal.science/hal-00802885/document): Used as a reference for implementing our own double-ended queue. 729 | - [Lewis Baker's blog](https://lewissbaker.github.io/): Provides excellent explanations of coroutines. 730 | 731 | # Licence 732 | 733 | Boost Software License - Version 1.0 - August 17th, 2003 734 | 735 | Permission is hereby granted, free of charge, to any person or organization 736 | obtaining a copy of the software and accompanying documentation covered by 737 | this license (the "Software") to use, reproduce, display, distribute, 738 | execute, and transmit the Software, and to prepare derivative works of the 739 | Software, and to permit third-parties to whom the Software is furnished to 740 | do so, all subject to the following: 741 | 742 | The copyright notices in the Software and this entire statement, including 743 | the above license grant, this restriction and the following disclaimer, 744 | must be included in all copies of the Software, in whole or in part, and 745 | all derivative works of the Software, unless such copies or derivative 746 | works are solely in the form of machine-executable object code generated by 747 | a source language processor. 748 | 749 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 750 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 751 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 752 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 753 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 754 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 755 | DEALINGS IN THE SOFTWARE. 756 | 757 | --------------------------------------------------------------------------------