From 446765843cd101afd10af8bbfaa6fefc3bc97d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Kardos?= Date: Mon, 20 Nov 2023 10:27:39 +0100 Subject: [PATCH 1/3] readme, fixes --- LICENSE.md | 1 - README.md | 235 ++++++++++++++++++++++++++++++++++ include/async++/lock.hpp | 10 ++ include/async++/scheduler.hpp | 26 ++-- test/test_mutex.cpp | 15 +++ test/test_shared_mutex.cpp | 15 +++ 6 files changed, 283 insertions(+), 19 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 81242c8..d764d13 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,3 @@ - The MIT License (MIT) Copyright (c) 2023 Péter Kardos diff --git a/README.md b/README.md index 36b49c3..1ed1ad9 100644 --- a/README.md +++ b/README.md @@ -1 +1,236 @@ # async++ + +![Language](https://img.shields.io/badge/Language-C++20-blue) +[![License](https://img.shields.io/badge/License-MIT-blue)](#license) +[![Build & test](https://github.com/petiaccja/asyncpp/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/petiaccja/asyncpp/actions/workflows/build_and_test.yml) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=petiaccja_asyncpp&metric=alert_status)](https://sonarcloud.io/dashboard?id=petiaccja_asyncpp) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=petiaccja_asyncpp&metric=coverage)](https://sonarcloud.io/dashboard?id=petiaccja_asyncpp) + +async++ is a C++20 coroutine library that provides primitives to write async and parallel code. + + +## Usage + +In this section: +- [Coroutine primitives](#coroutine_primitives) +- [Synchronization primitives](#sync_primitives) +- [Schedulers](#usage_schedulers) +- [Schedulers](#sync_join) +- [Interaction with other coroutine libraries](#extending_asyncpp) + +### Coroutine primitives + +asyncpp implements the following coroutine primitives: +- coroutine primitives: + - [task\](#usage_task) + - [shared_task\](#usage_shared_task) + - [generator\](#usage_generator) + - [stream\](#usage_stream) + + +#### Using `task` + +`task` works similarly to `std::future`: +- It returns one value +- Cannot be copied +- Can only be awaited once + +Defining a coroutine: +```c++ +task my_coro() { + co_return 0; +} +``` + +Retrieving the result: +```c++ +auto future_value = my_coro(); +int value = co_await future_value; +``` + + +#### Using `shared_task` + + +`task` works similarly to `std::shared_future`: +- It returns one value +- Can be copied +- Can be awaited any number of times from the same or different threads + +The interface is the same as `task`. + + +#### Using `generator` + +Generators allow you to write a coroutine that generates a sequence of values: +```c++ +generator my_sequence(int count) { + for (int i=0; iUsing `stream` + +Streams work similarly to generators, but they may run asynchronously so you have to await the results. + +Defining a stream: +```c++ +stream my_sequence(int count) { + for (int i=0; iSynchronization primitives + +asyncpp implements the following synchronization primitives: +- synchronization primitives: + - [mutex](#usage_mutex) + - [shared_mutex](#usage_shared_mutex) + - [unique_lock](#usage_unique_lock) + - [shared_lock](#usage_shared_lock) + + +#### Using `mutex` + +Mutexes have a similar interface to `std::mutex`, except they don't have a `lock` method. Instead, you have to `co_await` them: + +```c++ +mutex mtx; +const bool locked = mtx.try_lock(); +if (!locked) { + co_await mtx; // Same as co_await mtx.unique() +} +mtx.unlock(); +``` + + +#### Using `shared_mutex` + +Mutexes have a similar interface to `std::shared_mutex`, except they don't have a `lock` method. Instead, you have to `co_await` them: + +```c++ +mutex mtx; +const bool locked = mtx.try_lock_shared(); +if (!locked) { + co_await mtx.shared(); +} +mtx.unlock_shared(); +``` + + +#### Using `unique_lock` + +Similar to `std::unique_lock`, but adapter to dealing with coroutines. + +```c++ +mutex mtx; +unique_lock lk(mtx); // Does NOT lock the mutex +unique_lock lk(co_await mtx); // Does lock the mutex +lk.unlock(); // Unlocks the mutex +lk.try_lock(); // Tries to lock the mutex +co_await lk; // Lock the mutex +``` + +#### Using `shared_lock` + +Works similarly to `unique_lock`, but locks the mutex in shared mode. + + +### Using schedulers + +Coroutines (except generator) can be made to run by a scheduler using `bind`: + +```c++ +thread_pool sched; +task my_task = my_coro(); +bind(my_task, sched); +``` + +When a coroutine is bound to a scheduler, it will always run on that scheduler, no matter if it's resumed from another scheduler. This allows you to create schedulers whose threads have a specific affinity or priority, and you can be sure your tasks run only on those threads. A task that is not bound will run synchronously. + + +Some coroutines are only started when you `co_await` them, however, other can be launched asynchronously before retrieving the results. This is done using `launch`: + +```c++ +task my_task = my_coro(); +// Launching on whatever scheduler it's bound to: +launch(my_task); +// Launching on a specific scheduler: +thread_pool sched; +launch(my_task, sched); +``` + +Launching coroutines before `co_await`-ing them is useful to create actual parallelism. This way, you can make callees execute on a thread pool in parallel to each other and the caller, and only then get their results. Simply using `co_await` will respect the bound scheduler of the callee, but it cannot introduce parallelism as there is only ever on task you can await. + +There is a shorthand forwarding version of both functions: + +```c++ +task my_task = bind(my_coro(), sched); +task my_task = launch(my_coro(), sched); +``` + + +### Synchronizing coroutines and functions + +Normally, coroutines can only be `co_await`-ed. Since you cannot `co_await` in regular functions, you have to use `join` to retrieve the results of a coroutine: + +```c++ +task my_task = launch(my_coro(), sched); +int result = join(my_task); +``` + +Note that this can also be used for any other primitives: + +```c++ +mutex mtx; +join(mtx); // Acquires the lock +mtx.unlock(); +``` + +In this case, `mutex` works just like `std::mutex`, but you lose all the advantages of coroutines. + + +### Interfacing with other coroutine libraries + +#### Existing libraries + +asyncpp's coroutines can `co_await` other coroutines, however, the scheduling will be partially taken over by the other library, meaning asyncpp's coroutines won't anymore respect their bound schedulers. + +Other coroutines cannot currently co_await asyncpp's coroutines. This is because asyncpp coroutines expect awaiting coroutines to have a `resumable_promise`. This restriction could be potentially lifted, however, other coroutines would be treated by asyncpp as having no bound scheduler and run synchronously. + +#### Building on top of asyncpp + +All asyncpp coroutines have a promise type that derives from `resumable_promise`. This way, whenever a coroutine is resumed, the exact way to resume it is delegated to the coroutine's promise rather than executed by the caller. This allows coroutines to retain their agency over when, where and how they run, and stay on the schedulers they have been bound to. + +To extend asyncpp, all you need to do is make your promises implement `resumable_promise`. If you want your coroutines to be schedulable, you have to implement `schedulable_promise`. Such coroutines should seamlessly fit into asyncpp. + + +## License + +asyncpp is distributed under the MIT license, therefore can be used in commercial and non-commercial projects alike with very few restrictions. + + + + diff --git a/include/async++/lock.hpp b/include/async++/lock.hpp index 1e0dab3..86e2815 100644 --- a/include/async++/lock.hpp +++ b/include/async++/lock.hpp @@ -71,6 +71,11 @@ class unique_lock { public: unique_lock(Mutex& mtx) noexcept : m_mtx(&mtx) {} unique_lock(mutex_lock&& lk) noexcept : m_mtx(&lk.mutex()), m_owned(true) {} + ~unique_lock() { + if (owns_lock()) { + m_mtx->unlock(); + } + } bool try_lock() noexcept { assert(!owns_lock()); @@ -136,6 +141,11 @@ class shared_lock { public: shared_lock(Mutex& mtx) noexcept : m_mtx(&mtx) {} shared_lock(mutex_shared_lock lk) noexcept : m_mtx(&lk.mutex()), m_owned(true) {} + ~shared_lock() { + if (owns_lock()) { + m_mtx->unlock_shared(); + } + } bool try_lock() noexcept { assert(!owns_lock()); diff --git a/include/async++/scheduler.hpp b/include/async++/scheduler.hpp index b52ddad..1e428ba 100644 --- a/include/async++/scheduler.hpp +++ b/include/async++/scheduler.hpp @@ -12,37 +12,27 @@ class scheduler { }; -template - requires requires(T& t, scheduler& s) { t.bind(s); } -T& bind(T& t, scheduler& s) { - t.bind(s); - return t; -} - - template requires requires(T&& t, scheduler& s) { t.bind(s); } -T bind(T&& t, scheduler& s) { +auto bind(T&& t, scheduler& s) -> decltype(auto) { t.bind(s); - return std::move(t); + return std::forward(t); } template - requires requires(T& t, scheduler& s) { bind(t, s); t.launch(); } -T& launch(T& t, scheduler& s) { - bind(t, s); + requires requires(T&& t) { t.launch(); } +auto launch(T&& t) -> decltype(auto) { t.launch(); - return t; + return std::forward(t); } template - requires requires(T&& t, scheduler& s) { bind(t, s); t.launch(); } -T launch(T&& t, scheduler& s) { + requires requires(T&& t, scheduler& s) { bind(t, s); launch(t); } +auto launch(T&& t, scheduler& s) -> decltype(auto) { bind(t, s); - t.launch(); - return std::move(t); + return launch(std::forward(t)); } } // namespace asyncpp \ No newline at end of file diff --git a/test/test_mutex.cpp b/test/test_mutex.cpp index 9879b5c..834882d 100644 --- a/test/test_mutex.cpp +++ b/test/test_mutex.cpp @@ -96,6 +96,21 @@ TEST_CASE("Mutex: unique lock unlock", "[Mutex]") { } +TEST_CASE("Mutex: unique lock destroy", "[Shared mutex]") { + static const auto coro = [](mutex& mtx) -> task { + { + unique_lock lk(co_await mtx); + REQUIRE(lk.owns_lock()); + } + REQUIRE(mtx.try_lock()); + co_return; + }; + + mutex mtx; + join(coro(mtx)); +} + + TEST_CASE("Mutex: resume awaiting", "[Mutex]") { static const auto awaiter = [](mutex& mtx, std::vector& sequence, int id) -> task { co_await mtx; diff --git a/test/test_shared_mutex.cpp b/test/test_shared_mutex.cpp index 91ed1e5..05794b1 100644 --- a/test/test_shared_mutex.cpp +++ b/test/test_shared_mutex.cpp @@ -193,6 +193,21 @@ TEST_CASE("Shared mutex: shared lock unlock", "[Shared mutex]") { } +TEST_CASE("Shared mutex: shared lock destroy", "[Shared mutex]") { + static const auto coro = [](shared_mutex& mtx) -> task { + { + shared_lock lk(co_await mtx.shared()); + REQUIRE(lk.owns_lock()); + } + REQUIRE(mtx.try_lock()); + co_return; + }; + + shared_mutex mtx; + join(coro(mtx)); +} + + TEST_CASE("Shared mutex: resume awaiting", "[Shared mutex]") { static const auto awaiter = [](shared_mutex& mtx, std::vector& sequence, int id) -> task { co_await mtx.unique(); From 23391a8104e77adb67547917cec26b5dae7a211f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Kardos?= Date: Mon, 20 Nov 2023 10:42:25 +0100 Subject: [PATCH 2/3] fix code quality issues --- include/async++/concepts.hpp | 11 +++++++++++ include/async++/lock.hpp | 30 +++++++++++++++++++++++++++++- include/async++/scheduler.hpp | 9 ++++----- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/include/async++/concepts.hpp b/include/async++/concepts.hpp index 2a3f03b..a3fc808 100644 --- a/include/async++/concepts.hpp +++ b/include/async++/concepts.hpp @@ -5,6 +5,8 @@ namespace asyncpp { +class scheduler; + // clang-format off template @@ -23,6 +25,15 @@ concept indirectly_awaitable = requires(std::remove_reference_t& t) { template concept awaitable = directly_awaitable || indirectly_awaitable; + +template +concept launchable_coroutine = requires(T&& t) { t.launch(); }; + + +template +concept bindable_coroutine = requires(T&& t, scheduler& s) { t.bind(s); }; + + // clang-format on template diff --git a/include/async++/lock.hpp b/include/async++/lock.hpp index 86e2815..5de7f5d 100644 --- a/include/async++/lock.hpp +++ b/include/async++/lock.hpp @@ -71,6 +71,20 @@ class unique_lock { public: unique_lock(Mutex& mtx) noexcept : m_mtx(&mtx) {} unique_lock(mutex_lock&& lk) noexcept : m_mtx(&lk.mutex()), m_owned(true) {} + unique_lock(unique_lock&& rhs) noexcept : m_mtx(rhs.m_mtx), m_owned(rhs.m_owned) { + rhs.m_mtx = nullptr; + rhs.m_owned = false; + } + unique_lock& operator=(unique_lock&& rhs) noexcept { + if (owns_lock()) { + m_mtx->unlock(); + } + m_mtx = std::exchange(rhs.m_mtx, nullptr); + m_owned = std::exchange(rhs.m_owned, false); + return *this; + } + unique_lock(const unique_lock& rhs) = delete; + unique_lock& operator=(const unique_lock& rhs) = delete; ~unique_lock() { if (owns_lock()) { m_mtx->unlock(); @@ -107,7 +121,7 @@ class unique_lock { } private: - Mutex* m_mtx; + Mutex* m_mtx = nullptr; bool m_owned = false; }; @@ -141,6 +155,20 @@ class shared_lock { public: shared_lock(Mutex& mtx) noexcept : m_mtx(&mtx) {} shared_lock(mutex_shared_lock lk) noexcept : m_mtx(&lk.mutex()), m_owned(true) {} + shared_lock(shared_lock&& rhs) noexcept : m_mtx(rhs.m_mtx), m_owned(rhs.m_owned) { + rhs.m_mtx = nullptr; + rhs.m_owned = false; + } + shared_lock& operator=(shared_lock&& rhs) noexcept { + if (owns_lock()) { + m_mtx->unlock_shared(); + } + m_mtx = std::exchange(rhs.m_mtx, nullptr); + m_owned = std::exchange(rhs.m_owned, false); + return *this; + } + shared_lock(const shared_lock& rhs) = delete; + shared_lock& operator=(const shared_lock& rhs) = delete; ~shared_lock() { if (owns_lock()) { m_mtx->unlock_shared(); diff --git a/include/async++/scheduler.hpp b/include/async++/scheduler.hpp index 1e428ba..c729b56 100644 --- a/include/async++/scheduler.hpp +++ b/include/async++/scheduler.hpp @@ -1,5 +1,6 @@ #pragma once +#include "concepts.hpp" #include "promise.hpp" @@ -12,16 +13,14 @@ class scheduler { }; -template - requires requires(T&& t, scheduler& s) { t.bind(s); } +template auto bind(T&& t, scheduler& s) -> decltype(auto) { t.bind(s); return std::forward(t); } -template - requires requires(T&& t) { t.launch(); } +template auto launch(T&& t) -> decltype(auto) { t.launch(); return std::forward(t); @@ -29,7 +28,7 @@ auto launch(T&& t) -> decltype(auto) { template - requires requires(T&& t, scheduler& s) { bind(t, s); launch(t); } + requires(bindable_coroutine && launchable_coroutine) auto launch(T&& t, scheduler& s) -> decltype(auto) { bind(t, s); return launch(std::forward(t)); From dbaa01b2a5813c01725bc5fa999714a3e71bf8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Kardos?= Date: Mon, 20 Nov 2023 10:49:58 +0100 Subject: [PATCH 3/3] fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ed1ad9..3b41bac 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ In this section: - [Coroutine primitives](#coroutine_primitives) - [Synchronization primitives](#sync_primitives) - [Schedulers](#usage_schedulers) -- [Schedulers](#sync_join) +- [Synchronizing coroutines and functions](#sync_join) - [Interaction with other coroutine libraries](#extending_asyncpp) ### Coroutine primitives