Cancel deadline_timer, call callback anyway

I was surprised not to find a clock component in boost :: asio (our widely used library), so he tried to make a simple, minimalistic implementation to test my code.

Using boost::asio::deadline_timer

I made the following class

class Clock
{
    public:
        using callback_t = std::function<void(int, Clock&)>;
        using duration_t = boost::posix_time::time_duration;

    public:
        Clock(boost::asio::io_service& io,
              callback_t               callback = nullptr,
              duration_t               duration = boost::posix_time::seconds(1),
              bool                     enable   = true)
            : m_timer(io)
            , m_duration(duration)
            , m_callback(callback)
            , m_enabled(false)
            , m_count(0ul)
        {
            if (enable) start();
        }

        void start()
        {
            if (!m_enabled)
            {
                m_enabled = true;
                m_timer.expires_from_now(m_duration);
                m_timer.async_wait(boost::bind(&Clock::tick, this, _1)); // std::bind _1 issue ?
            }
        }

        void stop()
        {
            if (m_enabled)
            {
                m_enabled = false;
                size_t c_cnt = m_timer.cancel();
                #ifdef DEBUG
                printf("[DEBUG@%p] timer::stop : %lu ops cancelled\n", this, c_cnt);
                #endif
            }
        }

        void tick(const boost::system::error_code& ec)
        {
            if(!ec)
            {
                m_timer.expires_at(m_timer.expires_at() + m_duration);
                m_timer.async_wait(boost::bind(&Clock::tick, this, _1)); // std::bind _1 issue ?
                if (m_callback) m_callback(++m_count, *this);
            }
        }

        void              reset_count()                            { m_count = 0ul;         }
        size_t            get_count()                        const { return m_count;        }
        void              set_duration(duration_t duration)        { m_duration = duration; }
        const duration_t& get_duration()                     const { return m_duration;     }
        void              set_callback(callback_t callback)        { m_callback = callback; }
        const callback_t& get_callback()                     const { return m_callback;     }

    private:
        boost::asio::deadline_timer m_timer;
        duration_t                  m_duration;
        callback_t                  m_callback;
        bool                        m_enabled;
        size_t                      m_count;
};

      

However, the method stop

doesn't seem to work. If I ask to Clock c2

stop anotherClock c1

boost::asio::io_service ios;
Clock c1(ios, [&](int i, Clock& self){
        printf("[C1 - fast] tick %d\n", i);
    }, boost::posix_time::millisec(100)
);
Clock c2(ios, [&](int i, Clock& self){
        printf("[C2 - slow] tick %d\n", i);
        if (i%2==0) c1.start(); else c1.stop(); // Stop and start
    }, boost::posix_time::millisec(1000)
);
ios.run();

      

I see that both clock ticks are expected to be expected, sometimes c1 doesn't stop for one second while it should.

It looks like the call m_timer.cancel()

doesn't always work due to some kind of sync issue. Did I have something wrong?

+3


source to share


2 answers


Show a reproducible problem first:

Live On Coliru (code below)

As you can see, I am running it as

./a.out | grep -C5 false

      

This filters the output for records that are printed from the C1 completion handler when indeed c1_active

is false (and the completion handler should not have run)

The problem, in a nutshell, is the "logical" race condition.

This is a bit of a clever bend because there is only one strand (visible on the surface). But it really isn't too difficult.

What's happening:

  • when the C1 clock expires, it will send the completion handler to the task queue io_service

    . This means it may not work right away.

  • imagine C2 expired too, and the completion handler now gets scheduled execution and executes before C1 just pressed. Imagine that for some big match this time C2 decides to call stop()

    C1.

  • After the C2 completion handler is executed, the C1 completion handler is called.

    OOPS

    It still has ec

    it saying "no error" ... Hence the deadline timer for C1 is rescheduled. Unfortunately.

Background

