Activation Timer -- a Simple Task Scheduler

Environment: VC++ 6/VC++.NET, Pure Win32 API

Overview

One of the most common problems in Win32 programming is dealing with a number of functions that must be executed each time the specified period expires. For example, you have three routines that must run:

function_1....every 14 seconds
function_2....every 11 seconds
function_3....every 90 seconds

A common solution involves several objects known as timers, which may seem fairly easy but cumbersome to use. This article introduces a simple yet powerful set of classes that greatly reduce the effort (and headache) required to handle multiple periodic jobs.

Algorithm

Another straightforward solution can be the following:

  • For each appended task, create a new thread.
  • Create a separate timer within every thread.
  • Use these timers in a common way.

This may seem great, but only if your system has enough resources to handle as many threads and tasks you wish to run. It is better to use a single timer and open a new thread only when your background task is really about to start. The modified algorithm is:

  • Create a timer.
  • After any task is appended:
    • Kill the timer.
    • Calculate a minimal timeout value = Greatest Common Measure of all timeout values.
    • Restart the timer.
  • Each time the timer activates, check whether any tasks are up to run.
  • If so, create a new thread(s) and run the task(s) in this thread(s).

This is nearly perfect, but one problem remains. If the task's execution period is less than the task's working time, a single task can start multiple threads; therefore, we must keep an eye on all of the threads to avoid resource leaks. In other words, we must implement some kind of garbage collection algorithm, which will track all thread objects and will close all unused handles.
The final algorithm is:

  • Create a timer.
  • After any task is appended:
    • Kill the timer.
    • Calculate a minimal timeout value = Greatest Common Measure of all timeout values.
    • Restart the timer.
  • Each time the timer activates, check whether any tasks are up to run.
  • For each ready-to-run task:
    • If a task is run for the first time, create a list of active threads, create and add a single thread object to this list.
    • If the thread list is not empty, append a new thread object to the end of the list.
    • Check all thread objects on the list, except of one recently appended, for completition of execution.
    • If any thread is not active, release its thread handle.

Advantages of this approach:

  • Minimal resource overhead.
  • Elimination of all memory and resource leaks.
  • Relatively high computational efficiency.
  • Ease of use (and debug)—only one timer to track.

Class Hierarchy

The given set consists of five classes; the higher class is always a manager-class (and an aggregate) for a subordinate class.
The actual hierarchy is:

   ActivationTimer --- manages ---> TaskList --- contains --->
   Tasks --- (each task) controls (its own) --->
   ThreadList --- handles ---> Threads

struct Thread       // Structure that incapsulates the
                    // thread handle.
{
    // Only Thread list is allowed to create/close threads:
    friend class ThreadList;

protected:

    // Active thread handle:
    HANDLE ThreadHandle;

    Thread* Next;           // Next thread in a list.
    Thread* Prev;           // Previous thread in a list.

    // Construction / destruction:
             Thread(HANDLE hNewThreadHandle);
    virtual ~Thread();
};

class ThreadList    // Double-linked list of thread objects.
{
    // Only task is allowed to handle ThreadList:
    friend class Task;

protected:

    Thread* Begin;          // Head of list
    Thread* End;            // Tail of list
    int     NumOfThreads;   // Number of threads in a list.

    // Construction / destruction:
             ThreadList(HANDLE hNewThreadHandle = NULL);
    virtual ~ThreadList();

    // Add a new thread to the list:
    int AppendThread(HANDLE hNewThreadHandle);

    // Get the pointer to a 'num'th thread object:
    Thread* operator[] (const int num) const;

    // Determine whether 'num'th thread is active or not:
    bool IsThreadActive(const int num) const;

    // Close the 'num'th thread object:
    int CloseThreadObject(const int num);

    // Get the number of threads in a list:
    int GetNumOfThreads() const;
};

class Task      // Class that incapsulates task data.
{
    // No one is allowed to create and handle Task except for
    // TaskList:
    friend class TaskList;

protected:

