How to Set Error_Code to Asio::Yield_Context

How to pass boost::asio::yield_context through my own function?

What do you want to /do/ with the ec? You CAN get access is precisely the same way as your my_coroutine already shows:

void my_wrapper(asio::yield_context&& yield) {
system::error_code ec;
asio::async_read(..., yield[ec]); // any asio async call
if (ec) {
// something went wrong
return;
}
...
}

If you mean you wanted to make an async operation that is composable, see this pattern:

  • How to set error_code to asio::yield_context

Basic example:

template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
#if BOOST_VERSION >= 106600
using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
typename result_type::completion_handler_type handler(std::forward<Token>(token));

result_type result(handler);
#else
typename asio::handler_type<Token, void(error_code, int)>::type
handler(std::forward<Token>(token));

asio::async_result<decltype (handler)> result (handler);
#endif

if (success)
handler(error_code{}, 42);
else
handler(asio::error::operation_aborted, 0);

return result.get ();
}

how spawn and post works with asio?


boost::asio::steady_timer t(svc, boost::asio::chrono::seconds(45));
// this function returns immediately and make new thread

No it doesn't create a new thread. It just constructs a service object (the timer) and returns. obviously immediately, like std::string s("hello"); returns when the string is constructed.

t.async_wait(&print);
// this function returns immediately>>>>also it adds 1 out standing work to
// svc. is async_wait body runned in main threaed OR in another thread????if
// it is ran in another thread,how immediate return happens"not
// blocking"??why async_meaning is not returning immediately like
// async_wait?

Slow down.

is async_wait body runned in main threaed OR in another thread?

It's just a function. It runs on the current thread. Like when you called printf.

if it is ran in another thread, how immediate return happens "not
blocking"?

Well, it's not in another thread. But if it were, then it would be obvious how
it would return "not blocking": because the work is not happening on the
current thread.

Why async_meaning_of_life is not returning immediately like
async_wait?

It is returning immediately.

Now, a bit subtler: Even if you use it with a yield_context (inside a
coroutine). It will return immediately and cause the the coroutine to yield.
This means that other tasks get a chance to run on the service thread(s) and
only when the async operation completed, the coroutine will be resumed. From
the point of view of the coroutine, it will have appeared as if the call was
blocking. This is the whole point of (stackful) coroutines. It "abstracts away"
the asynchrony.

So, yes, async_meaning_of_life always (always) returns (almost) immediately.


