C++ boost asynchronous timer to run in parallel with program

匆匆过客 提交于 2021-02-04 06:28:48

问题


Note: This is for C++98

I am trying to develop a simple timer/counter that runs in the background of my main program.

I haven't used asynchronous timers before, and I have been trying to follow the boost tutorials on how to do this, but they still seem to block my main function. I've slightly modified Timer.3 from the boost website to experiment.

Essentially, with the program below what I want to do is:

  1. Run main
  2. Execute testRun() which counts to 5
  3. At the same time testRun() is counting, print " TEST ABC " in main.

main.cpp

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

void print(const boost::system::error_code& /*e*/, boost::asio::deadline_timer* t, int* count)
{
  if (*count < 5)
  {
    std::cout << *count << std::endl;
    ++(*count);

    t->expires_at(t->expires_at() + boost::posix_time::seconds(1)); // every 1 second advance
    t->async_wait(boost::bind(print, boost::asio::placeholders::error, t, count));
  }
  std::cout << " PRINT " << std::endl;
}

void testRun()
{
  boost::asio::io_service io;

  int count = 0;
  boost::asio::deadline_timer t(io, boost::posix_time::seconds(2)); // start io object (function) after 2 seconds.
  t.async_wait(boost::bind(print, boost::asio::placeholders::error, &t, &count));

  io.run();
  std::cout << "Final count is " << count << std::endl;
}

int main()
{
  testRun();
  std::cout << " TEST ABC " << std::endl;
  return 0;
}

output

0
 PRINT 
1
 PRINT 
2
 PRINT 
3
 PRINT 
4
 PRINT 
 PRINT 
Final count is 5
 TEST ABC 

What I want my output to look like:

 TEST ABC 
0
 PRINT 
1
 PRINT 
2
 PRINT 
3
 PRINT 
4
 PRINT 
 PRINT 
Final count is 5


回答1:


To deconstruct the task at hand, I'll start with a bare-bones C++98 implementation.

We'll clean it up to be modern C++, and then replace with Asio.

You will see that Asio doesn't require threading, which is nice. But we have to work back in time, replacing modern C++ with C++98.

In the end you will see all the reasons to join modern C++, as well as how to organize your code in such a way that you can easily manage the complexity.

C++98

Here's how I'd write that in c++98:

Live On Coliru

#include <pthread.h>
#include <iostream>
#include <sstream>
#include <unistd.h>

static pthread_mutex_t s_mutex = {};
static bool s_running = true;

static bool is_running(bool newvalue) {
    pthread_mutex_lock(&s_mutex);
    bool snapshot = s_running;
    s_running = newvalue;
    pthread_mutex_unlock(&s_mutex);
    return snapshot;
}

static bool is_running() {
    pthread_mutex_lock(&s_mutex);
    bool snapshot = s_running;
    pthread_mutex_unlock(&s_mutex);
    return snapshot;
}

static void output(std::string const& msg) {
    pthread_mutex_lock(&s_mutex);
    std::cout << msg << "\n";
    pthread_mutex_unlock(&s_mutex);
}

static void* count_thread_func(void*) {
    for (int i = 0; i < 5; ++i) {
        ::sleep(1);
        std::ostringstream oss;
        oss << "COUNTER AT " << (i+1);
        output(oss.str());
    }
    is_running(false);
    return NULL;
}

int main() {
    pthread_t thr = {0};
    pthread_create(&thr, NULL, &count_thread_func, NULL);

    while (is_running()) {
        ::usleep(200000);
        output("TEST_ABC");
    }

    pthread_join(thr, NULL);
}

Prints

TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 1
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 2
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 3
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 4
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 5
TEST_ABC

C++11

Well the above really is hardly C++. It would actually be the "same" but more convenient in C with printf. Here's how C++11 improves things:

  • std::thread, std::atomic_bool, std::chono, std::this_thread, std::to_string, std::mutex/lock_guard, better initialization all around.

Live On Coliru

#include <thread>
#include <iostream>
#include <chrono>
#include <mutex>
#include <atomic>
using std::chrono::milliseconds;
using std::chrono::seconds;

static std::mutex s_mutex;
static std::atomic_bool s_running {true};

static void output(std::string const& msg) {
    std::lock_guard<std::mutex> lk(s_mutex);
    std::cout << msg << "\n";
}

static void count_thread_func() {
    for (int i = 0; i < 5; ++i) {
        std::this_thread::sleep_for(seconds(1));
        output("COUNTER AT " + std::to_string(i+1));
    }

    s_running = false;
}

int main() {
    std::thread th(count_thread_func);

    while (s_running) {
        std::this_thread::sleep_for(milliseconds(200));
        output("TEST_ABC");
    }

    th.join();
}

Same output, but much more legible. Also, many more guarantees. We could have detached the thread with just th.detach(), or passed any arguments we want to the thread function, instead of the void* dance.

C++17

C++14 adds some more (chrono literals), C++17 only marginally (fold expressions used here to have natural ostream-access):

Live On Coliru only. Note this is down to 35 LoC

Back To C++1x: ASIO

