1
0
Fork 0
mirror of https://gitlab.com/niansa/libcrosscoro.git synced 2025-03-06 20:53:32 +01:00

Add coro::task<T> example (#48)

* Add coro::task<T> example

* remove std::move on return statement in task example
This commit is contained in:
Josh Baldwin 2021-01-31 13:56:48 -07:00 committed by GitHub
parent cb335b4474
commit 5ad45c3848
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 212 additions and 14 deletions

View file

@ -34,6 +34,14 @@ done
# template_contents=$(cat '.githooks/readme-template.md')
cp .githooks/readme-template.md README.md
template_contents=$(cat 'README.md')
example_contents=$(cat 'examples/coro_task.cpp')
echo "${template_contents/\$\{EXAMPLE_CORO_TASK_CPP\}/$example_contents}" > README.md
template_contents=$(cat 'README.md')
example_contents=$(cat 'examples/coro_generator.cpp')
echo "${template_contents/\$\{EXAMPLE_CORO_GENERATOR_CPP\}/$example_contents}" > README.md
template_contents=$(cat 'README.md')
example_contents=$(cat 'examples/coro_event.cpp')
echo "${template_contents/\$\{EXAMPLE_CORO_EVENT_CPP\}/$example_contents}" > README.md
@ -46,8 +54,4 @@ template_contents=$(cat 'README.md')
example_contents=$(cat 'examples/coro_mutex.cpp')
echo "${template_contents/\$\{EXAMPLE_CORO_MUTEX_CPP\}/$example_contents}" > README.md
template_contents=$(cat 'README.md')
example_contents=$(cat 'examples/coro_generator.cpp')
echo "${template_contents/\$\{EXAMPLE_CORO_GENERATOR_CPP\}/$example_contents}" > README.md
git add README.md

View file

@ -20,21 +20,40 @@
- coro::latch
- coro::mutex
- coro::sync_wait(awaitable)
- coro::when_all(awaitable...) -> coro::task<T>...
- coro::when_all_results(awaitable...) -> T... (Future)
- coro::when_all(awaitable...) -> awaitable
* Schedulers
- coro::thread_pool for coroutine cooperative multitasking
- coro::io_scheduler for driving i/o events, uses thread_pool for coroutine execution
- coro::io_scheduler for driving i/o events, uses thread_pool for coroutine execution upon triggered events
- epoll driver
- io_uring driver (Future, will be required for async file i/o)
* Coroutine Networking
- coro::net::dns_resolver for async dns, leverages libc-ares
- coro::net::tcp_client and coro::net::tcp_server
- coro::net::dns_resolver for async dns
- Uses libc-ares
- coro::net::tcp_client
- coro::net::tcp_server
- coro::net::udp_peer
### A note on co_await
Its important to note with coroutines that depending on the construct used _any_ `co_await` has the potential to switch the thread that is executing the currently running coroutine. In general this shouldn't affect the way any user of the library would write code except for `thread_local`. Usage of `thread_local` should be extremely careful and _never_ used across any `co_await` boundary do to thread switching and work stealing on thread pools.
### coro::task<T>
The `coro::task<T>` is the main coroutine building block within `libcoro`. Use task to create your coroutines and `co_await` or `co_yield` tasks within tasks to perform asynchronous operations, lazily evaluation or even spreading work out across a `coro::thread_pool`. Tasks are lightweight and only begin execution upon awaiting them. If their return type is not `void` then the value can be returned by const reference or by moving (r-value reference).
```C++
${EXAMPLE_CORO_TASK_CPP}
```
Expected output:
```bash
$ ./examples/coro_task
Task1 output = 9
expensive_struct() move constructor called
expensive_struct() move assignment called
expensive_struct() move constructor called
12345678-1234-5678-9012-123456781234 has 90000 records.
Answer to everything = 42
```
### coro::generator<T>
The `coro::generator<T>` construct is a coroutine which can generate one or more values.

104
README.md
View file

@ -20,21 +20,115 @@
- coro::latch
- coro::mutex
- coro::sync_wait(awaitable)
- coro::when_all(awaitable...) -> coro::task<T>...
- coro::when_all_results(awaitable...) -> T... (Future)
- coro::when_all(awaitable...) -> awaitable
* Schedulers
- coro::thread_pool for coroutine cooperative multitasking
- coro::io_scheduler for driving i/o events, uses thread_pool for coroutine execution
- coro::io_scheduler for driving i/o events, uses thread_pool for coroutine execution upon triggered events
- epoll driver
- io_uring driver (Future, will be required for async file i/o)
* Coroutine Networking
- coro::net::dns_resolver for async dns, leverages libc-ares
- coro::net::tcp_client and coro::net::tcp_server
- coro::net::dns_resolver for async dns
- Uses libc-ares
- coro::net::tcp_client
- coro::net::tcp_server
- coro::net::udp_peer
### A note on co_await
Its important to note with coroutines that depending on the construct used _any_ `co_await` has the potential to switch the thread that is executing the currently running coroutine. In general this shouldn't affect the way any user of the library would write code except for `thread_local`. Usage of `thread_local` should be extremely careful and _never_ used across any `co_await` boundary do to thread switching and work stealing on thread pools.
### coro::task<T>
The `coro::task<T>` is the main coroutine building block within `libcoro`. Use task to create your coroutines and `co_await` or `co_yield` tasks within tasks to perform asynchronous operations, lazily evaluation or even spreading work out across a `coro::thread_pool`. Tasks are lightweight and only begin execution upon awaiting them. If their return type is not `void` then the value can be returned by const reference or by moving (r-value reference).
```C++
#include <coro/coro.hpp>
#include <iostream>
int main()
{
// Task that takes a value and doubles it.
auto double_task = [](uint64_t x) -> coro::task<uint64_t> { co_return x* x; };
// Create a task that awaits the doubling of its given value and
// then returns the result after adding 5.
auto double_and_add_5_task = [&](uint64_t input) -> coro::task<uint64_t> {
auto doubled = co_await double_task(input);
co_return doubled + 5;
};
auto output = coro::sync_wait(double_and_add_5_task(2));
std::cout << "Task1 output = " << output << "\n";
struct expensive_struct
{
std::string id{};
std::vector<std::string> records{};
expensive_struct() = default;
~expensive_struct() = default;
// Explicitly delete copy constructor and copy assign, force only moves!
// While the default move constructors will work for this struct the example
// inserts explicit print statements to show the task is moving the value
// out correctly.
expensive_struct(const expensive_struct&) = delete;
auto operator=(const expensive_struct&) -> expensive_struct& = delete;
expensive_struct(expensive_struct&& other) : id(std::move(other.id)), records(std::move(other.records))
{
std::cout << "expensive_struct() move constructor called\n";
}
auto operator=(expensive_struct&& other) -> expensive_struct&
{
if (std::addressof(other) != this)
{
id = std::move(other.id);
records = std::move(other.records);
}
std::cout << "expensive_struct() move assignment called\n";
return *this;
}
};
// Create a very large object and return it by moving the value so the
// contents do not have to be copied out.
auto move_output_task = []() -> coro::task<expensive_struct> {
expensive_struct data{};
data.id = "12345678-1234-5678-9012-123456781234";
for (size_t i = 10'000; i < 100'000; ++i)
{
data.records.emplace_back(std::to_string(i));
}
// Because the struct only has move contructors it will be forced to use
// them, no need to explicitly std::move(data).
co_return data;
};
auto data = coro::sync_wait(move_output_task());
std::cout << data.id << " has " << data.records.size() << " records.\n";
// std::unique_ptr<T> can also be used to return a larger object.
auto unique_ptr_task = []() -> coro::task<std::unique_ptr<uint64_t>> { co_return std::make_unique<uint64_t>(42); };
auto answer_to_everything = coro::sync_wait(unique_ptr_task());
if (answer_to_everything != nullptr)
{
std::cout << "Answer to everything = " << *answer_to_everything << "\n";
}
}
```
Expected output:
```bash
$ ./examples/coro_task
Task1 output = 9
expensive_struct() move constructor called
expensive_struct() move assignment called
expensive_struct() move constructor called
12345678-1234-5678-9012-123456781234 has 90000 records.
Answer to everything = 42
```
### coro::generator<T>
The `coro::generator<T>` construct is a coroutine which can generate one or more values.

View file

@ -1,6 +1,10 @@
cmake_minimum_required(VERSION 3.0)
project(libcoro_examples)
add_executable(coro_task coro_task.cpp)
target_compile_features(coro_task PUBLIC cxx_std_20)
target_link_libraries(coro_task PUBLIC libcoro)
add_executable(coro_generator coro_generator.cpp)
target_compile_features(coro_generator PUBLIC cxx_std_20)
target_link_libraries(coro_generator PUBLIC libcoro)
@ -18,6 +22,7 @@ target_compile_features(coro_mutex PUBLIC cxx_std_20)
target_link_libraries(coro_mutex PUBLIC libcoro)
if(${CMAKE_CXX_COMPILER_ID} MATCHES "GNU")
target_compile_options(coro_task PUBLIC -fcoroutines -Wall -Wextra -pipe)
target_compile_options(coro_generator PUBLIC -fcoroutines -Wall -Wextra -pipe)
target_compile_options(coro_event PUBLIC -fcoroutines -Wall -Wextra -pipe)
target_compile_options(coro_latch PUBLIC -fcoroutines -Wall -Wextra -pipe)

76
examples/coro_task.cpp Normal file
View file

@ -0,0 +1,76 @@
#include <coro/coro.hpp>
#include <iostream>
int main()
{
// Task that takes a value and doubles it.
auto double_task = [](uint64_t x) -> coro::task<uint64_t> { co_return x* x; };
// Create a task that awaits the doubling of its given value and
// then returns the result after adding 5.
auto double_and_add_5_task = [&](uint64_t input) -> coro::task<uint64_t> {
auto doubled = co_await double_task(input);
co_return doubled + 5;
};
auto output = coro::sync_wait(double_and_add_5_task(2));
std::cout << "Task1 output = " << output << "\n";
struct expensive_struct
{
std::string id{};
std::vector<std::string> records{};
expensive_struct() = default;
~expensive_struct() = default;
// Explicitly delete copy constructor and copy assign, force only moves!
// While the default move constructors will work for this struct the example
// inserts explicit print statements to show the task is moving the value
// out correctly.
expensive_struct(const expensive_struct&) = delete;
auto operator=(const expensive_struct&) -> expensive_struct& = delete;
expensive_struct(expensive_struct&& other) : id(std::move(other.id)), records(std::move(other.records))
{
std::cout << "expensive_struct() move constructor called\n";
}
auto operator=(expensive_struct&& other) -> expensive_struct&
{
if (std::addressof(other) != this)
{
id = std::move(other.id);
records = std::move(other.records);
}
std::cout << "expensive_struct() move assignment called\n";
return *this;
}
};
// Create a very large object and return it by moving the value so the
// contents do not have to be copied out.
auto move_output_task = []() -> coro::task<expensive_struct> {
expensive_struct data{};
data.id = "12345678-1234-5678-9012-123456781234";
for (size_t i = 10'000; i < 100'000; ++i)
{
data.records.emplace_back(std::to_string(i));
}
// Because the struct only has move contructors it will be forced to use
// them, no need to explicitly std::move(data).
co_return data;
};
auto data = coro::sync_wait(move_output_task());
std::cout << data.id << " has " << data.records.size() << " records.\n";
// std::unique_ptr<T> can also be used to return a larger object.
auto unique_ptr_task = []() -> coro::task<std::unique_ptr<uint64_t>> { co_return std::make_unique<uint64_t>(42); };
auto answer_to_everything = coro::sync_wait(unique_ptr_task());
if (answer_to_everything != nullptr)
{
std::cout << "Answer to everything = " << *answer_to_everything << "\n";
}
}