    // Time (in milliseconds), at which task is periodically
    // performed:
    unsigned long timeout;

    void*         pFunc;            // Pointer to the task
                                    // (i.e.: function).
    void*         pParameter;       // Pointer to a *structure*
                                    // parameter of task.
    ThreadList*   listOfThreads;    // List of active threads
                                    // opened by a task.

    Task* Next;                     // Pointer to the next task
    Task* Prev;                     // Pointer to the previous task

    // Construction / destruction:
             Task(const unsigned long msTimeout,
                  void* pNewFunc, void* pParam);
    virtual ~Task();

    void Execute();                 // Call a task AND clean the
                                    // threadlist.
};

class TaskList               // Double-linked list of
                             // task data
{
    // No one is allowed to create and handle TaskList except
    // for ActivationTimer:
    friend class ActivationTimer;

protected:

    Task* Begin;        // The very first Task.
    Task* End;          // The last Task.

    int NumOfTasks;     // Number of Tasks in the list.

    // Construction / destruction:
             TaskList(const unsigned long msTimeout = 0,
                      void* pNewFunc = NULL,
                      void* pParam = NULL);
    virtual ~TaskList();

    // Retrieve a pointer to the 'num'th task:
    Task* operator[] (const int num) const;

    // Get the number of tasks:
    int  GetNumOfTasks () const;

    // Get the timeout value for the 'num'-th  task:
    unsigned long GetTaskTimeout(const int num) const;

    // Add a task:
    int  AppendTask(const unsigned long msTimeout, 
                    void* pNewFunc, void* pParam);

    // Remove a task from the list:
    int  DeleteTask(const int num);

    // Call a task and create an individual thread for it:
    void CallTask  (const int num);
};

class ActivationTimer     // A simple task scheduler -
                          // designed as a singleton.
{
protected:

    static TaskList* listOfTasks;       // Pointer to the list
                                        // of tasks.

    // Minimal timeout - timer checks tasks for execution every
    // 'minTimeout' milliseconds:
    static unsigned long minTimeout;
    static unsigned long maxTimeout;    // Maximum timeout in
                                        // the list.
    static unsigned long curTimeout;    // Current time.
    static unsigned int  TimerId;       // ID of the internal
                                        // timer.

    // Check if it is a time to call a task:
    static void ExecAppropriateFunc();

    // Calculate 'minTimeout' value.
           void RecalcTimerInterval();

    static void CALLBACK TimerProc(HWND, UINT, UINT, DWORD);

public:

    // Construction / destruction:
             ActivationTimer(const unsigned long msTimeout = 0,
                             void* pNewFunc = NULL,
                             void* pParam = NULL);
    virtual ~ActivationTimer();

    // Add a single task:
    int AddTask(const unsigned long msTimeout, void* pNewFunc,
                void* pParam = NULL);

    // Remove a task:
    int RemoveTask(const int cpPos);

    // Get the number of active tasks:
    int GetNumOfTasks() const;

    // Stop the timer:
    void Halt();

    // Restart the timer:
    void Restart();

    // Deallocate a singleton object and
    // reallocate it once again.
    void Reset();
};

Note 1: As you can see, two double-linked lists (the list of tasks and the list of threads for every task) form the foundation of this set of classes. The question may arise: Why not use STL's <list> container template class? The answer is: STL's <list> brings not only the <list>, but also a HUGE amount of unnecessary code, which slows and enlarges the application. So I've decided to use the most simple (and the most efficient, in our case!) "hand-made" implementation of a double-linked list.

Note 2: Another point of interest—the core class, ActivationTimer, which is implemented as a Singleton. A singleton is a class that assures existence of a maximum of one object of its type at a given time, and provides a global access point to this object. This article gives a comprehensive overview of a singleton design pattern.

