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.
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_aftervariable is not empty, it shows the stop_signal display after an interval specified by the
- 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.
Currently, the following items are supported:
- 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 first
- It may loop while
yielding on every iteration. The loop breaks when:
- The coroutine should end; or
- When the
False; this is a signal from the coroutines plugin to the generator to signal that the coroutines ends.
- When the generator
yields Falseitself; this is a signal from the generator to the coroutines to signal that the coroutines ends.
- No time-consuming things should happen between
yieldstatements, 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