Learn EV3 Python‎ > ‎

Multithreading

A 'thread' is a part of program code that can run independently and at the same time as other parts of the program (some programing languages call threads 'tasks'). Every program has at least one thread, the main thread. If the program launches other threads that run simultaneously with the main thread then that is known as 'multithreading'. For example, you could create a main thread that controls the motors, while a different thread can watch sensors or user input. In the first example below a thread called 'playtone' is started - it plays ten tones while the main script waits for the touch sensor button to be 'bumped' (pressed and released) 5 times. If the tones stop before the five bumps have been finished then program execution stops after the bumps (and the beep) have been completed, as you would expect. But what if the five bumps and the beep are completed before the ten tones have completed: will the playing of the tones be interrupted? Try running the script and you will observe that in this case the playing of the tones is NOT interrupted, which doesn't seem to be a big problem here but can be a big problem in other scripts, as explained further down. The key lines for setting up the thread are in bold. Notice that in the line
t = Thread(target=playtone)
there are no parentheses after the thread name 'playtone'.

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep
from threading import Thread
ts = TouchSensor() 
assert ts.connected, "Connect a touch sensor to any sensor port"

def playtone():
    for j in range(0,10):             # Do ten times.
        Sound.tone(1000, 200).wait()  #1000Hz for 0.2s
        sleep(0.5)

t = Thread(target=playtone)
t.start()
for i in range(0,5):       # Do five times.
    while ts.value() ==0:  # while button is not pressed
        sleep(0.01)        # do nothing other than wait
    while ts.value() ==1:  # while button is pressed
        sleep(0.01)        # do nothing other than wait
Sound.beep()


In the above script the program ends only after both the tone sequence AND the button presses have been finished. But check out the next example where the tones play in an infinite loop and the five button presses are intended to stop the tones playing. As you learnt above, the script below will fail to allow the button presses to stop the program since the program will only stop when both parts have completed, and the tone sequence never completes. (You can force the program to quit by pressing Ctrl-C if the script was run from SSH or by long-pressing the Back button if the script was run from Brickman.)

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep
from threading import Thread
ts = TouchSensor() 
assert ts.connected, "Connect a touch sensor to any sensor port"

def playtone():
    while True:             # Do forever.
        Sound.tone(1000, 200).wait()  #1000Hz for 0.2s
        sleep(0.5)

t = Thread(target=playtone)
t.start()
for i in range(0,5):       # Do five times.
    while ts.value() ==0:  # while button is not pressed
        sleep(0.01)        # do nothing other than wait
    while ts.value() ==1:  # while button is pressed
        sleep(0.01)        # do nothing other than wait
Sound.beep()

