Multithreaded Python Tutorial with the “Threadworms” Demo

Going back to our robot ticket seller metaphor, this is like having a robot pick up the list (the list is a “lock”), and then reading it the ticket is available, grabbing the ticket, and then crossing out the seat on the list. When the robot puts the list back down, it is “releasing the lock”. If another robot needs to pick up the list but it is not there, it will wait until the list is available.

You can cause bugs by writing code that forgets to release a lock. This will cause a deadlock situation since the other threads will hang and do nothing while waiting for a lock to bereleased.

Threads in Python

Okay, let’s write a Python program that demonstrates how to use threads and locks. This program is based off of my “Snake” clone in Chapter 6 of my Making Games with Python & Pygame book. Except instead of a worm running around eating apples, we’ll just have the worm running around the screen. And instead of just one worm, we will have multiple worms. Each worm will be controlled by a separate thread. The shared variable will have the data structure that represents which places on the screen (called “cells” in this program) are occupied by a worm. A worm cannot move forward to occupy a cell if another worm is already there. We will use locks to ensure that the worms don’t occupy the same cell as another worm.

The code for this tutorial can be downloaded here: threadworms.py or from GitHub. This code works with Python 3 or Python 2, and you need Pygame installed as well in order to run it.

Here’s a summary of the thread-related code in our threadworms.py program:

import threading

Python’s thread library is in a module named threading, so first import this module.

GRID_LOCK = threading.Lock()

The class Lock in the threading module has acquire() and release() methods. We will create a new Lock object and store it in a global variable named GRID_LOCK. (Since the state of the grid-like screen and which cells are occupied is stored in a global variable named GRID. The pun was unintended.)

# A global variable that the Worm threads check to see if they should exit.

WORMS_RUNNING = True

Our WORMS_RUNNING global variable is regularly checked by the worm threads to see if they should quit. Calling sys.exit() will not stop the program because it only quits the thread that made the call. As long as there are other threads still running the program will continue. The main thread in our program (which handles the Pygame drawing and event handling) will set WORMS_RUNNING to False before it calls pygame.quit() and sys.exit(). The next time a thread checks WORMS_RUNNING, it will quit, until eventually the last thread quits and then the program terminates.

class Worm(threading.Thread):

def __ init__(self, name='Worm', maxsize=None, color=None, speed=None):

threading.Thread.__init__(self)

self.name = name

The thread’s code must start from a class that is a child of the Thread class (which is in the threading module). Our Thread subclass will be named Worm since it controls You don’t need an __init__() function, but since our Worm classes uses one we need to call the threading.Thread class’s __init__() method first. Also optional is to override the name member. Our __init__() function uses the string 'Worm' by default, but we can supply each thread with a unique name. Python will display the thread’s name in the error message if it crashes.

GRID_LOCK.acquire()

# ...some code that reads or modifies GRID...

GRID_LOCK.release()

Before we read or modify the value in the GRID variable, the thread’s code should attempt to acquire the lock. If the lock isn’t available, the method call to acquire() will not return and instead “block” until the lock becomes available. The thread is paused while this happens. This way, we know that the code after the acquire() call will only happen if the thread has acquired the lock.

Acquiring and releasing a lock around a bit of code ensures that another thread does not execute this code while the current thread is. This makes the code atomic because the code is always executed as a single unit.

After the thread’s code is done with the GRID variable, the lock can be released by calling the release() method.

def run(self):

# thread code goes here.

Page 3 of 10 | Previous page | Next page