1

I am currently writing a simple server using Boost.Asio 1.68 and I am wondering if there is a way for me to add a handler that is executed when the event loop has no other work to do.

Currently I have this:

void completionHandler (boost::asio::io_context* ioCtx){
  // poll for some condition
  // if (condition) do some work;
  ioCtx->post(boost::bind(completionHandler, ioCtx));
}

//elsewhere
ioCtx->post(boost::bind(completionHandler, ioCtx));

However, this doesn't exactly match what I want to do.

zirn
  • 11
  • 1

1 Answers1

1

This wouldn't do what you expect it to do.

For example, with a single async_accept loop, you would never reach the point of "no other work to do".

Likewise, if only a single party owns outstanding work<> (docs and why) there will never be a situation where there is "no other work to do".

Basically, what you really want to do is to chain the polling:

template <typename Condition, typename Handler, typename Executor>
void run_when(Executor& ex, Condition c, Handler h) {
    struct Op {
        Executor& _executor;
        Condition _cond;
        Handler _handler;

        Op(Executor& ex, Condition c, Handler h) 
            : _executor(ex), _cond(std::move(c)), _handler(std::move(h))
        { }

        void operator()() const {
            if (_cond())
                _handler(error_code{});
            else
                ba::post(_executor, std::move(*this));
        }
    };

    ba::post(ex, Op{ex, std::move(c), std::move(h)});
}

This can be used like:

run_when(io,
        [&] { return bool(triggered); },
        [](error_code ec) { std::clog << "triggered: " << ec.message() << "\n"; });

Demo

Live On Coliru

#include <boost/asio.hpp>
namespace ba = boost::asio;
using boost::system::error_code;
using namespace std::chrono_literals;

template <typename Condition, typename Handler, typename Executor>
void run_when(Executor& ex, Condition c, Handler h) {
    struct RunWhen {
        Executor& _executor;
        Condition _cond;
        Handler _handler;

        RunWhen(Executor& ex, Condition c, Handler h) 
            : _executor(ex), _cond(std::move(c)), _handler(std::move(h))
        { }

        void operator()() const {
            if (_cond())
                _handler(error_code{});
            else
                ba::post(_executor, std::move(*this));
        }
    };

    ba::post(ex, RunWhen{ex, std::move(c), std::move(h)});
}

#include <iostream>
int main() {
    // some state that gets changed in the background
    std::atomic_bool triggered { false };
    std::thread([&] { 
            std::this_thread::sleep_for(1.5s);
            triggered = true;
        }).detach();

    ba::io_context io;

    // just some background polling that shall not block other work
    run_when(io, [&] { return bool(triggered); }, [](error_code ec) { std::clog << "triggered: " << ec.message() << "\n"; });

    io.run_for(3s);
}

Prints (after ~1.5s):

triggered: Success

BONUS

Why does our handler take an error_code? Well, in line with other Asio operations, you might want to be able to cancel them. Either you make the caller responsible for extending the lifetime of the the run_when<>(...)::Op instance, complicating life. Or you make it so the Condition calleable can return a code indicating whether the condition was satisfied or the wait was abandoned¹:

Live On Coliru

#include <boost/asio.hpp>
namespace ba = boost::asio;
using boost::system::error_code;
using boost::system::system_error;
using ba::error::operation_aborted;
using namespace std::chrono_literals;

template <typename Condition, typename Handler, typename Executor>
void run_when(Executor& ex, Condition c, Handler h) {
    struct Op {
        Executor& _executor;
        Condition _cond;
        Handler _handler;

        Op(Executor& ex, Condition c, Handler h) 
            : _executor(ex), _cond(std::move(c)), _handler(std::move(h))
        { }

        void operator()() const {
            try {
                if (_cond())
                    _handler(error_code{});
                else
                    ba::post(_executor, std::move(*this));
            } catch(system_error const& se) {
                _handler(se.code());
            }
        }
    };

    ba::post(ex, Op{ex, std::move(c), std::move(h)});
}

#include <random>
auto random_delay() {
    static std::mt19937 engine(std::random_device{}());
    return (std::uniform_int_distribution<>(1,2)(engine)) * 1s;
}

#include <iostream>
int main() {
    // some state that gets changed in the background
    std::atomic_bool triggered { false }, canceled { false };
    std::thread([&] { std::this_thread::sleep_for(1.5s); triggered = true; }).detach();

    // add a randomized cancellation
    {
        auto cancel_time = random_delay();
        std::clog << "hammer time: " << (cancel_time/1.0s) << "s\n";
        std::thread([&] { std::this_thread::sleep_for(cancel_time); canceled = true; }).detach();
    }

    ba::io_context io;

    // just some background polling that shall not block other work
    auto condition = [&] { return canceled? throw system_error(operation_aborted) : bool(triggered); };

    run_when(io, condition, [](error_code ec) { std::clog << "handler: " << ec.message() << "\n"; });

    io.run_for(3s);
}

Which prints either

hammer time: 1s
handler: Success

or

hammer time: 2s
handler: Success

depending on the value of random_delay().


¹ (or that the mayor's daughter had a divorce, because error_code is pretty versatile like that)

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    CAVEAT: in real life systems you want to throttle your callback chain, because if nothing else is queued, it would approximate a busy-loop. – sehe Oct 27 '18 at 22:25