Threading is essentially doing two or more things at once. So far the programs that we have written have been sequential, there is one flow of control that executes each statement in order. In threading, there can be multiple flows of control.
A thread is a single sequential flow of control that cannot exist by itself. It exists within a program and so shares memory with that program.
In our MD program, if we wanted to run the simulation at the same time as our GTK main loop, we would put it in a thread. We could also put the minimiser in a thread, and use them alternately, or even at the same time.
The result, as shown above, is two or more flows of execution running at the same time. Of course, they are not actually running simultaneously. What is really happening is that the python interpreter is switching between the two threads, giving them equal amounts of time to execute some code, and then switch again. This switching is done hundreds of times a second, so to us, they seem to be running simultaneously.
Not only do the two threads run at the same time, they also share the same memory. For example, in our MD program, we might have a GTK controller in the main program thread, and a simulator in another thread. They would both share a visual renderer, so the simulator would be able to call update, and the controller would be able to call another method, say a method that changes the refresh rate, on the same object. Not only that, but they can call methods on each other. While the simulator is running, the controller can call a method that will tell it to stop.
One big issue that arises in multi threaded applications is synchronisation. Let's say that we have our GTK controller running, and our simulator waiting for someone to click start. How does the simulator wait for this event? A simple approach would use the following code:
def start(self): self.simulate = True . . . while self.simulate == False: pass
In this code, the program would continually check to see if the simulate
variable was False. As soon as another thread called start
, the program
would continue. This is a bad implementation. The problem is that it uses
busy waiting. Busy waiting means that the program is continually doing
something while it's waiting, such as our example above where it is continually
checking to see if it can go. This is extremely inefficient, as the thread
doesn't need to be doing anything at all.
Fortunately, Python provides many methods of synchronisation. We will only
look at one. The simplest method that Python provides is through the use of
events. An event is a flag that is either true or false. It is created
by calling threading.Event()
. It can be set to true using set()
and
cleared to false using clear()
. It also has a isSet()
method that
returns true if the event is set, and false if the event is not set. So far,
this is no more than a boolean flag. There is one more method that event has,
the wait()
method. The wait()
method waits until the event is set, and
then returns. It does this without the use of busy waiting, and so overcomes
the issues with our previous implementation.
All Python threads inherit from the threading.Thread
class. The thread is
set up by the __init__()
method in the threading class, this means that if
the __init__()
method is redefined, the __init__()
method of the thread
class must be explicitly called.
Once set up, a thread is started by calling the start()
method on that
class. The start()
method must not be redefined. It starts the new flow
of execution, once it has done that, it calls the run()
method in the
class. This is a deferred method, and so must be implemented. This is where
we will implement the simulation.