I have a C++ project which needs a few periodic timers. I have created a timer class for this project using Grand Central Dispatch.
I tried using boost::asio
, but for some company policy reasons, I cannot link with said library.
Here's the header file with the interface:
//
// PeriodicTimer.hpp
//
#pragma once
#include <thread>
#include <mutex>
#include <memory>
#ifdef __OBJC__
#import <dispatch/dispatch.h>
#else
using dispatch_queue_t = void *;
using dispatch_source_t = void *;
#endif
namespace Test
{
#pragma mark -
#pragma mark TimerWorkloop
/**
@class TimerWorkloop
@brief a workloop to host all the timer events
*/
class TimerWorkloop
{
public:
using Context = dispatch_queue_t;
private:
Context mContext;
public:
/**
@function ctor
*/
TimerWorkloop();
/**
@function dtor
*/
~TimerWorkloop();
/**
@function getContext
@abstract the loop used by the thread for the io context
*/
Context getContext()
{ return mContext; }
};
#pragma mark -
#pragma mark PeriodicTimer
/**
@class PeriodicTimer
@brief a timer that gives a periodic asynchronous callback
@note please note that the callback issued by this class is on the TimerWorkloop thread, and if there is significant
work to be done in the callback, then the client should offload the work onto a separate thread
*/
class PeriodicTimer
{
public:
using CallbackFunc = std::function<void(PeriodicTimer *)>;
private:
using Timer = dispatch_source_t;
bool mIsActive;
uint32_t mPeriodMs;
CallbackFunc mCb;
std::shared_ptr<TimerWorkloop> mWl;
Timer mTimer;
std::mutex mLock;
public:
static constexpr uint32_t kMinPeriodMs = 5;
/**
@function ctor
*/
PeriodicTimer(std::shared_ptr<TimerWorkloop> inWl);
/**
@function dtor
*/
~PeriodicTimer();
/**
@function start
@param inPeriod the period for the timer
@param inCb the callback function that will be called
*/
void start(uint32_t inperiodMs, CallbackFunc inCb);
/**
@function stop
*/
void stop();
/**
@function getperiodMs
@return current period in milliseconds
*/
uint32_t getPeriodMs() const { return mPeriodMs; }
/**
@function isRunning
@return bool indicating whether the timer is running
*/
bool isRunning() const { return mIsActive; }
private:
/**
@function _timerHandler
@param inErrorCode the error code used to indicate whether the timer has expired or cancelled
*/
void _timerHandler();
PeriodicTimer(PeriodicTimer const &) = delete;
PeriodicTimer(PeriodicTimer &&) = delete;
};
}
And the implementation:
//
// PeriodicTimer.mm
//
#include "PeriodicTimer.hpp"
#include <stdexcept>
using namespace Test;
#pragma mark -
#pragma mark TimerWorkloop
TimerWorkloop::TimerWorkloop()
{
mContext = dispatch_queue_create("com.test.timerworkloop",
DISPATCH_QUEUE_SERIAL);
}
TimerWorkloop::~TimerWorkloop()
{
}
#pragma mark -
#pragma mark PeriodicTimer
PeriodicTimer::PeriodicTimer(std::shared_ptr<TimerWorkloop> inWl) :
mWl(inWl), mIsActive(false), mPeriodMs(0), mCb(nullptr)
{
}
PeriodicTimer::~PeriodicTimer()
{
if (mIsActive)
{
stop();
}
}
void
PeriodicTimer::start(uint32_t inPeriodMs, CallbackFunc inCb)
{
if (!mIsActive)
{
if (inPeriodMs < kMinPeriodMs)
{
throw std::runtime_error("Too short a period for the timer");
}
if (inCb == nullptr)
{
throw std::runtime_error("Empty timer callback");
}
mPeriodMs = inPeriodMs;
mCb = inCb;
mIsActive = true;
mTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mWl->getContext());
ThrowIfFalse(mTimer != nil, "Could not allocate a timer dispatch source");
dispatch_source_set_timer(mTimer, DISPATCH_TIME_NOW, inPeriodMs * NSEC_PER_MSEC,
NSEC_PER_MSEC);
dispatch_source_set_event_handler(mTimer, ^{ this->_timerHandler(); });
dispatch_resume(mTimer);
}
}
void
PeriodicTimer::stop()
{
if (mIsActive)
{
dispatch_source_cancel(mTimer);
mIsActive = false;
}
}
void
PeriodicTimer::_timerHandler()
{
if (mCb != nullptr)
{
mCb(this);
}
}
I have not used GCD before, and I am relatively new to Objective C as well. Please review my code.
EDIT: I did some performance measurements, and I am seeing some weird behavior:
The first time the timer fires, it fires too quickly, within <1ms of enabling, even if the period is quite high. In addition to this, even at some other times, the timer fires a bit sooner than expected (up to 5 ms sooner).
Why might this be?