So what is the solution? I looked for ways of including something in the main script that would terminate the playtone thread - it turns out that this cannot be done in Python (and that's probably a good thing, because it could have messy consequences, but see the note at the bottom of this page). Having watched this video, I was able to write the script below in which the tones will play indefinitely until the touch sensor is bumped five times, after which the playtone thread terminates neatly. Pay attention to the lines in bold.

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep
from threading import Thread
ts = TouchSensor() 
assert ts.connected, "Connect a touch sensor to any sensor port"

def playtone():
    global run # so that we can use the same global variable 'run'
    # that we created in the main script below
    while run:  # Same as while run == True
        Sound.tone(1000, 200).wait()  #1000Hz for 0.2s
        sleep(0.5)

global run
run=True
t = Thread(target=playtone)
t.start()
for i in range(0,5):
    while ts.value() ==0:  # while button is not pressed
        sleep(0.01)        # do nothing other than wait
    while ts.value() ==1:  # while button is pressed
        sleep(0.01)        # do nothing other than wait
Sound.beep()
run=False # So that the loop in the playtone thread stops looping

In the above script the main part creates a global variable called 'run' which is given the value True. In the playtone thread the loop repeats as long as the 'run' variable is equal to True. The last line of the main part of the script sets run to equal False so that the loop in the playtone thread also stops running. It's a bit annoying to have to stop the thread in such a complex way, but it's necessary. Professional programmers would prefer not to 'waste' a global variable in the way shown in the above script - the video hints at how they might handle the problem.

Note that it's also be possible to do the above script 'backwards', with the 'thread' stopping a loop in the main part of the script, as shown below. The thread 'buttonStop' is started - it waits for the touch sensor to be bumped 5 times and once that has happened it sets the global variable 'run' to be False which causes the loop in the main part of the script to stop looping.

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep
from threading import Thread
ts = TouchSensor() 
assert ts.connected, "Connect a touch sensor to any sensor port"

def buttonStop():
    global run # so that we can use the same global variable 'run'
    # that we created in the main script below
    for i in range(0,5):
        while ts.value() ==0:  # while button is not pressed
            sleep(0.01)  # do nothing other than wait
        while ts.value() ==1:  # while button is pressed
            sleep(0.01)  # do nothing other than wait
    Sound.beep()
    run=False # So that the loop in the main part stops looping
        
# Create a global variable 'run' which will be set to
# False when we want the loop below to stop looping.
global run
run=True
t = Thread(target=buttonStop)
t.start()

while run:  # Same as while run == True
    Sound.tone(1000, 200).wait()  #1000Hz for 0.2s
    sleep(0.5)

Of course the above script could be simplified so that only a single bump of the touch sensor is needed to stop the tone loop. It would even be possible to stop the loop with a single press of the touch sensor rather than a 'bump' (press and release). But why use a sensor when we can instead make use of the buttons that are built into the EV3 brick? The script below has a main part which uses a loop to make the robot advance only when the light level is above 15 and a thread which waits for any EV3 button to be pressed and then beeps and causes the 'move-when-bright' loop to stop looping. If you play with this script you may be surprised to note that even though the beep happens as soon as you press any button it may take many more seconds before the script actually stops. This is logical if you think about it - we are not issuing an instruction to instantly terminate the main part of the code, only to cause the loop not to repeat, so the program has to reach the bottom of the loop before the program ends and in this case, according to how the light level changes, that could take many seconds. To avoid that problem you could in this case change 
while cl.value()<=15:     into     while cl.value()<=15 and run:
and change
while cl.value()>15:     into     while cl.value()>15 and run:
so that the script waits for a change in the light level only when 'run' is True. Note that 
while cl.value()<=15 and run:
is the same as 
while (cl.value()<=15) and (run==True):

#!/usr/bin/env python3

from ev3dev.ev3 import *
from time import sleep
from threading import Thread
mB = LargeMotor('outB')
mC = LargeMotor('outC')
cl = ColorSensor()
assert cl.connected, "Connect a color sensor to any sensor port"
cl.mode='COL-AMBIENT'  # set mode to measure ambient light level
btn=Button()

def buttonStop():
    global run # so that we can use the same global variable 'run'
    # that we created in the main script below
    while btn.any()==False:  # while no button is pressed
        sleep(0.01)  # do nothing other than wait
    run=False  # Set run to False to cause loop below to stop.
    Sound.beep()

# Create a global variable 'run' which will be set to
# False when we want the thread above to stop running.
global run
run=True
t = Thread(target=buttonStop)
t.start()

while run:  # Loop until run is set to False by the buttonStop thread.
    # Wait for the ambient light level to rise above 15
    # then start the robot.
    while cl.value()<=15:
        sleep(1)  # Do nothing while we're waiting.
    mB.run_forever(speed_sp=500)
    mC.run_forever(speed_sp=500)
    
    # Wait for the ambient light level to be less than or equal to 15,
    # then stop the robot.
    while cl.value()>15:
        sleep(1)  # Do nothing while we're waiting.
    mB.stop()
    mC.stop()

Since Python threads are cumbersome, it's always worth considering whether they can be avoided. The script below has the same functionality as the one above, but avoids the use of a thread. It can probably be simplified even more by using if.... else rather than the two while loops that check the light level - I'll leave that up to you.

#!/usr/bin/env python3

# The script below makes the robot advance when the ambient light
# is bright. It can be stopped by pressing any EV3 button for at
# least 0.5 seconds and this is made possible without the use of
# a thread. Instead, the code checks for a button press whenever
# it checks the light level and if a button press is detected
# then the sleep commands are not run and
# the main loop quickly reaches the end. The main loop does not
# repeat because it can repeat only when no buttons are pressed.

from ev3dev.ev3 import *
from time import sleep
mB = LargeMotor('outB')
mC = LargeMotor('outC')
cl = ColorSensor()
assert cl.connected, "Connect a color sensor to any sensor port"
cl.mode='COL-AMBIENT'  # set mode to measure ambient light level
btn=Button()

while btn.any()==False# Loop while no button is pressed.
    # Wait for the ambient light level to rise above 15
    # then start the robot.
    while cl.value()<=15 and btn.any()==False:
        sleep(0.5)  # Do nothing while we're waiting.
    mB.run_forever(speed_sp=500)
    mC.run_forever(speed_sp=500)
    
    # Wait for the ambient light level to be less than or equal to 15,
    # then stop the robot.
    while cl.value()>15 and btn.any()==False:
        sleep(0.5)  # Do nothing while we're waiting.
    mB.stop()
    mC.stop()
Sound.beep()

Generally speaking, multi-threading is quite a complex topic. To really understand multi-threading, some extra study is recommended.

HERE is the official documentation for the Python threading module.

HERE is a nice introduction to Python 3 threading, but it is not EV3-specific.

HERE is an article which says that multi-threading in Python is difficult and that Python experts recommend "Do not use multiple threads. Use multiple processes." Running multiple processes is outside the scope of this site but see HERE and note that although the page refers to threads it is really about processes.

A final note: I said above that in Python you cannot give an instruction on one thread for another thread to be terminated. But it's worth noting that RobotC has a stopTask() function which I believe can be used to terminate a thread, and EV3-G has a useful Loop Interrupt block which can sometimes be used to cause a thread to terminate...
Comments