ll.taskswarm – Running tasks in parallel
Overview
ll.taskswarm provides an easy way to execute multiple Python functions in
parallel using the multiprocessing module. It is designed to improve
performance by utilizing multiple CPU cores efficiently.
Features
Parallel execution of multiple functions or other callables;
Automatic process management;
Supports passing arguments to functions;
Configurable number of parallel processes;
Tasks can dynamically add new tasks during execution;
Live progress output on the terminal (and in the iTerm2 session status via
ll.iterm2).
Limitations
Functions/callables, their arguments (and the object in case of bound methods) must be picklable (depending on the start method used by
multiprocessing).No return values are collected; tasks are executed for their side effects (like created files, executed HTTP or database requests).
Overhead from creating processes may outweigh benefits of parallelization.
Usage
A task is a function (or other callable) that gets a Task object as
its first argument. It may report its progress via Task.log() and
submit further tasks via Task.submit(). The task is finished when the
function returns (or raises an exception).
Tasks are submitted to a Swarm object inside a with block.
The tasks are started when the with block is exited, so the with
block itself only collects the initial tasks:
import sys, json, pathlib
from ll import taskswarm
def walk(task, dir):
task.log(t"Walking {dir}")
for file in dir.iterdir():
if file.is_dir():
task.submit(walk, file.name, file)
elif file.suffix == ".json":
task.submit(prettyprint, file.name, file)
task.log(t"Walked {dir}")
def prettyprint(task, file):
task.log(t"Pretty printing {file}")
data = json.loads(file.read_text())
file.write_text(json.dumps(data, indent=" "))
task.log(t"Pretty printed {file}")
if __name__ == "__main__":
with taskswarm.Swarm("Pretty printing JSON files", 20, False) as swarm:
dir = pathlib.Path(sys.argv[1])
swarm.submit(walk, str(dir), dir)
Each task runs in its own multiprocessing.Process. At most
processes (20 in the example) tasks run at the same time, additional tasks
are queued until a slot becomes free. The swarm is finished when all tasks
(including those submitted by other tasks) have finished.
Then a summary of the total, mean and longest wait and run times is printed.
Log messages and task names can be strings or t-strings (i.e.
string.templatelib.Template objects). A t-string is passed to the
swarm unchanged and formatted there by Swarm.format(): Each
interpolated value is formatted (and colored) according to its type (again
by Swarm.format()), so the task function doesn’t have to do that
itself. When the type isn’t enough to decide how a value should be formatted
(e.g. a database schema name that is a plain str), the format spec
selects the formatting: t"Exporting {name:schema}" calls the method
format_schema of the swarm (which a subclass can add). Since the
t-string is sent to the main process via a multiprocessing.Queue,
all interpolated values must be picklable. Values that aren’t must be
preformatted by the task (e.g. t"Exporting {str(obj):sqlobject}").
Output
While the swarm is running, its progress is printed to stdout. There are
two output modes (selected via the continuous argument of
Swarm):
- Continuous output (
continuous=True) Every event (a task being started, logging a message or finishing) is printed as a new line, prefixed with the elapsed time and the number of waiting, running and finished tasks. This mode is appropriate when the output is redirected to a file or a CI log.
- In-place output (
continuous=False) One line per process slot is reserved on the terminal and updated in place (via ANSI cursor movement) with the current state of the task running in that slot. Below these lines a status line shows the numbers of waiting, running and finished tasks and the current and mean load (i.e. the number of running processes). This mode requires a terminal that understands ANSI escape sequences.
In both modes values are colored via ANSI escape sequences (see
Swarm.format()), unless Swarm.plain_output is true, which is
the case on Windows or if stdout isn’t a terminal. The formatting can be
customized by overwriting the methods Swarm.format(),
Swarm.format_sep() and Swarm.format_task() in a subclass, or by
adding format_<spec> methods that can be selected via the format spec in
t-strings (see above).
When running in iTerm2, the title of the swarm and the current progress are
also shown in the iTerm2 session status (see
ll.iterm2.set_session_status()), so that the progress is visible in the
tab even when another tab is active.
- class ll.taskswarm.Swarm[source]
Bases:
objectA
Swarmexecutes tasks in parallel processes.A
Swarmis used as a context manager: Inside thewithblock tasks are submitted viasubmit(), when the block is exited the tasks are executed (and further tasks submitted by running tasks viaTask.submit()are executed too). Thewithblock ends when all tasks are done. If an exception is raised inside thewithblock, no tasks are started.The constructor arguments are:
titlestringThe title of the swarm. It is shown in the iTerm2 session status while the swarm is running.
processesintThe maximum number of tasks that run in parallel.
continuousboolSelects the output mode: continuous output (one line per event) if true, in-place output (one line per process slot) if false. See the module documentation for details.
After the
withblock the attributestasks,run_timeandmean_loadcan be used to inspect the result.All output of the swarm goes through the methods
format(),format_sep()andformat_task(), so the formatting can be changed by overwriting them in a subclass.The following attributes influence the formatting and can be changed after the constructor call:
plain_outputboolIf true, no ANSI escape sequences are output. Defaults to true on Windows or if
stdoutisn’t a terminal.currentdirpathlib.Pathformat()prints paths relative to this directory (if possible). Defaults to the current directory when the swarm was created.
- property mean_load
The mean number of running tasks over the runtime of the swarm (or
Noneif the swarm hasn’t run yet).The load is sampled whenever a task event (start, log message or done) is processed, so this is an average over events, not over time.
- property run_time
The total run time of the swarm as a
timedelta(orNoneif the swarm hasn’t finished yet).
- format(obj, spec='')[source]
Format
obj(a log message, a task name or a value interpolated in one of them) for the output of the swarm (with ANSI colors).If
objis a t-string (i.e. astring.templatelib.Templateobject) each interpolated value is converted according to its conversion (!r,!sor!a) and then formatted withformat()(passing the format spec);specitself is ignored in this case.Otherwise
specis the format spec from a t-string and is used as follows:If
specisn’t empty and the swarm has a method namedformat_<spec>, this method is called withobj. This is how a subclass can support formatting values whose type isn’t enough to decide how they should be formatted (e.g.{name:schema}callsself.format_schema(name)).Otherwise the formatting depends on the type of
obj:pathlib.Pathobjects are output relative tocurrentdir(if possible) in yellow, numbers anddatetimeobjects in bold magenta (withspecas the format spec, or,forintand,.01fforfloatifspecis empty),timedeltaobjects asHH:MM:SSwith the leading zeros in normal magenta and the rest in bold magenta (specis ignored). Everything else is formatted with the builtinformat()andspec(so an unknownspecraises aValueError).
If
plain_outputis true, no ANSI escape sequences are output.Overwrite this method in a subclass to change the formatting or to support additional types.
- format_sep(text)[source]
Format the separator
textfor the output of the swarm (i.e. in dark grey).If
plain_outputis true, no ANSI escape sequences are output.
- format_task(task)[source]
Format the “full name” of
task(i.e. the names of all tasks inTask.path()joined with separators) for the output of the swarm.
- submit(func, name, *args, **kwargs)[source]
Submit a new task to the swarm and return the
Taskobject.funcis the callable that gets executed in a separate process. It will be called asfunc(task, *args, **kwargs)(wheretaskis theTaskobject). The task is finished whenfuncreturns or raises an exception.nameis the name of the task used in the output. It can be a string or a t-string (seeformat()).Tasks submitted before the
withblock is exited are started (in submission order, as far as free slots are available) when the block is exited. This method may only be called from the main process; a running task submits further tasks viaTask.submit().
- __exit__(exc_type, exc_value, traceback)[source]
Run all submitted tasks (and the tasks they submit) until all of them are done, then print a summary.
Events from the tasks (log messages, done messages and submissions of new tasks) arrive via a
multiprocessing.Queueand are processed in the main process.If an exception was raised inside the
withblock, no tasks are started and the exception propagates.
- class ll.taskswarm.Task[source]
Bases:
objectA
Taskobject represents one task submitted to aSwarm.It is created by
Swarm.submit()(orTask.submit()) and passed as the first argument to the task function. The task function uses it to communicate with the swarm (which runs in the main process):log()outputs a progress message andsubmit()submits further tasks. The task is finished when the task function returns.Everything sent to the swarm (log messages, names and arguments of new tasks) is pickled by the task itself before it’s put into the
multiprocessing.Queue, so that unpicklable values raise an exception in the task function (instead of silently losing the message in the feeder thread of the queue).The following attributes are available:
process_idintThe unique id of the task inside the swarm.
namestring or t-stringThe name of the task (used in the output, see
Swarm.format()).parentTaskorNoneThe task that submitted this task (or
Nonefor tasks submitted viaSwarm.submit()).submitted_at,started_at,finished_atdatetimeorNoneWhen the task was submitted, started and finished. Note that these are only maintained in the main process, so they are not available inside the task function.
- __str__()[source]
The names of all tasks in
path()joined with::, i.e. the “full name” of the task including its ancestors (without any formatting, seeSwarm.format_task()for that).
- property plain_name
The
nameof the task as a plain string.If the name is a t-string, the interpolated values are simply converted with
str(format specs are ignored).
- property wait_time
How long the task waited for a free slot as a
timedelta(orNoneif it hasn’t started yet).
- property run_time
How long the task ran as a
timedelta(orNoneif it hasn’t finished yet).
- path()[source]
Yield the chain of tasks from the root task down to this task (i.e. all ancestors via
parentfollowed by the task itself).
- log(message)[source]
Output the progress message
messagefor this task.messagecan be a string or a t-string (seeSwarm.format()). All interpolated values of a t-string must be picklable; preformat those that aren’t (e.g.t"{str(obj):sqlobject}").
- submit(func, name, *args, **kwargs)[source]
Submit a new task to the swarm from inside a running task.
The arguments have the same meaning as for
Swarm.submit(). The new task will have this task as itsparent.func,name,argsandkwargsare sent to the main process via amultiprocessing.Queue, so they must be picklable.