svc.post([]() { // this adds 1 outstanding work to svc and does not start

Correct. Use a {poll|run}[_one,_for,_until] function to run tasks².


    auto answer = async_meaning_of_life(true, asio::use_future);
std::cout << __FUNCTION__ << ": Answer: " << answer.get() << "\n";

You don't ask anything here, but using a future just to await it immediately is
an anti-pattern¹. It's absolutely useless, as it will always generate blocking
behaviour.

You should store the future somewhere, do other work and then when you need the result of the future (and it may or may not have already been completed) you await it (.get() You should store the future somewhere, do other work and then when you need the result of the future (and it may or may not have already been completed) you await it (e.g. by invoking .get()).


// using_yield_ec( yield);this is wrong usage
// using_yield_catch( yield);this is wrong usage

Correct. Used correctly, the Asio service will provide a yield context for you.

// boost::asio::yield_context yield;
// 4-18-2020 this is only used with spawn ,if you want to use stakeful
// coroutines,use push and pull types of coroutine "i wonder how to do
// this???"

No idea. Just refer to the documentation of Boost Coroutine (I suggest Boost Coroutine2). This is off-topic for Asio async operations.


// using_future();this is normal usage but it does not return immediately
// and it executes in main thread.

Well, duh. You took it from a minimal example that ONLY shows the mechanics of the different async_result tokens.

Just refer to a few lines above:

You should store the future somewhere, do other work and then when you need
the result of the future (and it may or may not have already been completed)
you await it (.get() You should store the future somewhere, do other work
and then when you need the result of the future (and it may or may not have
already been completed) you await it (e.g. by invoking .get()).


svc.post(using_future_composed);

Again, I see no questions, but I don't think it means you understand it. I tread.

I see using_future_composed is basically using_future but calling async_meaning_of_life_composed instead.

Now looking at async_meaning_of_life_composed I have no idea what that's supposed to do. It looks like async_meaning_of_life with random lines of code added, doing all kinds of things including blocking operations (see anti-pattern¹) in a function that is supposed to schedule a async operation only.

That's just not what you want to do. Ever.


spawn(svc, using_yield_ec);
// this adds 2 outstanding work to svc why 2 works are made while we are
// launching one function????

Honestly, I do not know. I assume it's because the launch of the coro itself is posted onto the work queue, so it runs exception-safely from one of the worker threads.

The bigger point here is that you still haven't actually started any io-workers, see [²] above.


spawn(svc, using_yield_catch);
// what i think i understand about mechanism of work of spawn:spawn is
// called from main thread>>>>it is just used with coroutines taking
// yield_context as argument,spawn post function to service,spawn makes link
// between the context in which service will be ran"may be main thread or
// new thread AND the context of coroutine function ran in same thread as
// service"...

Erm, basically, yes.

//          ... or may be the coroutine makes new thread in which it is
// running???" ...

Definitely not. Both Coroutines and Asio are a device/framework to arrive at concurrency without necessarily multi-threading. Coroutine will never create a thread. Asio will typically not create any threads (unless to implement certain kinds of services on some platforms, but they'd be implementation-details and your tasks/handlers will never run on such a hidden thread).

//         ... .Then when svc.run is called,svc calls task"here svc is caller
// and coroutine is callee",task is executing,yield is called as completion
// token"can we call yield outside initiating function to switch to caller
// "here caller is svc"????. then we are now in svc context which calls
// another task .....

Huh. No, yield_context is not a portal to a different time-space continuum.

I'm not very sure what you mean with `'call yield' so when you are thinking about calling it from outside the initiating function, I'd say: probably don't do that.


// t.async_wait(&using_future);wrong usage leading to error?why can not in
// use using_future function as completion callback with async_wait???

Because it doesn't satisfy the handler requirements for steady_time::async_wait (which should take a boost::system::error_code only. Did you perhaps mean use_future (from Asio) instead of your own using_future?

auto ignored_future = t.async_wait(boost::asio::use_future);

I admit the names are somewhat confusing. If it helps, rename all the using_XYZ functions to demonstration_using_XYZ.


// spawn(svc, using_future);wrong usage as using_future is not coroutine?

You got that right.


std::thread work([] 
using_future();
using_handler();
auto answer = async_meaning_of_life(true, asio::use_future);
// this function does not return immediately and is executing in main
// thread >>>>>how can we make it behave like async_wait???? first

std::cout << __FUNCTION__ << ": Answer: " << answer.get() << "\n";

});

I believe you just copy/pasted the comment, but in case you really worried: no that is not run on the main thread. It's run on the work thread, and yes, that's because you block on the future::get(). See above¹.


std::thread work_io([&] { // this starts new thread in which svc is run
svc.run();
});

Better late than never :)

svc.run(); // this run svc in main thread

Correct, and running more doesn't hurt. Running the service on multiple threads may require handler synchronization: Why do I need strand per connection when using boost::asio?


// general question:
/*
using_* is considered normal function or coroutine OR composed operation??

Normal functions (see the clarification about renaming it to demonstration_using_XYZ above)

async_meaning is considered initiating function?

Correct.

why does not it return immediately when ran in main thread? 

It does. See above. If you mean, why does your own function async_meaning_of_life_composed bblock? That's because you made it do blocking operations (see above).

how can we make
it return immediately and then when certain result is present ,it calls its
callback??

The usual way to do to it, is by launching other async operations. Say, for example, you wait for network operation to complete (asynchronously, e.g. using boost::asio::async_write) and when it's done, you invoke the handler. The async_result helper makes it so you don't have to know the actual completion_handler_type, and it will "magically" do the right thing regardless of how your initiating function was invoked.

async_wait is considered initiating function? why does it return
immediately then when timer expires ,it calls back its completion token??

Because that's how async operations are designed. They were designed that way because that is useful behaviour.

can i make the following composed operation:

i will make composed operation which returns future to caller thread,

and inside it i shall call another composed operation with coroutine,
*/

You are free to start a coroutine. Just make sure you transfer ownership of the async_result result so you can invoke the handler from there, to signal completion of your operation.

In the case of futures, the usual way to compose operations is by composing futures, like: https://www.boost.org/doc/libs/1_72_0/doc/html/thread/synchronization.html#thread.synchronization.futures.then

std::string someotheroperation(int);

future<int> fut1 = foo();
future<std::string> fut2 = foo().then(someotheroperation);

BONUS

The ultimate piece of documentation on writing Composed Operations with Asio is (ironically) this page in the Beast documentation. Perhaps seeing some more real-life examples may give you more ideas.

Keep in mind Beast comes with a few facilities that make library maintenance for /them/ a bit easier, but could well be overkill for your own application. Then again, if you err on theur path you will not overlook important things like the one we discussed here earlier:

Sample Image

How should I pass a boost::asio::yield_context in my own functions?

It makes a lot of sense that the context has a lifetime that corresponds to that of the coroutine itself.

This means, I anticipate it containing a pointer (or reference_wrapper) to the actual (hidden) implementation state.

That said, simply do as the Boost library itself does, which is to take the yield_context object by value.

Can a boost::asio::yield_context be used as a deadline_timer handler when doing cancel?

The async_wait() operation is being cancelled, resulting in the asynchronous operation failing with an error code of boost::asio::error::operation_aborted. As noted in the Stackful Coroutines documentation, when the boost::asio::yield_context detects that the asynchronous operation has failed, it converts the boost::system::error_code into a system_error exception and throws. Within the coroutine, consider either:

  • Initiating the asynchronous operation with a handler of context[error_code], causing the yield_context to populate the provided boost::system::error_code on failure rather than throwing.

    boost::system::error_code error;
    timer_.async_wait(context[error]); // On failure, populate error.
  • Catch the system_error and suppress it.


On failure Boost.Asio will populate a boost::system::error_code if the application is capable of receiving it, otherwise it will throw an exception. This pattern can be observed throughout Boost.Asio:

  • All asynchronous operation handler's accept an lvalue const boost::system::error_code as their first parameter. Hence, the initiating function should not throw, as the application will be informed of the error within the handler. This is not always apparent when using functors that discards extra arguments, such as boost::bind.
  • Synchronous operations are overloaded to support throwing and non-throwing versions. For example, timer.cancel() will throw on failure, where as timer.cancel(boost::system::error_code&) will set the error_code to indicate the error.
  • As noted above, when an asynchronous operation fails within a stackful coroutine and the yield_context handler is not provided a boost::system::error_code, then a system_error exception will be thrown.
  • When using futures, if the asynchronous operation fails, then the error_code is converted into a system_error exception and passed back to the caller through the future.

Here is a complete minimal example based on the original problem that runs to completion.

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>

int main()
{
boost::asio::io_service io_service;
boost::asio::deadline_timer timer(io_service);
timer.expires_from_now(boost::posix_time::pos_infin);

boost::asio::spawn(io_service,
[&](boost::asio::yield_context yield)
{
// As only one thread is processing the io_service, the posted
// timer cancel will only be invoked once the coroutine yields.
io_service.post([&](){ timer.cancel(); });

// Initiate an asynchronous operation, suspending the current coroutine,
// and allowing the io_service to process other work (i.e. cancel the
// timer). When the timer is cancelled, the asynchronous operation is
// completed with an error, causing the coroutine to resume. As an
// error_code is provided, the operation will not throw on failure.
boost::system::error_code error;
timer.async_wait(yield[error]);
assert(error == boost::asio::error::operation_aborted);
});

io_service.run();
}

asio use_future instead of yield[ec]

If you can change your implementation, use the async_result pattern.

This makes it so you can use your method with any of the approaches (completion handler, yield context or use_future).

I reproduce the self-contained example from here for inspiration:

Comprehensive Demo

Showing how to use it with with

  • coro's and yield[ec]
  • coro's and yield + exceptions
  • std::future
  • completion handlers

Live On Coliru

#define BOOST_COROUTINES_NO_DEPRECATION_WARNING 
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/use_future.hpp>

using boost::system::error_code;
namespace asio = boost::asio;

template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
#if BOOST_VERSION >= 106600
using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
typename result_type::completion_handler_type handler(std::forward<Token>(token));

result_type result(handler);
#else
typename asio::handler_type<Token, void(error_code, int)>::type
handler(std::forward<Token>(token));

asio::async_result<decltype (handler)> result (handler);
#endif

if (success)
handler(error_code{}, 42);
else
handler(asio::error::operation_aborted, 0);

return result.get ();
}

void using_yield_ec(asio::yield_context yield) {
for (bool success : { true, false }) {
boost::system::error_code ec;
auto answer = async_meaning_of_life(success, yield[ec]);
std::cout << __FUNCTION__ << ": Result: " << ec.message() << "\n";
std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
}
}

void using_yield_catch(asio::yield_context yield) {
for (bool success : { true, false })
try {
auto answer = async_meaning_of_life(success, yield);
std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
} catch(boost::system::system_error const& e) {
std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
}
}

void using_future() {
for (bool success : { true, false })
try {
auto answer = async_meaning_of_life(success, asio::use_future);
std::cout << __FUNCTION__ << ": Answer: " << answer.get() << "\n";
} catch(boost::system::system_error const& e) {
std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
}
}

void using_handler() {
for (bool success : { true, false })
async_meaning_of_life(success, [](error_code ec, int answer) {
std::cout << "using_handler: Result: " << ec.message() << "\n";
std::cout << "using_handler: Answer: " << answer << "\n";
});
}

int main() {
asio::io_service svc;

spawn(svc, using_yield_ec);
spawn(svc, using_yield_catch);
std::thread work([] {
using_future();
using_handler();
});

svc.run();
work.join();
}

Prints:

using_yield_ec: Result: Success
using_yield_ec: Answer: 42
using_yield_ec: Result: Operation canceled
using_yield_ec: Answer: 0
using_future: Answer: 42
using_yield_catch: Answer: 42
using_yield_catch: Caught: Operation canceled
using_future: Caught: Operation canceled
using_handler: Result: Success
using_handler: Answer: 42
using_handler: Result: Operation canceled
using_handler: Answer: 0

Note: for simplicity I have not added output synchronization, so the output can become intermingled depending on runtime execution order

Exception throw in boost::asio::spawn not caught by try catch

All handlers are invoked by the service loop, meaning you always need to handle errors: Should the exception thrown by boost::asio::io_service::run() be caught?

Sidenote: In the case of coroutines, it's my experience that catching by reference is a bit tricky. This more than likely has to do with the lifetime of the coroutine stack itself.

You can demonstrate it using:

while (true) {
try {
loop.run();
break;
} catch(std::runtime_error ex) {
std::cerr << "L:" << __LINE__ << ": " << ex.what() << "\n";
}
}

Other Notes

Note that passing error_code errors can be done in two ways across idiomatic Asio interfaces, including Coroutines: How to set error_code to asio::yield_context

Full Sample

Simply adapting your sample to be selfcontained:

Live On Coliru

#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/high_resolution_timer.hpp>

using namespace std::chrono_literals;

int main() {
boost::asio::io_service loop;

spawn(loop, [&loop](boost::asio::yield_context yield)
{
try
{
spawn(yield, [&loop](boost::asio::yield_context yield)
{
boost::asio::high_resolution_timer timer{loop};
for(unsigned i = 0; i < 3; ++i)
{
std::cout << i * 2 + 1 << std::endl;
timer.expires_from_now(100ms);
timer.async_wait(yield);
}
throw std::system_error(ENOENT, std::system_category(), "Throw an error");
//throw boost::system::system_error(ENOENT, boost::system::system_category(), "Throw an error");
});

spawn(yield, [&loop](boost::asio::yield_context yield)
{
boost::asio::high_resolution_timer timer{loop};
for(unsigned i = 0; i < 3; ++i)
{
std::cout << (i + 1) * 2 << std::endl;
timer.expires_from_now(100ms);
timer.async_wait(yield);
}
});
} catch(const std::runtime_error& ex)
{
std::cerr << "L:" << __LINE__ << ": " << ex.what() << "\n";
}
});

while (true) {
try {
loop.run();
break;
} catch(std::runtime_error ex) {
std::cerr << "L:" << __LINE__ << ": " << ex.what() << "\n";
}
}
}

Prints

1
2
3
4
5
6
L:49: Throw an error: No such file or directory

BONUS:

Doing the extra approach with generalized competion token and async_result:

Live On Coliru

#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/high_resolution_timer.hpp>

using namespace std::chrono_literals;

boost::asio::io_service loop;

template <typename Token>
auto async_foo(bool success, Token&& token)
{
typename boost::asio::handler_type<Token, void(boost::system::error_code, int)>::type
handler (std::forward<Token> (token));

boost::asio::async_result<decltype (handler)> result (handler);

boost::asio::yield_context yield(token);

boost::asio::high_resolution_timer timer{loop};
for(unsigned i = 0; i < 3; ++i) {
std::cout << (i * 2 + (success?0:1)) << std::endl;
timer.expires_from_now(100ms);
timer.async_wait(yield);
}

if (success)
handler(42);
else
throw boost::system::system_error(ENOENT, boost::system::system_category(), "Throw an error");

return result.get();
}

int main() {

auto spawn_foo = [](bool success) {
spawn(loop, [=](boost::asio::yield_context yield) {
try
{
int answer = async_foo(success, yield);
std::cout << "async_foo returned " << answer << std::endl;
} catch(const std::runtime_error& ex)
{
std::cerr << "L:" << __LINE__ << ": " << ex.what() << std::endl;
}
});
};

spawn_foo(true);
spawn_foo(false);

loop.run();
}

Prints

0
1
2
3
4
5
async_foo returned 42
L:45: Throw an error: No such file or directory

How to wait for either of two timers to finish (Boost Asio)

I took the question to mean "how do you async_wat_any(timer1, timer2, ..., yield).

The other answer is correct in pointing at callback completion-handlers to provide this, but they don't provide the glue back to a single coroutine.

Now Asio's async operations abstract away the difference between all the invocation styles (callback, use_future, use_awaitable, yield_context etc...) - bringing them all back under the "callback" style essentially.

Therefore you can make your own async intiation that ties these torgether, rough sketch:

template <typename Token>
auto async_wait_any( std::vector<std::reference_wrapper<timer>> timers, Token token) {
using Result =
boost::asio::async_result<std::decay_t<Token>, void(error_code)>;
using Handler = typename Result::completion_handler_type;

Handler handler(token);
Result result(handler);

for (timer& t : timers) {
t.async_wait([=](error_code ec) mutable {
if (ec == boost::asio::error::operation_aborted)
return;
for (timer& t : timers) {
t.cancel_one();
}
handler(ec);
});
}

return result.get();
}

Now in your coroutine you can say:

timer a(ex, 100ms);
timer b(ex, 200ms);
timer c(ex, 300ms);

async_wait_any({a, b, c}, yield);

and it will return when the first one completes.

Let's Demo

Also, making it more generic, not hard-coding the timer type. In fact on a Windows environment you will be able to wait on Waitable Objects (like Event, Mutex, Semaphore) with the same interface:

template <typename Token, typename... Waitable>
auto async_wait_any(Token&& token, Waitable&... waitable) {
using Result =
boost::asio::async_result<std::decay_t<Token>, void(error_code)>;
using Handler = typename Result::completion_handler_type;

Handler completion_handler(std::forward<Token>(token));
Result result(completion_handler);

// TODO use executors from any waitable?
auto ex = get_associated_executor(
completion_handler,
std::get<0>(std::tie(waitable...)).get_executor());

auto handler = [&, ex, ch = completion_handler](error_code ec) mutable {
if (ec != boost::asio::error::operation_aborted) {
(waitable.cancel_one(), ...);
post(ex, [=]() mutable { ch(ec); });
}
};

(waitable.async_wait(bind_executor(ex, handler)), ...);

return result.get();
}

We'll write a demo coroutine like:

int main() {
static auto logger = [](auto name) {
return [name, start = now()](auto const&... args) {
((std::cout << name << "\t+" << (now() - start) / 1ms << "ms\t") << ... << args) << std::endl;
};
};

boost::asio::io_context ctx;
auto wg = make_work_guard(ctx);

spawn(ctx, [log = logger("coro1"),&wg](yield_context yield) {
log("started");

auto ex = get_associated_executor(yield);
timer a(ex, 100ms);
timer b(ex, 200ms);
timer c(ex, 300ms);

log("async_wait_any(a,b,c)");
async_wait_any(yield, a, b, c);

log("first completed");
async_wait_any(yield, c, b);
log("second completed");
assert(a.expiry() < now());
assert(b.expiry() < now());

// waiting again shows it expired as well
async_wait_any(yield, b);

// but c hasn't
assert(c.expiry() >= now());

// unless we wait for it
async_wait_any(yield, c);
log("third completed");

log("exiting");
wg.reset();
});

ctx.run();
}

This prints Live On Coliru

coro1   +0ms    started
coro1 +0ms async_wait_any(a,b,c)
coro1 +100ms first completed
coro1 +200ms second completed
coro1 +300ms third completed
coro1 +300ms exiting

Notes, Caveats

Tricky bits:

  • It's hard to decide what executor to bind the handlers to, since there could be multiple associated executors. However, since you're using coroutines, you'll always get the correct strand_executor associated with the yield_context

  • It's important to do the cancellations before invoking the caller's completion token, because otherwise the coroutine is already resumed before it was safe, leading to potential lifetime issues

  • Speaking of which, since now we post async operations outside the coroutine with the coroutine suspended, we will need a work-guard, because coroutines are not work.



Related Topics



Leave a reply



Submit