Note 3: The real cause of ActivationTimer being a singleton is a stupid limitation of Win32 API: the timer callback procedure, TimerProc, is the only callback function in Win32 that does not take any user-defined argument. This behavior can be overridden in two ways:

  • By using not an ordinary timer (SetTimer / KillTimer), but a special synchronization object—waitable timer
    (CreateWaitableTimer / SetWaitableTimer / CancelWaitableTimer).
    Unfortunately, this approach produces significant performance overhead.
  • By using a special synchronization object, which is new to VC++ 7 - timer-queue timer
    (CreateTimerQueue / CreateTimerQueueTimer / DeleteTimerQueueEx / DeleteTimerQueueTimer).
    Timer-queue timers are lightweight objects that enable you to specify a callback function to be called at a specified time or a period of time. These objects are new to VC++ 7, and I still don't have the ability to implement them, so ... their time is yet to come.

Implementation

Note 4: For better understanding, only member-functions that have a direct influence on the algorithm will be explained here. The source code contains detailed comments on every function.

Step by step:

  • Create a timer:
  • // Create a timer with one task in the task list:
    ActivationTimer::ActivationTimer(const unsigned long msTimeout,
                                     void* pNewFunc, void* pParam)
    {
        if((msTimeout <= 0UL) || !pNewFunc)
        {
            listOfTasks = new TaskList();
            TimerId     = 0;
            minTimeout  = 0;
            maxTimeout  = 0;
            curTimeout  = 0;
    
            return;
        }
    
        listOfTasks = new TaskList(msTimeout, pNewFunc, pParam);
        minTimeout  = msTimeout;
        maxTimeout  = msTimeout;
        curTimeout  = msTimeout;
    
        Restart();
    }
    
    // Restart the timer:
    void ActivationTimer::Restart()
    {
        TimerId = SetTimer(NULL, NULL, minTimeout, TimerProc);
    }
    
  • Append a task:
  • // Add a new task to the task list.
    // Return value: zero-based position of task in a task list.
    int ActivationTimer::AddTask(const unsigned long msTimeout,
                                 void* pNewFunc, void* pParam)
    {
        // A little bit of parameter validation:
        if((msTimeout <= 0UL) || !pNewFunc)
            return -1;
    
        // Kill the timer:
        Halt();
    
        // Append a new task:
        int pos = listOfTasks->AppendTask(msTimeout,
                                          pNewFunc, pParam);
    
        // Recalculate 'maxTimeout' value:
        maxTimeout = maxTimeout < msTimeout ?
                                  msTimeout : maxTimeout;
    
        // Recalculate 'minTimeout' value:
        RecalcTimerInterval();
    
        // Recreate the timer:
        Restart();
    
        return pos;
    }
    
  • After any task is appended—calculate minTimeout value:
  • // Calculate the value of minTimeout.
    // This is a time when 'ExecAppropriateFunc()' routine is
    // periodically called.
    void ActivationTimer::RecalcTimerInterval()
    {
        minTimeout = listOfTasks->GetTaskTimeout(0);
        for(int i = 0; i < listOfTasks->NumOfTasks; i++)
        {
            unsigned long tempTimeout = GCM(minTimeout,
                                            listOfTasks->
                                            GetTaskTimeout(i));
                           minTimeout = minTimeout >
                                        tempTimeout ?
                                        tempTimeout : minTimeout;
        }
    
        curTimeout = minTimeout;
    }
    
  • Each time timer activates, check if any tasks are up to run:
  • // Check whether it is a time to call one of the tasks from
    // the task list.
    void ActivationTimer::ExecAppropriateFunc()
    {
        bool reset = true;
        for(int i = 0; i < listOfTasks->NumOfTasks; i++)
        {
            // If time has come, call the 'i'th task:
            if((curTimeout % listOfTasks->GetTaskTimeout(i)) == 0)
            {
                listOfTasks->CallTask(i);
                reset &= true;
            }
            else
            {
                reset &= false;
            }
        }
    
        // If all tasks are called simultaneously,
        // reset curTimeout value:
        if(reset)
            curTimeout  = minTimeout;
        else
            curTimeout += minTimeout;
    }
    
  • If so, execute these task(s):
  • // Call a task and create an individual thread for it:
    void TaskList::CallTask(const int num)
    {
        Task* task = operator[](num);
        task->Execute();
    }
    
    // Create an individual thread for a task:
    void Task::Execute()
    {
        listOfThreads->AppendThread(CreateThread(NULL, 0,
                                   (LPTHREAD_START_ROUTINE)pFunc,
                                    pParameter, 0, 0));
    
        // ...and wipe out all unused thread handles:
        int numThreads = listOfThreads->GetNumOfThreads();
        for(int i = 0; i < numThreads - 1; i++)
        {
            if(!(listOfThreads->IsThreadActive(i)))
                 listOfThreads->CloseThreadObject(i);
        }
    }
    
  • On exit, free all the resources, close all thread handles:
  • Task::~Task()
    {
        delete listOfThreads;
    }
    
    ThreadList::~ThreadList()
    {
        Thread* target = Begin;
        Thread* temp;
    
        if(target)      // If ThreadList is not empty...
        while(target)
        {
            temp = target->Next;
    
            delete target;
    
            target = temp;
        }
    }
    
    Thread::~Thread()
    {
        // Check if thread has exited.
        // If it did not - terminate it.
        DWORD exitCode;
        GetExitCodeThread(ThreadHandle, &exitCode);
    
        // !!! UNSAFE CODE !!!
        // Try not to use lengthy operations OR
        // use some kind of internal completition
        // flag to explicitly instruct thread to exit.
        // Refer MSDN on hazards of TerminateThread.
        if(exitCode == STILL_ACTIVE)
            TerminateThread(ThreadHandle, 0);
    
        CloseHandle(ThreadHandle);
    }
    

