Troubleshooters.Com, Code Corner and Python Patrol present:

Introduction to Python Threads

See the Troubleshooters.Com Bookstore.

CONTENTS:

Introduction

This document is a lightweight introduction to the simplest way to use threads within Python 3. It barely scratches the surface of all the capabilities and landmines of threads in Python.

What Are Threads?

The following is a breathtakingly oversimplified definition of a thread:

A thread is a child of a process. The thread shares the same memory and some other resources with the parent process, and with its sibling threads.

The memory and resource sharing are both a blessing and a curse. The blessing is that less RAM is used and that communication between threads is quicker and easier because threads can change each others variables, program counter, registers etc. If you want more information, you can look at the following resources:

The code in this document's examples might not be thread safe. It has, however, been tested and appears to work correctly. The Python language itself takes steps to add quite a bit of thread safety.

Trivial Python Thread Example

This section contains an 18 line Python program that illustrates the creation and running of a function as a separate thread. This program contains only local variables with no global variables. Threads each have a separate stack, so a thread can't overwrite another thread's local variable. However, I can't tell you whether the three imported libraries contain global variables, nor can I tell you whether the Python print() function is thread safe.

The Python program introduced in this section has function main() which simply runs function background_function() as a separate thread. The background_function() function simply prints a start message, sleeps five seconds, and then prints an end message. The main function also prints its own begin and end message.

Because background_function() is run in an independent thread with no blocking facilities (such as the threading.Thread.join() function) or coordination facilities such as mutexes, control returns to function main() the instant function background_function() starts in its new thread, even though background_function takes five seconds to run. In other words, background_function() runs asynchronously. Therefore, the expected behavior of the program is as follows:

  1. The main function start message prints.
  2. The background function start message prints.
  3. The main function end message prints.
  4. You wait five seconds.
  5. The background end message prints.

If the preceding sequence occurs in the order stated, it proves that function background_function() runs asynchronously in it's own thread. And in fact, the preceding sequence does occur in the order stated when you run the program.

Before showing the program's listing, a few words about the program's import files:

The program listing follows:

#!/usr/bin/python3
import threading
import time

def background_function():
    print('Top of background function')
    time.sleep(5)
    print('Bottom of background function')

def main():
    print('Start main program')
    thread = threading.Thread(
        target=background_function,
    )
    thread.start()
    print('End main program')

main()

In the preceding listing, function background_function() simply prints a start message, sleeps five seconds, then prints an end message.

The main function likewise prints its own start and end messages. The threading.Thread() function creates (but does not actually run) a thread associated with function background_function(). The thread.start() actually runs the thread. The sequence of outputs when you run this program proves that background_function() is run synchronously in its own thread.

Thread Safety of This Program

I'm no expert on thread safety, but my guess is that it's thread safe, assuming that the time library is thread safe and the print() function is thread safe. Notice that the background_function() uses nor sets any variables, so it can't clobber any other thread's variables. I'm going to assume that the Python interpreter makes sure that one thread's program counter doesn't overwrite the program counter of its sibling threads or its parent process.

The Python 3 documentation on time.sleep() seems to imply that time.sleep() is thread safe.

Based on the preceding, my best educated guess is that this code is thread safe.

A Looping Example

Conceptually, the program in this section builds on the Trivial Python Thread Example section's program in the following ways:

This program's code listing follows:

#!/usr/bin/python3
import threading
import time
import datetime

def printinfo(count, timestamp, msg):
    st='Iter={}, {}, {}'
    st=st.format(count, timestamp, msg)
    print(st)

def nowstamp():
    now=datetime.datetime.today()
    st='{:02}:{:02}:{:02}'
    st=st.format(
        now.hour,
        now.minute,
        now.second
    )
    return st

def background_function(counter):
    msg='Top of background function'
    printinfo(counter, nowstamp(), msg)
    sleeptime=5
    # FOLLOWING LINE PREVENTS RACE COND
    #sleeptime=5 + counter * 0.1
    time.sleep(sleeptime)
    msg='Bottom of background function'
    printinfo(counter, nowstamp(), msg)

msg='Start main program'
printinfo(0, nowstamp(), msg)
for counter in range(1,10):
    msg='Top of main loop'
    printinfo(counter, nowstamp(), msg)
    thread = threading.Thread(
        target=background_function,
        args=[counter]
    )
    thread.start()
    msg='Bottom of main loop'
    printinfo(counter, nowstamp(), msg)
    print('')
msg='End main program'
printinfo(counter, nowstamp(), msg)

A few facts to note about the preceding code:

LANDMINE!

If the args argument to the threading.Thread() call contains a single string rather than a list, threading.Thread() counts every letter of the string as an argument, and of course errors out saying "too many arguments".

The tricky thing is that if you have two or more arguments without square brackets, Python considers those a list and does the expected. It's like a little piece of Perl's "many ways to do it" ambiguity factory snuck into Python. I advise using square brackets regardless of the number of arguments: Doing so reveals your intent.

A GUI Threading Implementation

What caused me to investigate Python threads in the first place was a need to put up GUI message boxes, every few hours, without blocking. Blocking means you'd need to manually close one message box before the next one would show, which would have been the kiss of death for my use case. On the Internet, I found the easiest way to do it, easier than multiple processes, was to use Python threads.

In the following source code, note that we added from tkinter import * in order to show the GUI message box:

#!/usr/bin/python3
import threading
import time
import datetime
from tkinter import *

def nowstamp():
    now=datetime.datetime.today()
    st='{:02}:{:02}:{:02}'
    st=st.format(
        now.hour,
        now.minute,
        now.second
    )
    return st


def background_function(counter, junk_arg):
    root = Tk()
    root.configure()
    root.wm_title('Box # ' + str(counter))
    Lab1 = Label(root, text='Time: ' + nowstamp())
    Lab1.configure(
        foreground='#000000',
        background='#00ffff',
        font=('Sans', 14))
    Lab1.pack()
    geostring='400x50+{}+{}'
    geostring = geostring.format(
        str(100),
        str(90*counter)
    )
    root.geometry(geostring)
    root.mainloop()

####### MAIN PROGRAM #######
print('\n\n\n\nStart main program')
threads=[]
for counter in range(1,5):
    thread = threading.Thread(
            target=background_function,
            args=(counter, "junk")
            )
    thread.start()
    threads.append(thread)
    #time.sleep(0.5)
for thread in threads:
    thread.join()
print('\n')
print('########################')
print('########################')
print('End main program')
print('########################')
print('########################')    

In the preceding source code, the main program is pretty much the same as seen in previous code examples in this document, with a few exceptions:

  1. The background_function() function displays a GUI message box instead of printing to the console.
  2. Each instance of background_function() is launched with an argument corresponding to the iteration number in the thread launch loop.
  3. During the thread launch loop, an array of all the threads is assembled in array threads.
  4. After the loop finishes, another loop runs the join() method on every thread, in order to prevent the main program from progressing while any of the threads are still active.

The background_function() function is just the typical way to print a message box alone, without a parent window. The message boxes displayed have the iteration number in their titlebar, and the current time in their label. background_function() does not finish until a user closes the message box by clicking the X in the title bar or performing the "close window" key combination while the message box has focus. Therefore, to display several of these message boxes, function background_function() must be in its own thread so as to return control to the main program. Note also that the vertical position of the message box depends on the iteration number passed to background_function().

Wrapup

Threads are extremely helpful in some situations but they're complicated. This document serves as a bare beginning for using threads in Python, but it barely scratches the surface.