Translating into ASIO removes the need for threads altogether, replacing the sleep with asynchronous timers.

Because there is no threading, there doesn't have to be any locking, simplifying life.

We don't need a "running" flag, because we can stop the service or cancel timers if we need to.

The entire program boils down to:

Since we will have to tasks running on an interval, let's put the mechanics for that in a simple class, so we don't have to repeat it:

// simple wrapper that makes it easier to repeat on fixed intervals
struct interval_timer {
    interval_timer(boost::asio::io_context& io, Clock::duration i, Callback cb)
        : interval(i), callback(cb), timer(io)
    {}

    void run() {
        timer.expires_from_now(interval);
        timer.async_wait([=](error_code ec) {
            if (!ec && callback())
                run();
        });
    }

    void stop() {
        timer.cancel();
    }

  private:
    Clock::duration const interval; 
    Callback callback;
    boost::asio::high_resolution_timer timer;
};

That looks pretty self-explanatory to me. The whole program now boils down to only:

int main() {
    boost::asio::io_context io;

    interval_timer abc { io, 200ms, [] {
        std::cout << "TEST_ABC" << std::endl;
        return true;
    } };

    interval_timer counter { io, 1s, [&abc, current=0]() mutable {
        std::cout << "COUNTER AT " << ++current << std::endl;

        if (current < 5)
            return true;

        abc.stop();
        return false;
    } };

    abc.run();
    counter.run();

    io.run();
}

See it Live On Coliru.

We can simplify it a bit more if we use run_for to limit the execution (so we don't have to deal with exiting ourselves): Live On Coliru, down to 44 LoC

#include <boost/asio.hpp>
#include <iostream>
#include <chrono>
#include <functional>
using namespace std::chrono_literals;
using Clock = std::chrono::high_resolution_clock;
using Callback = std::function<void()>;
using boost::system::error_code;

// simple wrapper that makes it easier to repeat on fixed intervals
struct interval_timer {
    interval_timer(boost::asio::io_context& io, Clock::duration i, Callback cb)
        : interval(i), callback(cb), timer(io)
    { run(); }

  private:
    void run() {
        timer.expires_from_now(interval);
        timer.async_wait([=](error_code ec) {
            if (!ec) {
                callback();
                run();
            }
        });
    }

    Clock::duration const interval; 
    Callback callback;
    boost::asio::high_resolution_timer timer;
};

int main() {
    boost::asio::io_context io;

    interval_timer abc { io, 200ms, [] {
        std::cout << "TEST_ABC" << std::endl;
    } };

    interval_timer counter { io, 1s, [current=0]() mutable {
        std::cout << "COUNTER AT " << ++current << std::endl;
    } };

    io.run_for(5s);
}

Back to C++98

No lambda's. Okay, we can use boost::bind or just write some classes ourselves. You pick your poison, I chose a mixture:

  • boost::bind because it was the tool of that era (we're talking 20 years ago)
  • using virtual method instead of std::function for the callback.
  • The lambda captures have been replaced with explicit member variables.

It all becomes a lot less elegant, but basically recognizable as the same thing:

Live On Coliru

#include <boost/asio.hpp>
#include <iostream>
#include <boost/bind.hpp>
using boost::posix_time::seconds;
using boost::posix_time::millisec;
typedef boost::posix_time::microsec_clock Clock;
using boost::system::error_code;

// simple wrapper that makes it easier to repeat on fixed intervals
struct interval_timer {
    interval_timer(boost::asio::io_context& io, millisec i)
        : interval(i), timer(io)
    { run(); }

    virtual bool callback() = 0;

    void run() {
        timer.expires_from_now(interval);
        timer.async_wait(boost::bind(&interval_timer::on_timer, this, boost::asio::placeholders::error()));
    }

    void stop() {
        timer.cancel();
    }

  private:
    void on_timer(error_code ec) {
        if (!ec && callback())
            run();
    }
    millisec const interval; 
    boost::asio::deadline_timer timer;
};

int main() {
    boost::asio::io_context io;

    struct abc_timer : interval_timer {
        abc_timer(boost::asio::io_context& io, millisec i) : interval_timer(io, i) {}
        virtual bool callback() {
            std::cout << "TEST_ABC" << std::endl;
            return true;
        }
    } abc(io, millisec(200));

    struct counter_timer : interval_timer {
        counter_timer(boost::asio::io_context& io, millisec i, interval_timer& abc)
            : interval_timer(io, i), abc(abc), current(0) {}

        virtual bool callback() {
            std::cout << "COUNTER AT " << ++current << std::endl;

            if (current < 5)
                return true;

            abc.stop();
            return false;
        }
      private:
        interval_timer& abc;
        int current;
    } counter(io, millisec(1000), abc);

    io.run();
}

The output is still the same trusty

TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 1
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 2
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 3
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 4
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
TEST_ABC
COUNTER AT 5

The same transformation as earlier with run_for can be applied here as well, but we now have to link Boost Chrono because std::chrono didn't exist: Live On Coliru, still 56 LoC



来源:https://stackoverflow.com/questions/61334533/c-boost-asynchronous-timer-to-run-in-parallel-with-program

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!