Note 5: The Thread::~Thread destructor needs some explanation. Calling TerminateThread can be quite dangerous if the target thread is still running, because:

  • If the target thread is allocating memory from the heap, the heap lock will not be released.
  • If the target thread is executing certain kernel32 calls when it is terminated, the kernel32 state for the thread's process could be inconsistent.
  • If the target thread is manipulating the global state of a shared DLL, the state of the DLL could be destroyed, affecting other users of the DLL.

A possible solution may be one of the following:

  • Do not use lengthy operations as a thread function.
  • Use a special bool variable (something like: m_bStopExecution), and periodically check the state of this variable.
    If this variable is true, the thread can gracefully quit.
  • Use a synchronization object—for example, semaphore or waitable timer—to instruct a thread to stop execution (just the same as above).

Please refer to MSDN for more information on TerminateThread and synchronization objects.

Usage

To use ActivationTimer, do the following:

  • Include "ActivationTimer.cpp" and "ActivationTimer.h" in your project.
  • Write the following line at the beginning of the "StdAfx.h" file:
  •     #include "ActivationTimer.h"
  • Write the following line at the beginning of the "ActivationTimer.cpp" file:
  •     #include "StdAfx.h"
  • Within the main file of your application, declare:
  •     ActivationTimer actTimer;
  • To add a task with no parameters, write:
  •     void MyFunction()
        {
          ....
        }
    
        ....
    
        actTimer.AddTask(10000, MyFunction);
    
    This will periodically execute MyFunction every 10 seconds.

  • To add a task with a single parameter, declare:
  •     void MyFunction2(void* pParam)
        {
          int* value = (int*)pParam;
    
          ....
        }
    
        ....
    
        int someValue = ... ;
    
        actTimer.AddTask(15000, MyFunction2, &someValue);
    
  • If you wish to pass several parameters to your task-function, declare a structure:
  •     typedef struct
        {
           int   valueA;
           float valueB;
           bool  valueC;
    
           ...
        } MyStruct;
    
    and write:
        void MyFunction3(void* pParam)
        {
          MyStruct* structVal = (MyStruct*)pParam;
    
          ....
        }
    
        ....
    
        MyStruct* somestruct = new MyStruct;
    
        ...    // initialize struct here
    
        actTimer.AddTask(5100, MyFunction3, somestruct);
    

