diff src/timer/SDL_timer.c @ 5113:481dabb098ef

Improved timer implementation The new timer model is formalized as using a separate thread to handle timer callbacks. This was the case on almost every platform before, but it's now a requirement, and simplifies the implementation and makes it perform consistently across platforms. Goals: * Minimize timer thread blocking * Dispatch timers as accurately as possible * SDL_AddTimer() and SDL_RemoveTimer() are completely threadsafe * SDL_RemoveTimer() doesn't crash with a timer that's expired or removed
author Sam Lantinga <slouken@libsdl.org>
date Thu, 27 Jan 2011 14:45:06 -0800
parents 906d7293bb47
children e337f792c6a7
line wrap: on
line diff
--- a/src/timer/SDL_timer.c	Thu Jan 27 10:40:17 2011 -0800
+++ b/src/timer/SDL_timer.c	Thu Jan 27 14:45:06 2011 -0800
@@ -23,289 +23,360 @@
 
 #include "SDL_timer.h"
 #include "SDL_timer_c.h"
-#include "SDL_mutex.h"
-#include "SDL_systimer.h"
+#include "SDL_atomic.h"
+#include "SDL_thread.h"
 
 /* #define DEBUG_TIMERS */
 
-int SDL_timer_started = 0;
-int SDL_timer_running = 0;
+typedef struct _SDL_Timer
+{
+    int timerID;
+    SDL_TimerCallback callback;
+    void *param;
+    Uint32 interval;
+    Uint32 scheduled;
+    volatile SDL_bool canceled;
+    struct _SDL_Timer *next;
+} SDL_Timer;
+
+typedef struct _SDL_TimerMap
+{
+    int timerID;
+    SDL_Timer *timer;
+    struct _SDL_TimerMap *next;
+} SDL_TimerMap;
 
-/* Data to handle a single periodic alarm */
-Uint32 SDL_alarm_interval = 0;
-SDL_TimerCallback SDL_alarm_callback;
+/* A reasonable guess */
+#define CACHELINE_SIZE  128
+
+/* The timers are kept in a sorted list */
+typedef struct {
+    /* Data used by the main thread */
+    SDL_Thread *thread;
+    SDL_atomic_t nextID;
+    SDL_TimerMap *timermap;
+    SDL_mutex *timermap_lock;
+
+    /* Padding to separate cache lines between threads */
+    char pad[CACHELINE_SIZE];
+
+    /* Data used to communicate with the timer thread */
+    SDL_SpinLock lock;
+    SDL_sem *sem;
+    SDL_Timer * volatile pending;
+    SDL_Timer * volatile freelist;
+    volatile SDL_bool active;
 
-/* Data used for a thread-based timer */
-static int SDL_timer_threaded = 0;
+    /* List of timers - this is only touched by the timer thread */
+    SDL_Timer *timers;
+} SDL_TimerData;
+
+static SDL_TimerData SDL_timer_data;
+
 
