Parallel Task - Quick Reference Guide


This guide serves as a quick reference for the features supported by Parallel Task, including the new keywords, the syntax to use and the most important library functions you will probably use.

Keywords

Keywords for a Task declaration:

    • TASK
    • TASK(*)
    • IO_TASK
    • IO_TASK(*)

Keywords for a Task invocation:

    • dependsOn
    • notify 1
    • notifyInterim
    • asyncCatch


Important library functions


For a complete list of the runtime classes and methods, browse the API docs. But for starters, the most common classes that you will make use of include ParaTask, CurrentTask, TaskID and TaskIDGroup:


    Setting the ParaTask runtime environment:


    Working from inside a task:
  • If you are "inside" a task and want to inquire certain information, have a look at the CurrentTask class. Some of the useful functions include:
TASK public int parallel() {
    return sequential();
}

public int sequential() {
    ...
    if (CurrentTask.insideTask()) {
       // this is being executed as a task
    } else {
       // this is being executed as a standard sequential method
    }
    ...
}
    • plus many more methods, categorised below into their respective topic...
   

    Working from outside a task:

  • While CurrentTask contained methods to be invoked while inside a task, there is usually an equivalent method that may be invoked on a TaskID instance (i.e. for code "outside" the task). Some of these are categorised in the topics below, while others include:


    Task progress:
  • A task may update its progress using CurrentTask.setProgress(int)
  • Listeners interested in a task's progress use getProgress() on the task's respective TaskID. 
  • TIP: Updating the task's progress does not automatically notify the listeners, so you will most likely want to use the publish an intermediate result (or a "dummy" result) to notify the listeners of the task's updated progress.


    Canceling tasks:
  • Tasks cannot be canceled abruptly. Instead, a cancel request must be sent to the task (via it's TaskID instance). If that task has not started executing yet, then it won't be executed. If it already has started executing, then the task must actively check whether a request has been made (and gracefully cancel on its own terms):
    • To attempt to cancel a task, invoke cancelAttempt() on the task's respective TaskID.
    • For the cancel attempt to have any effect, the task should periodically call CurrentTask. cancelRequested() to check if a request has been made. If so, then the task would take the necessary steps to clean up and end the computation.


    Working with multi-tasks:

  • To define a multi-task, annotate a method declaration with either TASK(*) or IO_TASK(*). By default, this creates a sub-task for each worker thread.
    • Alternatively, instead of using '*', you may use an integer literal or interger variable to create a different sized multi-task.. e.g. TASK(myInt)  or  IO_TASK(2)
  • A invocation of a multi-task is like a standard task invocation, except that a TaskIDGroup is returned.
  • From inside the multi-task, inquire to see how many siblings (i.e. total number of sub-tasks) there are for the multi-task by calling CurrentTask.multiTaskSize().
  • Similarly, a sub-task may inquire on its respective position in the multi-task group relative to its siblings using CurrentTask.relativeID().
  • Each multi-task has a barrier allowing it to synchronise with its siblings, achieved by calling CurrentTask.barrier().
  • If the multi-task returns a result, you may access individual results using getInnerTaskResult(int) or perform a reduction to return a single result using getReturnResult(Reduction).
  • TIP: remember that multi-tasks are also tasks.. you may therefore use all the other features discussed!
 

    Task dependences
  • Sometimes you want to invoke a task, but don't want it to start executing until a previous task has completed. You may specify this dependency with the dependsOn keyword. The variables inside the dependsOn clause must be TaskIDs or TaskIDGroups (and you may specify as many of each as you want)  e.g.
TaskIDGroup grp = new TaskIDGroip(2);

TaskID id1 = firstTask();
TaskID id2 = secondTask();
grp.add(id1);
grp.add(id2);
TaskID id3 = thirdTask();

TaskID id = finalTask() dependsOn(grp, id3);
  • A new way of specifying dependences is to pass a TaskID<T> directly to a method that expects a parameter of type T. This signals to the compiler that the method is to be called once the first task completes with the return value of this TaskID as the parameter. In other words, the following two blocks of code are equivalent:
TASK public int firstTask() {
    return something();
}
TASK public void secondTask(TaskID<Integer> id) {
    System.out.println(id.getReturnResult());
}

TaskID id1 = firstTask();
TaskID id2 = secondTask(id1) dependsOn(id1);

// can be shortened to

TASK public int firstTask() {
    return something();
}
TASK public void secondTask(int value) {
    System.out.println(value);
}

TaskID id1 = firstTask();
TaskID id2 = secondTask(id1);


    Non-blocking task completion:

  • If you don't want to block on the TaskID until the task completes (especially important for event processing threads), then you may use one of the non-blocking clauses.
    • notify, the methods inside the clause are executed by the GUI thread (Event Dispatch Thread), regardless of the enqueuing thread, e.g.
TaskID id = myTask() notify( taskCompleted(TaskID), myGUIComponent::update() );
  • TIPS:
    • If you don't specify an object instance, then this is used. Otherwise, you may specify another instance object to invoke the method on using the format instance::method as in the example above.
    • You may specify any number of methods in the notify clause, no need to put them all in one method.
    • The methods you register in the clauses must either accept a TaskID as parameter, or nothing at all. The TaskID parameter will always refer to the task that just completed.


    Intermediate results:
  • When a task wants to publish an intermediate result, it may do so using CurrentTask.publishInterim(E)
  • To have any effect, there must be registered methods (using notifyInterim). The parameter list of those methods must be (TaskID, E) in that order, where:
    • The TaskID instance will represent the task that published the intermediate result, and
    • E will represent the actual intermediate result.
  • TIPS:
    • Just before publishing the result, the task could update it's progress (i.e. CurrentTask.setProgress(int)), and then the method would be able to read the task's progress using getProgress() on the TaskID instance.
    • The notifyInterim is similar to notify. The difference is that an extra parameter is added to the methods (i.e. the E), and the methods are only executed when/if the task decides to publish a result.


    Exception handling
  • Some tasks, like standard methods, might throw exceptions. If these exceptions are checked exceptions, then the programmer must adhere to Java's Catch or Specify Requirement and use the asyncCatch clause, e.g.
TASK public void process(File file) throws IOException {
    ...
}

TaskID id = process(file) asyncCatch(  IOException            fileHandler(TaskID),
                                                             RuntimeException  myHandler(TaskID),
                                                             Exception               endProgram() );
  • In the above example, we MUST use asyncCatch to catch the checked exception (IOException). We decided to also use it to catch any other non-checked exceptions (i.e. optional usage). Similar to the notify clause, the methods inside the asyncCatch clause will be executed by the enqueuing thread.
  • Below is an example of an exception handler:
public void fileHandler(TaskID id) {
    Exception e = id.getException();
    Object[] args = id.getTaskArguments();
    File f = (File) args[0];
    System.err.println("Problem processing task "+id.getTaskName()+" with file "+f.getName());
    e.printStackTrace();
}

    Pipelines
  • Pipelines are useful in situations where data arrives in a streamed format, such as data over a network or data that is too large to all fit in memory at once. Tasks can be converted to efficiently loop on independent threads as pipeline stages instead of as one-off tasks by passing a BlockingQueue<T> in place of a parameter that expects a value of type T.
TASK public Image blur(Image input) {
    Image blurred = ...;
    return blurred;
}

TASK public Image filter(Image input) {
    Image filtered = ...;
    return filtered;
}

TASK public Image resize(Image input) {
    Image resized = ...;
    return resized;
}

BlockingQueue<Image> input = new LinkedBlockingQueue<>();
TaskID<Image> stage1 = blur(input);
TaskID<Image> stage2 = filter(stage1);
TaskID<Image> stage3 = resize(stage2);

//...

input.put(image1);
input.put(image2);
input.put(image3);

//...

BlockingQueue<Image> output = stage3.getOutputQueue();
Image out1 = output.take();
Image out2 = output.take();
Image out3 = output.take();

//...

stage1.cancelAttempt(); // flows down through all stages



1. The keyword notifyGUI and notifyInterimGUI in the old versions have been removed in the latest version.