3

I'm using multiple boost::asio::deadline_timer on one io_service object. std::shared_ptr of boost::asio::deadline_timer are stored in the container std::map<int, std::shared_ptr<debug_tim>> timers with index.

In the timer handler, I erase other boost::asio::deadline_timer. However, it seems that the erased timer woule be often fired with success error code.

Is there any way to avoid that. I expect that the timer handler that corresponding to the erased boost::asio::deadline_timer always fires with Operation canceled.

Am I missing something?

Here is the code that reproduces the behavior

https://wandbox.org/permlink/G0qzYcqauxdqw4i7

#include <iostream>
#include <memory>

#include <boost/asio.hpp>

// deadline_timer with index ctor/dtor print
struct debug_tim : boost::asio::deadline_timer {
    debug_tim(boost::asio::io_service& ios, int i) : boost::asio::deadline_timer(ios), i(i) {
        std::cout << "debug_tim() " << i << std::endl;
    }
    ~debug_tim() {
        std::cout << "~debug_tim() " << i << std::endl;
    }
    int i;
};

int main() {
    boost::asio::io_service ios;
    std::map<int, std::shared_ptr<debug_tim>> timers;
    {
        for (int i = 0; i != 5; ++i) {
            auto tim = std::make_shared<debug_tim>(ios, i);
            std::cout << "set timer " << i << std::endl;
            tim->expires_from_now(boost::posix_time::seconds(1));
            timers.emplace(i, tim);
            tim->async_wait([&timers, i](auto ec){
                    std::cout << "timer fired " << i << " : " <<  ec.message() << std::endl;
                    auto it = timers.find(i);
                    if (it == timers.end()) {
                        std::cout << "  already destructed." << std::endl;
                    }
                    else {
                        int other_idx = i + 1; // erase other timer (e.g. i + 1)
                        timers.erase(other_idx);
                        std::cout << "  erased " << other_idx << std::endl;
                    }
                }
            );
        }
    }
    ios.run();
}

I also call boost::asio::deadline_timer::cancel() before I erase the timer. However, I got similar result. Here is the cancel version:

https://wandbox.org/permlink/uM0yMFufkyn9ipdG

#include <iostream>
#include <memory>

#include <boost/asio.hpp>

// deadline_timer with index ctor/dtor print
struct debug_tim : boost::asio::deadline_timer {
    debug_tim(boost::asio::io_service& ios, int i) : boost::asio::deadline_timer(ios), i(i) {
        std::cout << "debug_tim() " << i << std::endl;
    }
    ~debug_tim() {
        std::cout << "~debug_tim() " << i << std::endl;
    }
    int i;
};

int main() {
    boost::asio::io_service ios;
    std::map<int, std::shared_ptr<debug_tim>> timers;
    {
        for (int i = 0; i != 5; ++i) {
            auto tim = std::make_shared<debug_tim>(ios, i);
            std::cout << "set timer " << i << std::endl;
            tim->expires_from_now(boost::posix_time::seconds(1));
            timers.emplace(i, tim);
            tim->async_wait([&timers, i](auto ec){
                    std::cout << "timer fired " << i << " : " <<  ec.message() << std::endl;
                    auto it = timers.find(i);
                    if (it == timers.end()) {
                        std::cout << "  already destructed." << std::endl;
                    }
                    else {
                        int other_idx = i + 1; // erase other timer (e.g. i + 1)
                        auto other_it = timers.find(other_idx);
                        if (other_it != timers.end()) {
                            other_it->second->cancel();
                            timers.erase(other_it);
                        }
                        std::cout << "  erased " << other_idx << std::endl;
                    }
                }
            );
        }
    }
    ios.run();
}

Edit

Felix, thank you for the answer. I understand the boost::asio::deadline::timer::cancel() behavior. I always need to care the lifetime of boost::asio::deadline::timer. I my actual code of my project, the ``boost::asio::deadline::timer` is a member variable of another object such as a session object. And in the timer handler, it accesses the object. It's dangerous.

I consider how to write safe code. And I come up with using std::weak_ptr in order to check the object's lifetime.

Here is the updated code:

#include <iostream>
#include <memory>

#include <boost/asio.hpp>

// deadline_timer with index ctor/dtor print
struct debug_tim : boost::asio::deadline_timer {
    debug_tim(boost::asio::io_service& ios, int i) : boost::asio::deadline_timer(ios), i(i) {
        std::cout << "debug_tim() " << i << std::endl;
    }
    ~debug_tim() {
        std::cout << "~debug_tim() " << i << std::endl;
    }
    int i;
};

int main() {
    boost::asio::io_service ios;
    std::map<int, std::shared_ptr<debug_tim>> timers;
    {
        for (int i = 0; i != 5; ++i) {
            auto tim = std::make_shared<debug_tim>(ios, i);
            std::cout << "set timer " << i << std::endl;
            tim->expires_from_now(boost::posix_time::seconds(1));
            timers.emplace(i, tim);

            // Capture tim as the weak_ptr wp
            tim->async_wait([&timers, i, wp = std::weak_ptr<debug_tim>(tim)](auto ec){
                    std::cout << "timer fired " << i << " : " <<  ec.message() << std::endl;

                    // Check the lifetime of wp
                    if (!wp.lock()) std::cout << "  timer freed." << std::endl; // return here on actual code

                    auto it = timers.find(i);
                    if (it == timers.end()) {
                        std::cout << "  already destructed." << std::endl;
                    }
                    else {
                        int other_idx = i + 1; // erase other timer (e.g. i + 1)
                        timers.erase(other_idx);
                        std::cout << "  erased " << other_idx << std::endl;
                    }
                }
            );
        }
    }
    ios.run();
}

Is this a good way to avoid accessing the deleted object that has the boost::asio::deadline_timer ?

Edit

My weak_ptr solution works well.

See How to avoid firing already destroyed boost::asio::deadline_timer

Community
  • 1
  • 1
Takatoshi Kondo
  • 3,111
  • 17
  • 36
  • 1
    It took me three days to figure out the behavior of timer. Did your solution satisfy `I expect that the timer handler that corresponding to the erased boost::asio::deadline_timer always fires with Operation canceled`, that's exactly what I am looking for. – YNX Jan 26 '20 at 17:01

1 Answers1

1

According to the reference of deadline_timer::cancel:

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

  • have already been invoked; or

  • have been queued for invocation in the near future.

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

We can know that calling cancel() can not cancel the timer which has already been queued for firing.

And it seems that the dealine_timer doesn't override destructor. (There is no destructor in the member list of deadline_timer)

In your code snippet, all timers will fire at almost the same time. Concerning that asio will use some internal threads, it's quite probably that when one completion handler is called, the others are being queued.

Community
  • 1
  • 1
felix
  • 2,213
  • 7
  • 16
  • Thank you for the answer. I understand the behavior of dead_time_timer. The reason that I want to avoid or want to know cancelled status is that I access the object that has the deadline_timer in the timeout handler. Now, my goal has been changed to how to avoid accessing the deleted resource in the timer handler. So I updated (added) my question. Could you check my approach? – Takatoshi Kondo Mar 27 '17 at 22:48
  • @TakatoshiKondo Yes, this is good. Just watch out for race condition when firing them concurrently. – felix Mar 28 '17 at 07:20