appleberry

From Apple to Raspberry Pi: How to do Threading with Python and Tkinter

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.

The countdown happens in the shell, but not the window until the clock stops.
The countdown happens in the shell, but not the window until the clock stops.

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

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s