We’re going to have a diversion away from the penguin app for a bit due to some work I ‘m doing and thought it would be useful for many working with the Raspberry Pi and Python. I’m working with time-lapse photography on the Raspberry Pi, and ran into an issue which I solved with threading. Since not everyone has a camera, and I don’t want to go into the API for cameras, let’s use a countdown timer for an example.
The Problem Demonstrated
Let’s start with the problem. In a new file, type this code:
# #Run a clock for a specified time with a specified interval from tkinter import * import time def print_count(delay,counter): while counter: time.sleep(delay) print(str(counter) ) timeString.set(str(counter)) counter -= 1 def startClock(): print_count(1,10) #make the view, which is a single button to stop the thread #make the window root=Tk() root.title('My Clock') #make the frame frame = Frame(root) frame.grid(row=0, column=0) #make the button startbutton = Button(frame,text = "Start Clock", command = startClock) startbutton.grid(row=1,column=0,sticky=NSEW) timeString = StringVar() timeString.set("A timer with interrupts") timeLabel = Label(frame,textvariable = timeString) timeLabel.grid(row = 0,column=0,sticky=NSEW) mainloop()
Save as countdown.py and Run. Press the start button and you will notice two things. The first is the countdown proceeding along on the shell according to the print()
statement. In the Tkinter window, there is no movement even though the timeString.set()
happens right after the print()
until the count ends.