The demo project (see link below) is an ordinary "Hello, World!" application, built with AppWizard, with these examples included. Enjoy!

Acknowledgements

Thank you all the CodeGuru visitors who posted comments, and double-Thank-you to everyone who wrote me, reporting bugs, ideas, and suggestions!

P.S.

Why not a Task Scheduler instead of the Activation Timer? That's because Windows already has its Task Scheduler application, so I've decided not to mess up. At least, Activation Timer application is not included in a standard Windows package, yet........

Downloads

Download demo project - 12 Kb
Download source - 5 Kb


Comments

  • ...and the problem with Win32 timers?

    Posted by Legacy on 09/09/2003 12:00am

    Originally posted by: Naldo

    What about the poor precision of Win32 timers and its low priority message in the queue? this would be don't throw the timer event, right?

    Thx in advance,

    Naldo

    Reply
  • Why not use built-in Windows schedule ?

    Posted by Legacy on 07/07/2003 12:00am

    Originally posted by: Florian DREVET

    All is in the title :-)

    Reply
  • Ideas

    Posted by Legacy on 06/30/2003 12:00am

    Originally posted by: Jedi

    I have had a similar situation where I needed to retrieve data from multiple entities at the same time. So I have put a timer in a service and each time it wakes up, it retrieves data in multiple threads. But sometimes data retrieval of these threads goes beyond the timer limit and then everything collapses.
    Any design suggestions? Any patterns that you know of?
    Any idea how to configure multiple instances of the service to distribute load?

    Reply
  • BUGFIX

    Posted by Legacy on 06/27/2003 12:00am

    Originally posted by: Dmitry Khudorozhkov

    Instead of:
    
    

    void ActivationTimer::ExecAppropriateFunc()
    {
    for(int i = 0; i < listOfTasks->NumOfTasks; i++)
    {
    if((listOfTasks->GetTaskTimeout(i) == curTimeout)
    || // or
    ((curTimeout % listOfTasks->GetTaskTimeout(i)) == 0))
    listOfTasks->CallTask(i);
    }

    curTimeout += minTimeout;

    if(curTimeout > maxTimeout)
    curTimeout = minTimeout;
    }

    there must be:

    void ActivationTimer::ExecAppropriateFunc()
    {
    bool reset = false;
    for(int i = 0; i < listOfTasks->NumOfTasks; i++)
    {
    if((curTimeout % listOfTasks->GetTaskTimeout(i)) == 0)
    {
    listOfTasks->CallTask(i);
    reset = true;
    }
    else
    {
    reset = false;
    }
    }

    if(reset)
    curTimeout = minTimeout;
    else
    curTimeout += minTimeout;
    }

    Reply
  • C/C++ & OOP design

    Posted by Legacy on 06/25/2003 12:00am

    Originally posted by: no mather

    wow, what example of poor C/C++ & OOP design understandings ...
    any questions:
    what exactly means paramerer specification of 'void f(const unsigned int msTimeout)" ( especialy what means 'const' )?
    and with this funct declaration, what exactly checks next statement: if(msTimeout <= 0) ( especialy '<' )?
    why 'void CALLBACK TimerProc(...)' is not a static member of ActivationTimer?
    what about functors as task objects?
    and what about any params for specific task?
    why 'friend' anywhere?
    what about Double-linked list ws std::vector?
    may be ActivationTimer MUST be a singleton?
    let stop with questions.
    at another side, goot work. don't stop here. here always well have nonadvanced developers, that will search for something like this.

    enjoy

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Wednesday, September 24, 2014 8:00 AM - 9:00 AM PDT According to a recent Forrester Research report, many companies are choosing low-code platforms over traditional programming platforms, due to the speed with which low-code apps can be assembled and tested. With customer-facing applications on the rise, traditional programming platforms simply can't keep up with the "short schedules and rapid change cycles" required to develop these applications. Check out this upcoming webinar and join Clay Richardson from …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds