Should the Exception Thrown by Boost::Asio::Io_Service::Run() Be Caught

Catching exception from boost::asio::io_service::work running as a detached thread

You can catch exceptions in the worker thread, save them into a queue variable that is shared by the two threads, and check that queue periodically in the main thread.

To use a queue, you need to first convert your exceptions to a common type. You can use std::exception or string or whatever is the best for your situation. If you absolutely need to keep information of the original exception class, you can use boost::exception_ptr.

Variables you need (these could be members of AsioThread):

boost::mutex queueMutex;
std::queue<exceptionType> exceptionQueue;

Run this function in the worker thread:

void AsioThread::RunIoService(){
try{
ioService.run();
}
catch(const exceptionType& e){
boost::lock_guard<boost::mutex> queueMutex;
exceptionQueue.push(e);
}
catch(...){
boost::lock_guard<boost::mutex> queueMutex;
exceptionQueue.push(exceptionType("unknown exception"));
}
}

Launch the worker thread like this:

boost::thread t(boost::bind(&AsioThread::RunIoService, this));
t.detach();

In the main thread:

while(true){
// Do something

// Handle exceptions from the worker thread
bool hasException = false;
exceptionType newestException;
{
boost::lock_guard<boost::mutex> queueMutex;
if(!exceptionQueue.empty()){
hasException = true;
newestException = exceptionQueue.front();
exceptionQueue.pop();
}
}
if(hasException){
// Do something with the exception
}
}

This blog post implements a thread-safe queue, which you can use to simplify saving exceptions; in that case you would not need a separate mutex because that would be inside the queue class.

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

Exception handling in Boost.Asio

There is nothing wrong with the pattern recommended by Boost.Asio. What you should do is package any necessary information for handling the exception along with the exception object. If you use boost::exception (or a type derived from it) for your exception handling, you can very easily attach metadata (including session information) by creating a specialization of boost::error_info and attaching it to the exception object using operator<<. Your catch block can then extract this info with get_error_info.

Boost ASIO exception propagation

basic_deadline_timer::async_wait documentation states:

Regardless of whether the asynchronous operation completes immediately
or not, the handler will not be invoked from within this function.
Invocation of the handler will be performed in a manner equivalent to
using boost::asio::io_service::post().

This means that the handler will be called from the inside io_service::run() (in the thread that called it), so exception will be automatically propagated to user code and no special handling is required inside Asio. Usually samples doesn't include error handling for simplicity.

Sorry, I don't know why error_code placeholder is not specified in the sample. Would need to have a look at Asio sources.

Boost Exception Handling with Boost ASIO

The explanation you quote is somewhat misleading.

Actually, io_service propagates any exceptions that escape from completion handlers, so it doesn't matter whether we use it for "user work" or for "asio functions" - in any case we might want to handle exceptions escaping from io_service::run (not only std::exception!).
Consider the following sample:

void my_handler(const error_code&)
{
// this exception will escape from io_service::run()!
throw 0;
}

void setup_timer()
{
deadline_timer_.expires_from_now(seconds(5));
deadline_timer_.async_wait(my_handler);
}

The difference between io_service::run(error_code &ec) and io_service::run() is that the latter deliberately throws an exception, if ec implies error. Quoting from io_service.ipp:

std::size_t io_service::run()
{
boost::system::error_code ec;
std::size_t s = impl_.run(ec);
boost::asio::detail::throw_error(ec);
return s;
}

So, the bottom line is that it would be enough to use the throwing overload (and, optionally, multiple catch handlers to distinguish between exception types).

Calling boost::asio::io_service run function from multiple threads

To restart the event handler I need to call run again but the documentation stated that restart() has to be called first.

No the documentation does not say that. You need to reset once the service had run out of work/been stopped. You did neither, so you do not need reset there.

Simply do as explained in this post Should the exception thrown by boost::asio::io_service::run() be caught? (which links to the docs)

Exception handling in Boost.Asio when io_service is threaded

Nothing magical happens. If you catch the exception somewhere, then your catch block handles it. Otherwise, an uncaught exception terminates the process.

How you should handle it depends on what you want to happen. If the exception should never happen, then let it terminate the process. If you want to eat or handle the exceptions, then write a function that wraps io_service::run in a try/catch block and have the threads run that instead.

I don't like putting the intelligence that far from the code. My preferred solution is to never have my asynchronous functions throw exceptions unless there's a truly fatal error. If an asynchronous function knows how to handle an exception it might throw, then it should catch it.

However, it is perfectly acceptable to wrap run if that makes sense in your application.



Related Topics



Leave a reply



Submit