-struct _SDL_TimerID
+/* The idea here is that any thread might add a timer, but a single
+ * thread manages the active timer queue, sorted by scheduling time.
+ *
+ * Timers are removed by simply setting a canceled flag
+ */
+
+static void
+SDL_AddTimerInternal(SDL_TimerData *data, SDL_Timer *timer)
 {
-    Uint32 interval;
-    SDL_NewTimerCallback cb;
-    void *param;
-    Uint32 last_alarm;
-    struct _SDL_TimerID *next;
-};
+    SDL_Timer *prev, *curr;
+
+    prev = NULL;
+    for (curr = data->timers; curr; prev = curr, curr = curr->next) {
+        if ((Sint32)(timer->scheduled-curr->scheduled) < 0) {
+            break;
+        }
+    }
+
+    /* Insert the timer here! */
+    if (prev) {
+        prev->next = timer;
+    } else {
+        data->timers = timer;
+    }
+    timer->next = curr;
+}
+
+static int
+SDL_TimerThread(void *_data)
+{
+    SDL_TimerData *data = (SDL_TimerData *)_data;
+    SDL_Timer *pending;
+    SDL_Timer *current;
+    SDL_Timer *freelist_head = NULL;
+    SDL_Timer *freelist_tail = NULL;
+    Uint32 tick, now, interval, delay;
 
-static SDL_TimerID SDL_timers = NULL;
-static SDL_mutex *SDL_timer_mutex;
-static volatile SDL_bool list_changed = SDL_FALSE;
+    /* Threaded timer loop:
+     *  1. Queue timers added by other threads
+     *  2. Handle any timers that should dispatch this cycle
+     *  3. Wait until next dispatch time or new timer arrives
+     */
+    for ( ; ; ) {
+        /* Pending and freelist maintenance */
+        SDL_AtomicLock(&data->lock);
+        {
+            /* Get any timers ready to be queued */
+            pending = data->pending;
+            data->pending = NULL;
+
+            /* Make any unused timer structures available */
+            if (freelist_head) {
+                freelist_tail->next = data->freelist;
+                data->freelist = freelist_head;
+            }
+        }
+        SDL_AtomicUnlock(&data->lock);
+
+        /* Sort the pending timers into our list */
+        while (pending) {
+            current = pending;
+            pending = pending->next;
+            SDL_AddTimerInternal(data, current);
+        }
+        freelist_head = NULL;
+        freelist_tail = NULL;
+
+        /* Check to see if we're still running, after maintenance */
+        if (!data->active) {
+            break;
+        }
+
+        /* Initial delay if there are no timers */
+        delay = SDL_MUTEX_MAXWAIT;
+
+        tick = SDL_GetTicks();
+
+        /* Process all the pending timers for this tick */
+        while (data->timers) {
+            current = data->timers;
 
-/* Set whether or not the timer should use a thread.
-   This should not be called while the timer subsystem is running.
-*/
-int
-SDL_SetTimerThreaded(int value)
-{
-    int retval;
+            if ((Sint32)(tick-current->scheduled) < 0) {
+                /* Scheduled for the future, wait a bit */
+                delay = (current->scheduled - tick);
+                break;
+            }
+
+            /* We're going to do something with this timer */
+            data->timers = current->next;
+
+            if (current->canceled) {
+                interval = 0;
+            } else {
+                interval = current->callback(current->interval, current->param);
+            }
 
-    if (SDL_timer_started) {
-        SDL_SetError("Timer already initialized");
-        retval = -1;
-    } else {
-        retval = 0;
-        SDL_timer_threaded = value;
+            if (interval > 0) {
+                /* Reschedule this timer */
+                current->scheduled = tick + interval;
+                SDL_AddTimerInternal(data, current);
+            } else {
+                if (!freelist_head) {
+                    freelist_head = current;
+                }
+                if (freelist_tail) {
+                    freelist_tail->next = current;
+                }
+                freelist_tail = current;
+
+                current->canceled = SDL_TRUE;
+            }
+        }
+
+        /* Adjust the delay based on processing time */
+        now = SDL_GetTicks();
+        interval = (now - tick);
+        if (interval > delay) {
+            delay = 0;
+        } else {
+            delay -= interval;
+        }
+
+        /* Note that each time a timer is added, this will return
+           immediately, but we process the timers added all at once.
+           That's okay, it just means we run through the loop a few
+           extra times.
+         */
+        SDL_SemWaitTimeout(data->sem, delay);
     }
-    return retval;
+    return 0;
 }
 
 int
 SDL_TimerInit(void)
 {
-    int retval;
+    SDL_TimerData *data = &SDL_timer_data;
+
+    if (!data->active) {
+        data->timermap_lock = SDL_CreateMutex();
+        if (!data->timermap_lock) {
+            return -1;
+        }
 
-    retval = 0;
-    if (SDL_timer_started) {
-        SDL_TimerQuit();
+        data->sem = SDL_CreateSemaphore(0);
+        if (!data->sem) {
+            SDL_DestroyMutex(data->timermap_lock);
+            return -1;
+        }
+
+        data->active = SDL_TRUE;
+        data->thread = SDL_CreateThread(SDL_TimerThread, data);
+        if (!data->thread) {
+            SDL_TimerQuit();
+            return -1;
+        }
+
+        SDL_AtomicSet(&data->nextID, 1);
     }
-    if (!SDL_timer_threaded) {
-        retval = SDL_SYS_TimerInit();
-    }
-    if (SDL_timer_threaded) {
-        SDL_timer_mutex = SDL_CreateMutex();
-    }
-    if (retval == 0) {
-        SDL_timer_started = 1;
-    }
-    return (retval);
+    return 0;
 }
 
 void
 SDL_TimerQuit(void)
 {
-    SDL_SetTimer(0, NULL);
-    if (SDL_timer_threaded < 2) {
-        SDL_SYS_TimerQuit();
-    }
-    if (SDL_timer_threaded) {
-        SDL_DestroyMutex(SDL_timer_mutex);
-        SDL_timer_mutex = NULL;
-    }
-    SDL_timer_started = 0;
-    SDL_timer_threaded = 0;
-}
+    SDL_TimerData *data = &SDL_timer_data;
+    SDL_Timer *timer;
+    SDL_TimerMap *entry;
+
+    if (data->active) {
+        data->active = SDL_FALSE;
 
-void
-SDL_ThreadedTimerCheck(void)
-{
-    Uint32 now, ms;
-    SDL_TimerID t, prev, next;
-    SDL_bool removed;
-
-    SDL_mutexP(SDL_timer_mutex);
+        /* Shutdown the timer thread */
+        if (data->thread) {
+            SDL_SemPost(data->sem);
+            SDL_WaitThread(data->thread, NULL);
+            data->thread = NULL;
+        }
 
-    now = SDL_GetTicks();
-    do {
-        list_changed = SDL_FALSE;
-        for (prev = NULL, t = SDL_timers; t; t = next) {
-            removed = SDL_FALSE;
-            ms = t->interval - SDL_TIMESLICE;
-            next = t->next;
-            if ((int) (now - t->last_alarm) > (int) ms) {
-                struct _SDL_TimerID timer;
+        SDL_DestroySemaphore(data->sem);
+        data->sem = NULL;
 
-                if ((now - t->last_alarm) < t->interval) {
-                    t->last_alarm += t->interval;
-                } else {
-                    t->last_alarm = now;
-                }
-#ifdef DEBUG_TIMERS
-                printf("Executing timer %p (thread = %lu)\n",
-                       t, SDL_ThreadID());
-#endif
-                timer = *t;
-                SDL_mutexV(SDL_timer_mutex);
-                ms = timer.cb(timer.interval, timer.param);
-                SDL_mutexP(SDL_timer_mutex);
-                if (list_changed) {
-                    next = t->next;
-                    for (prev = SDL_timers; prev; prev = prev->next) {
-                        if (prev->next == t)
-                            break;
-                    }
-                }
-                if (ms != t->interval) {
-                    if (ms) {
-                        t->interval = ROUND_RESOLUTION(ms);
-                    } else {
-                        /* Remove timer from the list */
-#ifdef DEBUG_TIMERS
-                        printf("SDL: Removing timer %p\n", t);
-#endif
-                        if (prev) {
-                            prev->next = next;
-                        } else {
-                            SDL_timers = next;
-                        }
-                        SDL_free(t);
-                        --SDL_timer_running;
-                        removed = SDL_TRUE;
-                    }
-                }
-                if (list_changed) {
-                    /* Abort, list of timers modified */
-                    break;
-                }
-            }
-            /* Don't update prev if the timer has disappeared */
-            if (!removed) {
-                prev = t;
-            }
+        /* Clean up the timer entries */
+        while (data->timers) {
+            timer = data->timers;
+            data->timers = timer->next;
+            SDL_free(timer);
         }
-    } while (list_changed);
-
-    SDL_mutexV(SDL_timer_mutex);
-}
+        while (data->freelist) {
+            timer = data->freelist;
+            data->freelist = timer->next;
+            SDL_free(timer);
+        }
+        while (data->timermap) {
+            entry = data->timermap;
+            data->timermap = entry->next;
+            SDL_free(entry);
+        }
 
-static SDL_TimerID
-SDL_AddTimerInternal(Uint32 interval, SDL_NewTimerCallback callback,
-                     void *param)
-{
-    SDL_TimerID t;
-    t = (SDL_TimerID) SDL_malloc(sizeof(struct _SDL_TimerID));
-    if (t) {
-        t->interval = ROUND_RESOLUTION(interval);
-        t->cb = callback;
-        t->param = param;
-        t->last_alarm = SDL_GetTicks();
-        t->next = SDL_timers;
-        SDL_timers = t;
-        ++SDL_timer_running;
-        list_changed = SDL_TRUE;
+        SDL_DestroyMutex(data->timermap_lock);
+        data->timermap_lock = NULL;
     }
-#ifdef DEBUG_TIMERS
-    printf("SDL_AddTimer(%d) = %08x num_timers = %d\n", interval, (Uint32) t,
-           SDL_timer_running);
-#endif
-    return t;
 }
 
 SDL_TimerID
-SDL_AddTimer(Uint32 interval, SDL_NewTimerCallback callback, void *param)
+SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *param)
 {
-    SDL_TimerID t;
-    if (!SDL_timer_mutex) {
-        if (SDL_timer_started) {
-            SDL_SetError("This platform doesn't support multiple timers");
-        } else {
-            SDL_SetError("You must call SDL_Init(SDL_INIT_TIMER) first");
+    SDL_TimerData *data = &SDL_timer_data;
+    SDL_Timer *timer;
+    SDL_TimerMap *entry;
+
+    if (!data->active) {
+        int status = 0;
+
+        SDL_AtomicLock(&data->lock);
+        if (!data->active) {
+            status = SDL_TimerInit();
+        }
+        SDL_AtomicUnlock(&data->lock);
+
+        if (status < 0) {
+            return 0;
         }
-        return NULL;
+    }
+
+    SDL_AtomicLock(&data->lock);
+    timer = data->freelist;
+    if (timer) {
+        data->freelist = timer->next;
+    }
+    SDL_AtomicUnlock(&data->lock);
+
+    if (timer) {
+        SDL_RemoveTimer(timer->timerID);
+    } else {
+        timer = (SDL_Timer *)SDL_malloc(sizeof(*timer));
+        if (!timer) {
+            SDL_OutOfMemory();
+            return 0;
+        }
     }
-    if (!SDL_timer_threaded) {
-        SDL_SetError("Multiple timers require threaded events!");
-        return NULL;
+    timer->timerID = SDL_AtomicIncRef(&data->nextID);
+    timer->callback = callback;
+    timer->param = param;
+    timer->interval = interval;
+    timer->scheduled = SDL_GetTicks() + interval;
+    timer->canceled = SDL_FALSE;
+ 
+    entry = (SDL_TimerMap *)SDL_malloc(sizeof(*entry));
+    if (!entry) {
+        SDL_free(timer);
+        SDL_OutOfMemory();
+        return 0;
     }
-    SDL_mutexP(SDL_timer_mutex);
-    t = SDL_AddTimerInternal(interval, callback, param);
-    SDL_mutexV(SDL_timer_mutex);
-    return t;
+    entry->timer = timer;
+    entry->timerID = timer->timerID;
+
+    SDL_mutexP(data->timermap_lock);
+    entry->next = data->timermap;
+    data->timermap = entry;
+    SDL_mutexV(data->timermap_lock);
+
+    /* Add the timer to the pending list for the timer thread */
+    SDL_AtomicLock(&data->lock);
+    timer->next = data->pending;
+    data->pending = timer;
+    SDL_AtomicUnlock(&data->lock);
+
+    /* Wake up the timer thread if necessary */
+    SDL_SemPost(data->sem);
+
+    return entry->timerID;
 }
 
 SDL_bool
 SDL_RemoveTimer(SDL_TimerID id)
 {
-    SDL_TimerID t, prev = NULL;
-    SDL_bool removed;
+    SDL_TimerData *data = &SDL_timer_data;
+    SDL_TimerMap *prev, *entry;
+    SDL_bool canceled = SDL_FALSE;
 
-    removed = SDL_FALSE;
-    SDL_mutexP(SDL_timer_mutex);
-    /* Look for id in the linked list of timers */
-    for (t = SDL_timers; t; prev = t, t = t->next) {
-        if (t == id) {
+    /* Find the timer */
+    SDL_mutexP(data->timermap_lock);
+    prev = NULL;
+    for (entry = data->timermap; entry; prev = entry, entry = entry->next) {
+        if (entry->timerID == id) {
             if (prev) {
-                prev->next = t->next;
+                prev->next = entry->next;
             } else {
-                SDL_timers = t->next;
+                data->timermap = entry->next;
             }
-            SDL_free(t);
-            --SDL_timer_running;
-            removed = SDL_TRUE;
-            list_changed = SDL_TRUE;
             break;
         }
     }
-#ifdef DEBUG_TIMERS
-    printf("SDL_RemoveTimer(%08x) = %d num_timers = %d thread = %lu\n",
-           (Uint32) id, removed, SDL_timer_running, SDL_ThreadID());
-#endif
-    SDL_mutexV(SDL_timer_mutex);
-    return removed;
-}
+    SDL_mutexV(data->timermap_lock);
 
-/* Old style callback functions are wrapped through this */
-static Uint32 SDLCALL
-callback_wrapper(Uint32 ms, void *param)
-{
-    SDL_TimerCallback func = (SDL_TimerCallback) param;
-    return (*func) (ms);
-}
-
-int
-SDL_SetTimer(Uint32 ms, SDL_TimerCallback callback)
-{
-    int retval;
-
-#ifdef DEBUG_TIMERS
-    printf("SDL_SetTimer(%d)\n", ms);
-#endif
-    retval = 0;
-
-    if (SDL_timer_threaded) {
-        SDL_mutexP(SDL_timer_mutex);
+    if (entry) {
+        if (!entry->timer->canceled) {
+            entry->timer->canceled = SDL_TRUE;
+            canceled = SDL_TRUE;
+        }
+        SDL_free(entry);
     }
-    if (SDL_timer_running) {    /* Stop any currently running timer */
-        if (SDL_timer_threaded) {
-            while (SDL_timers) {
-                SDL_TimerID freeme = SDL_timers;
-                SDL_timers = SDL_timers->next;
-                SDL_free(freeme);
-            }
-            SDL_timer_running = 0;
-            list_changed = SDL_TRUE;
-        } else {
-            SDL_SYS_StopTimer();
-            SDL_timer_running = 0;
-        }
-    }
-    if (ms) {
-        if (SDL_timer_threaded) {
-            if (SDL_AddTimerInternal
-                (ms, callback_wrapper, (void *) callback) == NULL) {
-                retval = -1;
-            }
-        } else {
-            SDL_timer_running = 1;
-            SDL_alarm_interval = ms;
-            SDL_alarm_callback = callback;
-            retval = SDL_SYS_StartTimer();
-        }
-    }
-    if (SDL_timer_threaded) {
-        SDL_mutexV(SDL_timer_mutex);
-    }
-
-    return retval;
+    return canceled;
 }
 
 /* vi: set ts=4 sw=4 expandtab: */