For a deeper look at the guarantees that Asio (doesn't) do for the execution order of completion handlers, see



Solutions?

The simplest solution is to understand what m_enabled

could be false

. Let's just add a check:

void tick(const boost::system::error_code &ec) {
    if (!ec && m_enabled) {
        m_timer.expires_at(m_timer.expires_at() + m_duration);
        m_timer.async_wait(boost::bind(&Clock::tick, this, _1));

        if (m_callback)
            m_callback(++m_count, *this);
    }
}

      

On my system, it no longer reproduces the problem :)

Loudspeaker

Live On Coliru

#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/date_time/posix_time/posix_time_io.hpp>

static boost::posix_time::time_duration elapsed() {
    using namespace boost::posix_time;
    static ptime const t0 = microsec_clock::local_time();
    return (microsec_clock::local_time() - t0);
}

class Clock {
  public:
    using callback_t = std::function<void(int, Clock &)>;
    using duration_t = boost::posix_time::time_duration;

  public:
    Clock(boost::asio::io_service &io, callback_t callback = nullptr,
          duration_t duration = boost::posix_time::seconds(1), bool enable = true)
            : m_timer(io), m_duration(duration), m_callback(callback), m_enabled(false), m_count(0ul) 
    {
        if (enable)
            start();
    }

    void start() {
        if (!m_enabled) {
            m_enabled = true;
            m_timer.expires_from_now(m_duration);
            m_timer.async_wait(boost::bind(&Clock::tick, this, _1)); // std::bind _1 issue ?
        }
    }

    void stop() {
        if (m_enabled) {
            m_enabled = false;
            size_t c_cnt = m_timer.cancel();
#ifdef DEBUG
            printf("[DEBUG@%p] timer::stop : %lu ops cancelled\n", this, c_cnt);
#endif
        }
    }

    void tick(const boost::system::error_code &ec) {
        if (ec != boost::asio::error::operation_aborted) {
            m_timer.expires_at(m_timer.expires_at() + m_duration);
            m_timer.async_wait(boost::bind(&Clock::tick, this, _1));
            if (m_callback)
                m_callback(++m_count, *this);
        }
    }

    void reset_count()                     { m_count = 0ul;         } 
    size_t get_count() const               { return m_count;        } 

    void set_duration(duration_t duration) { m_duration = duration; } 
    const duration_t &get_duration() const { return m_duration;     } 

    void set_callback(callback_t callback) { m_callback = callback; } 
    const callback_t &get_callback() const { return m_callback;     } 

  private:
    boost::asio::deadline_timer m_timer;
    duration_t m_duration;
    callback_t m_callback;
    bool m_enabled;
    size_t m_count;
};

#include <iostream>

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

    bool c1_active = true;

    Clock c1(ios, [&](int i, Clock& self)
            { 
                std::cout << elapsed() << "\t[C1 - fast] tick" << i << " (c1 active? " << std::boolalpha << c1_active << ")\n";
            },
            boost::posix_time::millisec(1)
            );

#if 1
    Clock c2(ios, [&](int i, Clock& self)
            {
                std::cout << elapsed() << "\t[C2 - slow] tick" << i << "\n";
                c1_active = (i % 2 == 0);

                if (c1_active)
                    c1.start();
                else
                    c1.stop();
            },
            boost::posix_time::millisec(10)
        );
#endif

    ios.run();
}

      

+2


source


From the acceleration documentation:

If the timer has already expired when the cancel () function is called, then the handlers for asynchronous wait operations will be:

  • have already been called;
  • or have been queued for a call in the near future.

These handlers can no longer be canceled, and therefore an error code is passed that indicates the successful completion of the wait operation.

Your application on such successful completion (when the timer has already expired) will restart the timer again, and another interesting thing that in the "Start" function calls you, again implicitly canceling the timer in case it has not yet expired.



Function

expires_at sets the expiration time. Any pending asynchronous wait operations will be canceled. The handler for each canceled operation will be called by boost :: asio :: error :: operation_aborted error code.

Maybe you can reuse the m_enabled variable or just have another flag to detect when the timer is canceled.

Another solution is possible: timer example

+2


source







All Articles