I would like to learn how to pass timeout timer to boost::asio::yield_context.
Let's say, in terms of Boost 1.80, there is smth like the following:
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
void async_func_0(boost::asio::yield_context yield) {
async_func_1(yield);
}
void async_func_1(boost::asio::yield_context) {
}
int main() {
boost::asio::io_context ioc;
boost::asio::spawn(ioc.get_executor(), &async_func_0);
ioc.run();
return 0;
}
Let's imaging that the async_func_1 is quite a burden, it is async by means of boost::coroutines (since boost::asio does not use boost::coroutines2 for some unknown reason) and it can work unpredictably long, mostly on io operations.
A good idea would be to specify the call of async_func_1 with a timeout so that if the time passed it must return whatever with an error. Let's say at the nearest use of boost::asio::yield_context within the async_func_1.
But I'm puzzled how it should be expressed in terms of boost::asio.
P.S. Just to exemplify, in Rust it would be smth like the following:
use std::time::Duration;
use futures_time::FutureExt;
async fn func_0() {
func_1().timeout(Duration::from_secs(60)).await;
}
async fn func_1() {
}
#[tokio::main]
async fn main() {
tokio::task::spawn(func_0());
}
In Asio cancellation and executors are separate concerns.
That's flexible. It also means you have to code your own timeout.
One very rough idea:
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <iostream>
namespace asio = boost::asio;
using boost::asio::yield_context;
using namespace std::chrono_literals;
using boost::system::error_code;
static std::chrono::steady_clock::duration s_timeout = 500ms;
template <typename Token>
void async_func_1(Token token) {
error_code ec;
// emulating a long IO bound task
asio::steady_timer work(get_associated_executor(token), 1s);
work.async_wait(redirect_error(token, ec));
std::cout << "async_func_1 completion: " << ec.message() << std::endl;
}
void async_func_0(yield_context yield) {
asio::cancellation_signal cancel;
auto cyield = asio::bind_cancellation_slot(cancel.slot(), yield);
std::cout << "async_func_0 deadline at " << s_timeout / 1.0s << "s" << std::endl;
asio::steady_timer deadline(get_associated_executor(cyield), s_timeout);
deadline.async_wait([&](error_code ec) {
std::cout << "Timeout: " << ec.message() << std::endl;
if (!ec)
cancel.emit(asio::cancellation_type::terminal);
});
async_func_1(cyield);
std::cout << "async_func_0 completion" << std::endl;
}
int main(int argc, char** argv) {
if (argc>1)
s_timeout = 1ms * atoi(argv[1]);
boost::asio::io_context ioc;
spawn(ioc.get_executor(), async_func_0);
ioc.run();
}
No online compilers that accept this¹ are able to run this currently. So here's local output:
for t in 150 1500; do time ./build/sotest "$t" 2>"$t.trace"; ~/custom/superboost/libs/asio/tools/handlerviz.pl < "$t.trace" | dot -T png -o trace_$t.png; done
async_func_0 deadline at 0.15s
Timeout: Success
async_func_1 completion: Operation canceled
async_func_0 completion
real 0m0,170s
user 0m0,009s
sys 0m0,011s
async_func_0 deadline at 1.5s
async_func_1 completion: Success
async_func_0 completion
Timeout: Operation canceled
real 0m1,021s
user 0m0,011s
sys 0m0,011s
And the handler visualizations:
¹ wandbox, coliru, CE
Road From Here
You'll probably say this is cumbersome. Compared to your Rust library feature it is. To library this in Asio you could
derive your own completion token from type yield_context, adding the behaviour you want
make a composing operation (e.g. using deferred)
Related
I'm using boost::asio::async_read() method to asynchronously get response from server which take 10 second to process request. I am able to read response successfully. but I am attaching callback to this function to process received response which take 10 seconds. I am unable to find how i can process that callback asynchronously.
void read_response(std::string data)
{
cout << data << endl;
sleep(10);
// if you think sleep works differently if i send another rest api request from here still this function blocks the processing
}
boost::asio::async_read(socket, boost::asio::buffer(buffer), [&](const boost::system::error_code&
error,
std::size_t bytes_transferred) {
io_context.post([&]() {
read_response(buffer.data());
//read_response is not processing asynchronously
});
});
I tried posting function to io_context, i tried using
std::async(std::launch::deferred, read_handler,error,bytes_transferred,buffer.data()).wait() ;
I am new to asyn programming and c++ programming.
Indeed, don't do blocking operations on the IO service. Or grow the thread pool to accomodate for the maximum number of concurrent tasks that must be supported.
In all circumstances, copy the message into your read handler, instead of passing the buffer directly (as you did) as that invites race conditions/stale references.
Let's demonstrate using httpbin.org/delay:
Live On Coliru
#include <boost/asio.hpp>
#include <iomanip>
#include <iostream>
namespace asio = boost::asio;
using asio::ip::tcp;
using namespace std::chrono_literals;
std::string const request = "GET /delay/10 HTTP/1.1\r\nHost: httpbin.org\r\n\r\n";
void read_response(std::string data) {
std::cout << "Asynchronously received response: " << quoted(data) << std::endl;
sleep(10);
}
int main() {
asio::io_context ioc(1);
asio::thread_pool work(10); // support 10 tasks along-side async IO
tcp::socket conn(ioc);
connect(conn, tcp::resolver(ioc).resolve("httpbin.org", "80"));
write(conn, asio::buffer(request));
std::string buf;
async_read_until( //
conn, asio::dynamic_buffer(buf), "\r\n\r\n",
[&work, &buf](boost::system::error_code ec, size_t n) {
std::cout << "\n*** Completion " << ec.message() << ", " << n << std::endl;
post(work, [msg = buf.substr(0, n)] {
// NOTE: lambda owns msg, not a reference to `buf`
read_response(std::move(msg));
});
});
std::thread([&ioc] {
ioc.run();
std::cout << "\n*** IO complete" << std::endl;
}).detach();
for (int i = 0; i < 15; ++i) {
std::cout << "." << std::flush;
std::this_thread::sleep_for(1s);
}
work.join();
std::cout << "\n*** Work Done" << std::endl;
}
This shows work continuing, "asynchronously" (relative to IO) well after ioc completes:
Do boost::asio c++20 coroutines support multithreading?
The boost::asio documentation examples are all single-threaded, are there any multithreaded examples?
Yes.
In Asio, if multiple threads run execution context, you don't normally even control which thread resumes your coroutine.
You can look at some of these answers that ask about how to switch executors mid-stream (controlling which strand or execution context may resume the coro):
asio How to change the executor inside an awaitable?
Switch context in coroutine with boost::asio::post
Update to the comment:
To make the c++20 coro echo server sample multi-threading you could change 2 lines:
boost::asio::io_context io_context(1);
// ...
io_context.run();
Into
boost::asio::thread_pool io_context;
// ...
io_context.join();
Since each coro is an implicit (or logical) strand, nothing else is needed. Notes:
Doing this is likely useless, unless you're doing significant work inside the coroutines, that would slow down IO multiplexing on a single thread.
In practice a single thread can easily handle 10k concurrent connections, especially with C++20 coroutines.
Note that it can be a significant performance gain to run the asio::io_context(1) with the concurrency hint, because it can avoid synchronization overhead.
When you introduce e.g. asynchronous session control or full-duplex you will have the need for an explicit strand. In the below example I show how you would make each "session" use a strand, and e.g. do graceful shutdown.
Live On Coliru
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>
#include <list>
namespace asio = boost::asio;
namespace this_coro = asio::this_coro;
using boost::system::error_code;
using asio::ip::tcp;
using asio::detached;
using executor_type = asio::any_io_executor;
using socket_type = asio::use_awaitable_t<>::as_default_on_t<tcp::socket>; // or tcp::socket
//
using session_state = std::shared_ptr<socket_type>; // or any additional state
using handle = std::weak_ptr<session_state::element_type>;
using namespace std::string_view_literals;
using namespace asio::experimental::awaitable_operators;
asio::awaitable<void> echo_session(session_state s) {
try {
for (std::array<char, 1024> data;;) {
size_t n = co_await s->async_read_some(asio::buffer(data));
co_await async_write(*s, asio::buffer(data, n));
}
} catch (boost::system::system_error const& se) {
if (se.code() != asio::error::operation_aborted) // expecting cancellation
throw;
} catch (std::exception const& e) {
std::cout << "echo Exception: " << e.what() << std::endl;
co_return;
}
error_code ec;
co_await async_write(*s, asio::buffer("Server is shutting down\n"sv),
redirect_error(asio::use_awaitable, ec));
// std::cout << "echo shutdown: " << ec.message() << std::endl;
}
asio::awaitable<void> listener(std::list<handle>& sessions) {
auto ex = co_await this_coro::executor;
for (tcp::acceptor acceptor(ex, {tcp::v4(), 55555});;) {
session_state s = std::make_shared<socket_type>(
co_await acceptor.async_accept(make_strand(ex), asio::use_awaitable));
sessions.remove_if(std::mem_fn(&handle::expired)); // "garbage collect", optional
sessions.emplace_back(s);
co_spawn(ex, echo_session(s), detached);
}
}
int main() {
std::list<handle> handles;
asio::thread_pool io_context;
asio::signal_set signals(io_context, SIGINT, SIGTERM);
auto handler = [&handles](std::exception_ptr ep, auto result) {
try {
if (ep)
std::rethrow_exception(ep);
int signal = get<1>(result);
std::cout << "Signal: " << ::strsignal(signal) << std::endl;
for (auto h : handles)
if (auto s = h.lock()) {
// more logic could be implemented via members on a session_state struct
std::cout << "Shutting down live session " << s->remote_endpoint() << std::endl;
post(s->get_executor(), [s] { s->cancel(); });
}
} catch (std::exception const& e) {
std::cout << "Server: " << e.what() << std::endl;
}
};
co_spawn(io_context, listener(handles) || signals.async_wait(asio::use_awaitable), handler);
io_context.join();
}
Online demo, and local demo:
The connection attempt below creates (on my network configuration) a delay of 2 minutes because the target does not exist and the address is on a different subnet to my machine. So I added timeout logic to limit the attempt to 5 seconds:
#define BOOST_ASIO_HAS_CO_AWAIT
#define BOOST_ASIO_HAS_STD_COROUTINE
#include <iostream>
#include <chrono>
#include <thread>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v4.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/redirect_error.hpp>
namespace this_coro = boost::asio::this_coro;
using namespace std::chrono_literals;
boost::asio::awaitable<void> fail_to_connect()
{
auto executor = co_await this_coro::executor;
auto socket = boost::asio::ip::tcp::socket{executor};
auto ep = boost::asio::ip::tcp::endpoint{
boost::asio::ip::make_address_v4("192.168.1.52"),
80};
auto timer = boost::asio::steady_timer{executor};
timer.expires_after(5s);
boost::asio::co_spawn(
executor,
[&]() -> boost::asio::awaitable<void> {
auto ec = boost::system::error_code{};
co_await timer.async_wait(boost::asio::redirect_error(boost::asio::use_awaitable, ec));
std::cout << "Thread ID: " << std::this_thread::get_id()
<< " Timer: " << ec.message() << std::endl;
if (!ec) {
socket.close();
}
},
boost::asio::detached
);
std::cout << "Thread ID: " << std::this_thread::get_id()
<< " Starting connection" << std::endl;
co_await boost::asio::async_connect(socket,
std::array{std::move(ep)},
boost::asio::use_awaitable);
timer.cancel();
}
int main()
{
auto ctx = boost::asio::io_context{};
auto guard = boost::asio::make_work_guard(ctx.get_executor());
auto exception_handler = [&](auto e_ptr) {
if (e_ptr) {
std::rethrow_exception(e_ptr);
}
};
boost::asio::co_spawn(ctx, fail_to_connect, std::move(exception_handler));
ctx.run();
}
This works as expected. As you can see from the thread ID's, using the same execution context between the two coroutines means that I'm not accessing the socket concurrently too.
14:58:41: Starting /home/cmannett85/workspace/build-scratch-Desktop-Debug/scratch ...
Thread ID: 140171855615808 Starting connection
Thread ID: 140171855615808 Timer: Success
terminate called after throwing an instance of 'boost::system::system_error'
what(): Operation canceled
14:58:46: The program has unexpectedly finished.
However this feels clunky, especially compared to the non-coroutine callback-style. Is there a better approach for timeouts using coroutines? I'm struggling to find examples.
One suggestion is to lift the boilerplate into a class.
Basically just a wrapper around a timer, that can set and cancel a callback with an easier interface:
#include "asio.hpp"
class timeout {
asio::high_resolution_timer timer_;
using duration = asio::high_resolution_timer::duration;
public:
template <typename Executor>
timeout(Executor&& executor) : timer_(executor){};
template <typename Executor, typename Func>
timeout(Executor&& executor, const duration& timeout, Func&& on_timeout)
: timer_(executor) {
set(timeout, on_timeout);
}
template <typename Func>
void set(const duration& timeout, Func&& on_timeout) {
timer_.expires_after(timeout); // cancels outstanding timeouts
timer_.async_wait([&](std::error_code ec) {
if (ec) return;
std::cout << "cancelling\n";
on_timeout();
});
}
void cancel() { timer_.cancel(); }
~timeout() { timer_.cancel(); }
};
This can be used like this:
tmo.set(1s, [&]() { socket.close(); });
std::size_t n = co_await asio::async_read_until(socket, incoming, '\n',
asio::use_awaitable);
asio::const_buffer response(asio::buffers_begin(incoming.data()),
asio::buffers_begin(incoming.data()) + n);
tmo.set(1s, [&]() { socket.close(); });
co_await asio::async_write(socket, response, asio::use_awaitable);
incoming.consume(n);
It can be generally used with any object that supports true cancellation, which is somewhat sparsely supported. For example, the only true way to cancel an ongoing read or write on a socket is to call close().
Cancelling an async_resolve for example, is not possible, because cancel() only cancels a pending operation, not an ongoing one. In other words, this doesn't work as expected:
// does not actually cancel anything :(
tmo.set(1s, [&]() { resolver.cancel(); });
resolver.async_resolve(...);
Here's a full example of an echo client that stops if any of these takes more than 1 second:
async_connect
async_read_until
async_write
As noted, async_resolve cannot be cancelled, and so writing a timeout for it doesn't make a lot of sense. Perhaps posting an exception to the coroutine's execution context would work instead?
#include <chrono>
#include <iostream>
#include <string>
#include <string_view>
// clang-format off
#ifdef _WIN32
#include "sdkddkver.h"
#endif
#include "asio.hpp"
// clang-format on
using asio::ip::tcp;
class timeout {
asio::high_resolution_timer timer_;
using duration = asio::high_resolution_timer::duration;
public:
template <typename Executor>
timeout(Executor&& executor) : timer_(executor){};
template <typename Executor, typename Func>
timeout(Executor&& executor, const duration& timeout, Func&& on_timeout)
: timer_(executor) {
set(timeout, on_timeout);
}
template <typename Func>
void set(const duration& timeout, Func&& on_timeout) {
timer_.expires_after(timeout); // cancels outstanding timeouts
timer_.async_wait([&](std::error_code ec) {
if (ec) return;
std::cout << "cancelling\n";
on_timeout();
});
}
void cancel() { timer_.cancel(); }
~timeout() { timer_.cancel(); }
};
asio::awaitable<void> echo_client(tcp::socket& socket, std::string_view host,
std::string_view service) {
try {
using namespace std::chrono_literals;
auto exec = socket.get_executor();
tcp::resolver resolver(exec);
timeout tmo(exec);
std::cout << "resolving...\n";
tmo.set(1s, [&]() { resolver.cancel(); });
auto endpoints =
co_await resolver.async_resolve(host, service, asio::use_awaitable);
std::cout << "connecting...\n";
tmo.set(1s, [&]() { socket.close(); });
co_await asio::async_connect(socket, endpoints, asio::use_awaitable);
asio::streambuf incoming;
for (;;) {
std::cout << "reading... ";
tmo.set(1s, [&]() { socket.close(); });
std::size_t n = co_await asio::async_read_until(socket, incoming, '\n',
asio::use_awaitable);
std::string response(asio::buffers_begin(incoming.data()),
asio::buffers_begin(incoming.data()) + n);
std::cout << response;
tmo.set(1s, [&]() { socket.close(); });
co_await asio::async_write(socket, asio::buffer(response),
asio::use_awaitable);
incoming.consume(n);
}
} catch (std::exception& e) {
std::cerr << e.what() << "\n";
}
}
int main(int argc, char** argv) {
if (argc < 3) {
std::cout << "usage: " << argv[0] << " [host] [port]\n";
return 1;
}
asio::io_context io_context;
tcp::socket socket(io_context);
asio::co_spawn(io_context, echo_client(socket, argv[1], argv[2]),
asio::detached);
io_context.run();
return 0;
}
As the title says i have a question concerning the following scenario (simplyfied example):
Assume that i have an object of the Generator-Class below, which continuously updates its dataChunk member ( running in the main thread).
class Generator
{
void generateData();
uint8_t dataChunk[999];
}
Furthermore i have an async. acceptor of TCP-connections to which 1-N clients can connect to (running in a second thread).
The acceptor starts a new thread for each new client-connection, in which an object of the Connection class below, receives a request message from the client and provides a fraction of the dataChunk (belonging to the Generator) as an answer. Then waits for a new request and so on...
class Connection
{
void setDataChunk(uint8_t* dataChunk);
void handleRequest();
uint8_t* dataChunk;
}
Finally the actual question: The desired behaviour is that the Generator object generates a new dataChunk and waits until all 1-N Connection objects have delt with their client requests until it generates a new dataChunk.
How do i lock the dataChunk for writing access of the Generator object while the Connection objects deal with their requests, but all Connection objects in their respective threads are supposed to have reading-access at the same time during their request-handling phase.
On the other hand the Connection objects are supposed to wait for a new dataChunk after dealing with their respective request, without dropping a new client request.
--> I think a single mutex won't do the trick here.
My first idea was to share a struct between the objects with a semaphore for the Generator and a vector of semaphores for the connections. With these, every object could "understand" the state of the full-system and work accordingly.
What to you guys think, what is best practice i cases like this?
Thanks in advance!
There are several ways to solve it.
You can use std::shared_mutex.
void Connection::handleRequest()
{
while(true)
{
std::shared_lock<std::shared_mutex> lock(GeneratorObj.shared_mutex);
if(GeneratorObj.DataIsAvailable()) // we need to know that data is available
{
// Send to client
break;
}
}
}
void Generator::generateData()
{
std::unique_lock<std::shared_mutex> lock(GeneratorObj.shared_mutex);
// Generate data
}
Or you can use a boost::lockfree::queue, but data structures will be different.
How do i lock the dataChunk for writing access of the Generator object while the Connection objects deal with their requests, but all Connection objects in their respective threads are supposed to have reading-access at the same time during their request-handling phase.
I'd make a logical chain of operations, that includes the generation.
Here's a sample:
it is completely single threaded
accepts unbounded connections and deals with dropped connections
it uses a deadline_timer object to signal a barrier when waiting for to send of a chunck to (many) connections. This makes it convenient to put the generateData call in an async call chain.
Live On Coliru
#include <boost/asio.hpp>
#include <list>
#include <iostream>
namespace ba = boost::asio;
using ba::ip::tcp;
using boost::system::error_code;
using Clock = std::chrono::high_resolution_clock;
using Duration = Clock::duration;
using namespace std::chrono_literals;
struct Generator {
void generateData();
uint8_t dataChunk[999];
};
struct Server {
Server(unsigned short port) : _port(port) {
_barrier.expires_at(boost::posix_time::neg_infin);
_acc.set_option(tcp::acceptor::reuse_address());
accept_loop();
}
void generate_loop() {
assert(n_sending == 0);
garbage_collect(); // remove dead connections, don't interfere with sending
if (_socks.empty()) {
std::clog << "No more connections; pausing Generator\n";
} else {
_gen.generateData();
_barrier.expires_at(boost::posix_time::pos_infin);
for (auto& s : _socks) {
++n_sending;
ba::async_write(s, ba::buffer(_gen.dataChunk), [this,&s](error_code ec, size_t written) {
assert(n_sending);
--n_sending; // even if failed, decreases pending operation
if (ec) {
std::cerr << "Write: " << ec.message() << "\n";
s.close();
}
std::clog << "Written: " << written << ", " << n_sending << " to go\n";
if (!n_sending) {
// green light to generate next chunk
_barrier.expires_at(boost::posix_time::neg_infin);
}
});
}
_barrier.async_wait([this](error_code ec) {
if (ec && ec != ba::error::operation_aborted)
std::cerr << "Client activity: " << ec.message() << "\n";
else generate_loop();
});
}
}
void accept_loop() {
_acc.async_accept(_accepting, [this](error_code ec) {
if (ec) {
std::cerr << "Accept fail: " << ec.message() << "\n";
} else {
std::clog << "Accepted: " << _accepting.remote_endpoint() << "\n";
_socks.push_back(std::move(_accepting));
if (_socks.size() == 1) // first connection?
generate_loop(); // start generator
accept_loop();
}
});
}
void run_for(Duration d) {
_svc.run_for(d);
}
void garbage_collect() {
_socks.remove_if([](tcp::socket& s) { return !s.is_open(); });
}
private:
ba::io_service _svc;
unsigned short _port;
tcp::acceptor _acc { _svc, { {}, _port } };
tcp::socket _accepting {_svc};
std::list<tcp::socket> _socks;
Generator _gen;
size_t n_sending = 0;
ba::deadline_timer _barrier {_svc};
};
int main() {
Server s(6767);
s.run_for(3s); // COLIRU
}
#include <fstream>
// synchronously generate random data chunks
void Generator::generateData() {
std::ifstream ifs("/dev/urandom", std::ios::binary);
ifs.read(reinterpret_cast<char*>(dataChunk), sizeof(dataChunk));
std::clog << "Generated chunk: " << ifs.gcount() << "\n";
}
Prints (for just the 1 client):
Accepted: 127.0.0.1:60870
Generated chunk: 999
Written: 999, 0 to go
Generated chunk: 999
[... snip ~4000 lines ...]
Written: 999, 0 to go
Generated chunk: 999
Write: Broken pipe
Written: 0, 0 to go
No more connections; pausing Generator
I have a server and a client code written in boost ASIO and it works pretty fine.
Since synchronous and asynchornous boost asio API's are different, is it possible in any way that the code I have written for asynchronous communication behaves and works in a synchronous fashion instead of asynchronous. ?
You can run any asynchronous code on a dedicated io_service, and simply run the service blocking:
Live On Coliru
#include <boost/asio.hpp>
#include <boost/asio/high_resolution_timer.hpp>
#include <iostream>
using namespace std::chrono_literals;
using namespace boost::asio;
using boost::system::system_error;
io_service svc;
high_resolution_timer deadline(svc, 3s);
void task_foo() {
deadline.async_wait([](system_error) { std::cout << "task done\n"; });
}
int main() {
task_foo();
std::cout << "Before doing work\n";
svc.run(); // blocks!
std::cout << "After doing work\n";
}
Prints
Before doing work
task done
After doing work
Alternatively:
You can always use futures that you can then await blocking:
Live On Coliru
#include <boost/asio.hpp>
#include <boost/asio/high_resolution_timer.hpp>
#include <boost/make_shared.hpp>
#include <future>
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
using namespace boost::asio;
using boost::system::system_error;
io_service svc;
high_resolution_timer deadline(svc, 3s);
std::future<int> task_foo() {
auto p = boost::make_shared<std::promise<int> >();
auto fut = p->get_future();
deadline.async_wait([p](system_error) {
std::cout << "task done\n";
p->set_value(42);
});
return fut;
}
int main() {
auto foo = task_foo();
std::cout << "Before doing work\n";
std::thread([] { svc.run(); }).detach(); // doesn't block!
std::cout << "After starting work\n"; // happens before task completion
auto result = foo.get(); // blocks again!
std::cout << "Task result: " << result << "\n";
}
Prints
Before doing work
After starting work
task done
Task result: 42
This way you can still have the io_service running concurrently and don't require it to complete even though a particular task completes synchronously (foo.get())