This is classic in almost any UI. Code that takes a while to run freezes the UI. Even the code doing the freezing won’t release it long enough to update the timeString
label. What if we wanted to stop this clock or change a setting while it was running? You can’t since your buttons are frozen until the loop ends.
The solution is to run the loop somewhere else. We call that somewhere else a thread. Any line of processing is a thread. User interfaces always run on the main thread, and we run processes that might take a while on a separate thread or sub-thread. Our delay loop, a file load, a file save, a web access or image processing often get their own threads so as not to delay the user interface.
Python has in its library a threading class. We make a subclass of it and overwrite the __init__()
and run()
methods to make our thread. As in any Python class, __init__
will initialize and set up the properties of the class.
Here is our strategy: set up a global variable as a flag outside of the loop. On the main thread, make a button to turn the flag to a stop status. The loop will check how this flag’s status and when it is in the stop status, will exit the loop. Before we set up the thread, let’s set up this flag and see what happens without the thread. Just under the from..import
statements add the following code:
# a flag to determine if the processing loop stops exitFlag = 1
Add a button to stop the loop under the start button code:
stopButton = Button( frame, text = "Stop Clock", command = stopClock) stopButton.grid(row=1,column=1,sticky=NSEW)
Add the handler under the startClock()
function:
def stopClock(): global exitFlag exitFlag = 0
Line 2 tells the function to use the exitFlag
declared outside the function. Otherwise it would assume exitFlag
was a local variable to the function. Since 0 in Python appears as false to conditional statements, we can use exitFlag
in conditional statements. Change the print_count()
function to this:
def print_count(delay,counter): while counter and exitFlag : time.sleep(delay) print(str(counter) ) timeString.set(str(counter)) counter -= 1
Line 2 uses a logical and
to check the counter status and the exit flag. If either counter
or exitFlag
are 0, the loop ends.
Save and run. Start the countdown and try to stop the count. The stop button freezes as we expected.
Adding the Thread
We’ll need two modules to implement threading. Add this to our import statements:
import threading import _thread
Now add the class definition and the constructor:
# The thread class subclassed-- #overwrite __init__() with name and data for the thread #Overwrite run() with the code to run in the thread class MyThread (threading.Thread): def __init__(self,threadID,name,delay, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter self.delay = delay
We subclass threading.Thread
to make a thread in Line 1. We then override the constructor with information about the thread. For most threads, all but line 9 is going to be used whenever you implement this. Line 9 is a custom parameter for what we are doing with this thread, counting down from this number.
Next we override the class’ run()
function. This function is what will run when the thread starts.
def run(self): print ("Starting " + self.name) print_count(self.delay, self.counter) print ("Exiting " + self.name)
A good practice is to run a function defined elsewhere, not in run()
. In this case, we run the already defined print_count()
function.
Go down to the startClock()
function and change it to the following:
def startClock(): # print_count(1,10) thread1=MyThread(1,"thread1",1,10) thread1.start()
First this function makes the thread with an ID of 1, a name of thread1, and set the counter to 10 seconds. Instead of starting the function, we start the thread that will start the function. Build and run this. Let the counter run down. This time, the label does count down along with the shell.
Stop the program. Go back to the code and change the thread1
assignment in the startClock()
method.
def startClock(): # print_count(1,10) thread1=MyThread(1,"thread1",2,10) thread1.start()
We now have a two-second interval between displaying the numbers. Save and run again, and this time at the count of 6, stop the clock. It should stop, but pauses.
In the loop, we used the sleep()
function. A lot of people for some reason like this. If you are not going to have a user interface, sleep()
is a quick and dirty way of pausing, but it pauses everything on the thread for the specified period of time.
In most loops for sensing events, sleep()
is unacceptable. A better procedure is to test for a condition and break the loop for it. In our case the best would be to calculate when the next time interval is and when we should stop the loop. Add the following import
statement :
from datetime import datetime, timedelta
This give us the functions we need to calculate time correctly. Now add this function just above print_count()
code:
def print_timeCount(interval,counter): countDown = counter global exitFlag period = timedelta(seconds=interval) nextTime = datetime.now() + period print('Timer start') #for debugging print(str(countDown)) timeString.set(str(countDown)) while countDown > 0 and exitFlag: if nextTime <= datetime.now(): nextTime += period countDown -= interval print(str(countDown)) timeString.set(str(countDown)) exitFlag = 1 #end of loop and function
Line 2 creates a countdown variable which we will decrement as we go through the loop. Line 3 declares exitFlag
global, so the function knows to look outside itself for a value. Lines 4 and 5 create two important time based variables. The variable period
is our interval in system time measurements. That initializes another counter which tells us when the next period ends by adding the period to the current time found in datetime.now()
.
After doing a little printing, We get into the loop. The loop will run if the flag is in a run condition, which is a non-zero value and if the countdown stays above zero. When it hits zero, it stops. Within the loop, we check if next time has passed yet. When it does, we print the count, decrease it by the interval, and compute the next time we need to check.
Change the run()
to use this function:
def run(self): print ("Starting " + self.name) #print_count(self.delay, self.counter) print_timeCount(self.delay,self.counter) print ("Exiting " + self.name)
Build and Run. You can now stop the count immediately on pressing the stop button. Our example used time, but this is very flexible. The loop colud watch for more than one flag or look for external events such as an input from a GPIO pin.
There’s always one more bug
Run the script again. Press the stop
button first and then press the start
button. The count immediately ends. We reset the flag for the stop button in the print_timeCount()
code. We don’t want to set the flag to stop unless the thread is running. The is_Alive()
or isAlive()
method tests for this. Change the code to this:
def startClock(): #print_count(1,10) global thread1 thread1=MyThread(2,"thread1",10) thread1.start() def stopClock(): global exitFlag if threading.active_count() >2: global thread1 if thread1.isAlive(): exitFlag = 0 # exitFlag = 0
The thread is local to startClock()
. We want to test for it outside of startClock()
, so we cheat and use global
. If you wrapped this up in a class you can find better ways to do this with properties, but for our example this works. I did another cheat here, which should be done differently. If we hit stopClock
before we have ever run the clock, there is no thread1
and we would get an error. I do the cheat just for the example of checking how many threads are currently running. There happen to be two already, so any threads besides that are my own creation, and should be our sub thread. To be careful , it would be better to check the identity of the thread, but that’s enough code for now.
Now if you build and run, the stop button only works when the counter is working.
That is a very basic introduction to threading. There is a lot of complexity to it and a lot of danger you can get into with it. Dangers can occur when two threads modify the same variable at the same time. Application can get very unpredictable when there are conflicts between threads. Two threads may try to change the same variable. We may also have the case where a database loading thread hasn’t loaded the data yet, while the main thread goes and uses that information. But for small cases like the example here it works well for getting processing done and you don’t want your User Interface to freeze.
The Whole Code
Here is the code for the finished counter. It is also available on github here.
#!/usr/bin/python # #Run a countdown timer for a specified time with a specified interval from tkinter import * import threading import _thread from datetime import datetime, timedelta from time import sleep # a flag to determine if the processing loop stops exitFlag = 1 # The thread class subclassed-- #overwrite __init__() with name and data for the thread #Overwrite run() with the code to run in the thread class MyThread (threading.Thread): def __init__(self,threadID,name,counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print ("Starting " + self.name) #print_count(2, self.counter) print_timeCount(2,self.counter) print ("Exiting " + self.name) def print_count(delay,counter): while counter and exitFlag : time.sleep(delay) print(str(counter * delay) ) timeString.set(str(counter * delay)) counter -= 1 def print_timeCount(interval,counter): countDown = counter global exitFlag period = timedelta(seconds=interval) nextTime = datetime.now() + period print('Timer start') #for debugging print(str(countDown)) timeString.set(str(countDown)) while countDown > 0 and exitFlag: if nextTime <= datetime.now(): nextTime += period countDown -= interval print(str(countDown)) timeString.set(str(countDown)) exitFlag = 1 #end of loop and function def startClock(): #print_count(1,10) global thread1 thread1=MyThread(1,"thread1",10) thread1.start() print("clock Started") def stopClock(): global exitFlag if threading.active_count() >2: global thread1 if thread1.isAlive(): exitFlag = 0 # exitFlag = 0 print(threading.enumerate()) #make the view, which is a single button to stop the thread #make the window root=Tk() root.title('My Clock') #make the frame frame = Frame(root) frame.grid(row=0, column=0) #make the button startbutton = Button(frame,text = "Start Clock", command = startClock) startbutton.grid(row=1,column=0,sticky=NSEW) stopButton = Button( frame, text = "Stop Clock", command = stopClock) stopButton.grid(row=1,column=1,sticky=NSEW) timeString = StringVar() timeString.set("A timer with interrupts") timeLabel = Label(frame,textvariable = timeString) timeLabel.grid(row = 0,column=0,sticky=NSEW) mainloop()
Leave a Reply