Python: Why does the event scheduler keep drifting and is there a way to fix this?

Viewed 933

I want to use scheduler in a tkinter GUI for a stopwatch. I'm not sure if this is a better idea than using a background thread but I read that you should not stop threads, whereas I wanted to start and stop a function constantly.

I made a short snipped of code in Python 2.7 to test the scheduler and it seems like it starts drifting immediately. I wanted it to increment a counter every second, but by one minute I'm off by two seconds (62 seconds have passed instead of 60 seconds).

Is this machine related? Is there something wrong with my code? Should I use another library?

import sched, time

class Scheduler_Test:
    def __init__(self):
        self.counter = 0
        self.time_increment = 1.0

        self.end_time = 0.0

        self.s = sched.scheduler(time.time, time.sleep)

        self.start_time = time.time()
        self.s.enter(self.time_increment, 1, self.do_something, (self.s,))

        self.s.run() # run the event scheduler

    #Simple test of printing out the computer time (sec) and count
    def do_something(self, random_kwarg): 
        print "Time (sec):",time.time(),", count:", self.counter
        self.event = self.s.enter(self.time_increment, 1, self.do_something, (random_kwarg,))

        self.counter = self.counter + 1

Test = Scheduler_Test()
2 Answers

Never use delay if your goal is to keep up with actual time on long distance.

There always will be delay and eventually you'll be off-clock, reason for this — there's non-zero CPU work between event start and new event scheduling, plus you always have task priority.

So, if you want to have delay — use interfaces with "run_after" interfaces (in sched case, .enter). If you want to schedule things — use "run_at" (.enterabs in your case). Btw, taking into account that you have only single process in python, you still can be "late", but this is not something you can affect.

Side note: you rarely want to re-define scheduler timers, defaults is fine, it uses time.monotonic with fallback to time.time. Monotonic will save you from unexpected pain if your code will reach real world use.

Use time.time to actually do the timing. You can update the user interface using tkinter's clock, but update it to the value of the time delta from when you started the timer.

I made a similar application using PySimpleGUI, which is based on tkinter. The way I did the timing was this... pseudocode follows as I am using the wrapper to do these functions....

start_time = int(round(time.time() * 100))
while True:
    # use timer using tkinter's 'after' method
    # tkroot.after(timeout, alarmcallback)  (Pseudocode)
    # tkroot.mainloop()
    #     in the alarm callback I called quit   tkroot.quit()
    # Then I'm back in my code....
    current_time = int(round(time.time() * 100)) - start_time
    # Update the GUI with current_time
Related