timerpool

simple timerpool implementation for uncritical event-purposes. The "tick" is an abstract value and depends on the selected timebase and the environment

Its useful if you need wakeup-timers for protocol implementations or you like to calculate/interpolate something for a given timeslot

For each TimerPool object only one tickthread is spawned which handles the message-queue and the lifecycle of the TimerHandle. The maximum amount of timers is only constrained by memory and the given timebase.

The allocation of a new TimerHandle always block but is threadsafe. The maximum blocking-time relates directly to your given timebase of the pool

There is a blocking and a nonblocking API on the TimerHandles which can be used simulataneously from different threads at once. All actions on the TimerHandles are completely threadsafe and the ptrs itself can be shared between threads.

the following example demonstrates the basic use. For detailed api use and for multithreading examples use the tests as a starter.

import timerpool

let
  tpRef = timerpool.newTimerPool(10.int) # timerpool with 10ms timebase
  timerhdl = allocTimer(tpRef)

timerhdl.setAlarmCounter(5)  # set expiration to 50ms (timebase * 5)

while timerhdl.getAlarmCounter() > 0: # you can poll it
  discard

timerhdl.waitForAlarm()     # or sleep till timer expired
timerhdl.deallocTimer()     # pushes the timer back to pool
tpRef.shutdownTimerPool()   # shutdown the pool and blocks till all
                            # timers are expired

Types

TimerHandlePtr = ptr TimerHandle
pointer type to the timerpoolhandle.
TPError = object of Exception
  
generic exception
TimerPoolPtr = ptr TimerPool
used to share among threads
TimerPoolRef = ref TimerPool
Tickval = range[1 .. int.high]
MinTimerval = range[1 .. int.high]
integer type used to initialise the timerpool and to set the timeout of the timer
PoolStats = tuple[runningCount: int, freedCount: int, inactiveCount: int]
container type returned by waitForGetStats. the sum of runningCount,freedCount and inactiveCount is the total amount of timerhandles within the pool

Procs

proc initThreadContext(tpptr: TimerPoolPtr): void {...}{.raises: [TPError], tags: [].}

to be called explicit if the pool-accessing thread is not the owner of the timerpool (initialises threadvar globs)

raises a TPError if called within the spawning thread

proc newTimerPool(tbase_ms: Tickval = 100; minFreedTimers: MinTimerval = 5): ref TimerPool {...}{.
    raises: [], tags: [].}
creator proc. The tickval is of milliseconds and the default timebase is 100 milliseconds the default of the mintimers parameter is 5 (shrink_pool leave this minimum amount of freed timers within the pool)
proc deinitThreadContext(tpptr: TimerPoolPtr): void {...}{.raises: [TPError], tags: [].}

call this proc if the pool-accessing thread should be detached from the timerpool (cleanup threadvar globs)

call this proc only if the current thread is not owner of the timerpool. if not a TPError is raised

proc allocTimer(tpptr: TimerPoolPtr): TimerHandlePtr {...}{.raises: [TPError], tags: [].}

returns a timerhandle. the timer is always of type:oneshot but could also act as a continous one. in this case the caller needs to reset the alarm to the needed value. This threadsafe call blocks till the request was handled by the pool-tick-thread

before calling (if the pool was not spawned by the calling thread) initThreadContext() should be called

raises TPError if the pointer parameter is nil and/or the threadContext was not initialised with initThreadContext

proc allocTimer(tpptr: TimerPoolRef): TimerHandlePtr {...}{.inline, raises: [TPError],
    tags: [].}
proc deallocTimer(timerhdl: TimerHandlePtr): void {...}{.raises: [TPError], tags: [].}

the timer handle is pushed back to the pool. once freed it is not handled by the timerscan any more and its recycled for later use

this proc could be called from multiple threads simultaneously. if one ore more threads are waiting on the timers signal all threads gets informed. This call is part of the nonblocking api

raises TPError if the pointer parameter is nil

proc setAlarmCounter(timerhdl: TimerHandlePtr; value: Tickval): void {...}{.
    raises: [TPError], tags: [].}

sets the timers countdown alarm-value to the given one. reset the counter after it´s fired to obtain a continous timer

this call is threadsafe and part of the nonblocking-api

raises TPError if the pointer parameter is nil or the timer is freed

proc getAlarmCounter(timerhdl: TimerHandlePtr): int {...}{.raises: [TPError], tags: [].}

returns the current value of the alarmcounter could be used for a polling-style-waiting_for_timer_fired

this call is threadsafe and part of the nonblocking-api

raises TPError if the pointer parameter is nil or the timer already freed

proc waitForAlarm(timerhdl: TimerHandlePtr): void {...}{.raises: [TPError], tags: [].}

blocking wait till the alarmcounter is decremented to 0

threadsafe impl and could be called by multiple threads simultaniously

raises TPError if the pointer parameter is nil or the timer already freed

proc waitForGetStats(tpptr: TimerPoolPtr): PoolStats {...}{.raises: [TPError], tags: [].}

fetches some pool statistics for debugging purposes

raises TPError if the pointer parameter is nil or the threadContext was not initialized with initThreadContext

proc shrinkTimerPool(tpptr: TimerPoolPtr) {...}{.raises: [TPError], tags: [].}

shrinks the pool of freed Timers. the given minFreedTimers value at pool construction specifies the lower watermark

this is a nonblocking call. raises TPError if the pointer parameter is nil and/or the threadContext was not initialised with initThreadContext (only needed if the pool was not spawned by the caller)

Templates

template checkForNil(timerhdl: TimerHandlePtr; callingProc: string = ""): void
checks if the timerhdl is nil. if so a TPError is raised
template poolRef2Ptr(stpp: TimerPoolRef): TimerPoolPtr
convenience template to get the TimerPoolPtr from the ref