Doing things in parallel
Coroutines run multiple items in parallel--or, to be more exact, they run items in rapid alternation in a way that looks parallel. Not all items support coroutines.
Using coroutines
You can use coroutines through the coroutines plugin (see Figure 1).
As you can see, the coroutines plugin looks similar to the sequence item, but has a few extra options:
- Duration indicates the total duration of the coroutines.
- End after item (optional) indicates that the coroutines should end when a specific item has ended. This allows you, for example, to indicate that the coroutines should end when a key press has been collected, by selecting a keyboard_response item here.
- Generator function name (optional) indicates the name of a generator function that has been defined in an inline_script (see Writing a custom coroutine below).
- Each item has a Start time. Most items also have an End time. The end time does not apply to one-shot items; for example, sketchpads show a display and terminate immediately, so they have no end time.
Specifically, the example from Figure 1 (from the stop-signal-task example) does the following:
- It shows a target display immediately.
- If the
stop_after
variable is not empty, it shows the stop_signal display after an interval specified by thestop_after
variable. - During the entire (2000 ms) interval, a keyboard response is collected.
The temporal flow is controlled by the coroutines plugin. Therefore, the timeout and duration values specified in the items are not used. For example, in Figure 1, the keyboard_response will run for 2000 ms, regardless of the timeout that is specified in the item.
Supported items
Currently, the following items are supported:
- keyboard_response
- mouse_response
- sampler
- sketchpad
- feedback
- inline_script (see Writing a custom coroutine)
Writing a custom coroutine
Technically, coroutines are generators. Generators are functions that can suspend their execution (i.e., they yield
) and resume later on; therefore, multiple generators can run in a rapidly alternating suspend-resume cycle. This trick is sometimes called weightless threading, because it has most of benefits of real threading, without any of the overhead or (potential) instability. Coroutines do not use threading or multiprocessing.
In the coroutines plugin, you can indicate the name of a generator function that you have defined in an inline_script. This generator needs to work in a particular way (as illustrated in the examples below):
- It must initialize and then
yield
. This firstyield
returns nothing. - It may loop while
yield
ing on every iteration. The loop breaks when:- The coroutine should end; or
- When the
yield
returnsFalse
; this is a signal from the coroutines plugin to the generator to signal that the coroutines ends. - When the generator
yields False
itself; this is a signal from the generator to the coroutines to signal that the coroutines ends.
- No time-consuming things should happen between
yield
statements, except during initialization.
The first and simplest option is to write a one-shot coroutine. This is a function that is called once to prepare itself, once to execute, and then terminates. For example:
def my_oneshot_coroutine():
"""
This is an example of a one-shot generator coroutine.
"""
# All initialization stuff goes here
print('Initialize!')
# Now yield to signal the end of the preparation
yield
# Do something here and terminate
print('Stopped!')
A more complicated example is a coroutine that prepares itself, is then called repeatedly (and keeps track of how often), and then terminates:
def my_coroutine():
"""
This is an example of a generator coroutine that keeps track of
how many cycles it goes through.
"""
# All initialization stuff goes here
i = 0
# Now yield to signal the end of the preparation
yield
# Enter an infinite loop
while True:
# Do some important stuff here. This should not be
# too time consuming; otherwise you will block the
# other items of the coroutines.
i += 1
# Yield to give other items an opportunity to execute,
# and receive a boolean that indicates whether we
# should keep going
keep_going = yield
if not keep_going:
break
# We are done!
print('Stopped after %d cycles!' % i)
Another example, which stops the coroutines when a response has been collected.
def my_coroutine():
"""
This is an example of a generator coroutine that aborts the coroutines when
a response has been collected.
"""
# To start, set response to None
var.response = None
yield
# Loop while coroutines is running
while True:
# If response is None (i.e. no response has been collected), signal
# True to the coroutines to indicate that, from this end, the coroutines
# should keep going.
signal_to_coroutines = var.response is None
# Send the signal to the coroutines, and get a return signal. If the
# return signal is False, we should break the loop.
signal_from_coroutines = yield signal_to_coroutines
if not signal_from_coroutines:
break