Task construct ============== .. index:: task The programmer can specify a task using the ``task`` construct. This construct can appear inside any code block of the program, which will mark the following statement as a task. .. highlight:: c The syntax of the ``task`` construct is the following:: #pragma oss task [clauses] structured-block The valid clauses for the ``task`` construct are: * ``private()`` * ``firstprivate()`` * ``shared()`` * ``depend(: )`` * ``()`` * ``reduction(:)`` * ``priority()`` * ``cost()`` * ``if()`` * ``final()`` * ``wait`` * ``onready()`` * ``label()`` * ``create()`` The ``private``, ``firstprivate`` and ``shared`` clauses allow to specify the data sharing attribute of the variables referenced in the construct. A description of these clauses can be found in :ref:`Data sharing attributes` section. The ``depend`` clause allows to infer additional task scheduling restrictions from the parameters it defines. These restrictions are known as dependences. The syntax of the ``depend`` clause include a dependence type, followed by colon and its associated list items. The list of valid type of dependences are defined in section *Dependence model* in the previous chapter. In addition to this syntax, OmpSs-2 allows to specify this information using the type of dependence as the name of the clause. Then, the following code:: #pragma oss task depend(in: a,b,c) depend(out: d) Is equivalent to this one:: #pragma oss task in(a,b,c) out(d) The ``reduction`` clause allows to define the task as a participant of a reduction operation. The first occurrence of a participating task defines the begin of the scope for the reduction. The end of the scope is implicitly ended by a taskwait or a dependence over the ``memory-reference-item``. More information about task reductions on OmpSs-2 at the following Master Thesis: ``__. The ``priority`` clause indicates a priority hint for the task. Greater numbers indicate higher priority, and lower numbers indicate less priority. By default, tasks have priority 0. The expression of the priority is evaluated as a singed integer. This way, strictly positive priorities indicate higher priority than the default, and negative priorities indicate lower than default priority. If the expression of the ``if`` clause evaluates to *true*, the execution of the new created task can be deferred, otherwise the current task must suspend its execution until the new created task has complete its execution. If the expression of the ``final`` clause evaluates to *true*, the new created task will be a final task and all the *task generating code* encountered when executing its *dynamic extent* will also generate final tasks. In addition, when executing within a final task, all the encountered *task generating codes* will execute these tasks immediately after its creation as if they were simple routine calls. And finally, tasks created within a final task can use the data environment of its parent task. Tasks with the ``wait`` clause will perform a taskwait-like operation immediately after exiting from the task code. Since it is performed outside the scope of the code of the task, this happens once the task has abandoned the stack. For this reason, its use is restricted to tasks that upon exiting do not have any subtask accessing its local variables. Otherwise, the regular taskwait shall be used instead. The ``onready`` clause allow defining an action in the form of a statement (e.g., a call to a function) that will be executed once the task becomes ready. This is explained in more detail in section :ref:`Task Onready clause`. The ``label`` clause defines a string literal that can be used by any performance or debugger tool to identify the task with a more *human-readable* format. The string literal must be wrapped in double quotes. For instance, a task that initializes an array could be labeled as ``label("init array")``. The following C code shows an example of creating tasks using the ``task`` construct:: float x = 0.0; float y = 0.0; float z = 0.0; int main() { #pragma oss task do_computation(x); #pragma oss task { do_computation(y); do_computation(z); } #pragma oss taskwait return 0; } When the control flow reaches ``#pragma oss task`` construct, a new task instance is created. The execution order of the tasks is not guaranteed. Moreover, when the program reaches the ``#pragma oss taskwait`` the previously created tasks may not have been executed yet by the OmpSs-2 run-time system. After potentially being blocked in the ``taskwait`` construct for a while, it is guaranteed that both tasks have already deeply completed. The task construct is extended to allow the annotation of function declarations or definitions in addition to structured-blocks. When a function is annotated with the task construct each invocation of that function becomes a task creation point. The following C code is an example of how task functions are used:: extern void do_computation(float a); #pragma oss task extern void do_computation_task(float a); float x = 0.0; int main() { do_computation_task(x); //this will create a task do_computation(x); //regular function call #pragma oss taskwait } The invocation of ``do_computation_task`` inside ``main`` function creates an instance of a task. Note that OmpSs-2 does not gaurantee that the task has been already executed after returning from the regular function call ``do_computation(x)``. Note that only the execution of the function itself is part of the task not the evaluation of the task arguments. Another restriction is that the task is not allowed to have any return value, that is, the return must be void. .. warning:: The ``for`` clause from the ``task`` directive is no longer part of OmpSs-2. Task Onready clause ------------------- .. index:: onready The ``onready`` clause allows defining an action in the form of a statement (e.g., a call to a function) that will be executed once the task becomes ready. The run-time system will execute the statement only once, at any moment after the task satisfies all its data dependencies and before the task runs its body. The onready action cannot assume that is running within a task context; it should not reach any task scheduling point. Moreover, the action is recommended to be lightweight and should not perform blocking operations. The onready action can register external events to the ready task to delay its execution until all the events are fulfilled. As an example, the callback could execute an asynchronous `TAMPI `__ operation, such as ``TAMPI_Iwait``. Such a call would delay the task's execution until the corresponding MPI communications are completed. In that way, the data dependencies allow tasks to define local dependencies with other tasks on the same process, whereas the ``onready`` clause allows defining remote dependencies with other processes. See :ref:`Task external events` for more information about the management of task external events. The data sharing and dependency rules for the variables used in the ``onready`` action are the same that apply to the task body. We show an example below where a task safely increases by two the value of a variable from the ``onready`` action and the task body:: void function(int *a) { // This is the first increase because it's the onready action *a += 1; } int main() { int a = 0; #pragma oss task inout(a) onready(function(&a)) { // At this point, the onready action was already executed ++a; } #pragma oss taskwait fprintf(stdout, "a: %d\n", a); // Should print "a: 2" } Below there is an example where we asynchronously receive (through TAMPI services) and process remote data using a single task with an ``onready`` action:: void function(int *a, int rank) { MPI_Request request; MPI_Irecv(a, 1, MPI_INT, src, 0, MPI_COMM_WORLD, &request); // Asynchronously delay the execution of the task until the communication // has completed. This service will register an external event if the // receive has not completed immediately TAMPI_Iwait(&request, MPI_STATUS_IGNORE); } int main() { // Initialize MPI... int a = 0; int rank = ...; #pragma oss task inout(a) onready(function(&a, rank)) { fprintf(stdout, "received data: %d\n", a); process(a); } #pragma oss taskwait // Finalize MPI... } Please note this is just an example of how ``onready`` can be used to delay the execution of a task by registering external events. Performing heavy operations like ``MPI_Irecv`` in an ``onready`` action is not recommended because the ``onready`` action does not run in the context of a task. Task Create clause (experimental) --------------------------------- .. index:: create .. important:: The create clause is experimental and may change or be removed in the future without any notice. The optional ``create`` clause can inhibit the creation of a task and just execute the body directly. It is typically used to override the behavior of the ``final`` clause for an specific task. The clause expects a conditional argument ``create(cond)`` which is evaluated when the task is to be created. If the condition evaluates to *true* the task is created, even if the task is final. Otherwise, if evaluates to false, the task is never created. The following ``create`` clause:: #pragma oss task create(cond) do_work(size) Is equivalent to this code:: if (cond) { #pragma oss task create(true) do_work(size) } else { do_work(size) } An example of the ``create`` clause to override the final effect for some specific tasks is depicted in the following diagram: .. image:: ../images/create-true.png :width: 60% :align: center The tasks in grey won't be created due to the final clause, but the ones in green with the ``create(true)`` clause, will always be created. A task without the `create` clause will follow the normal creation process, following the rules imposed by the `final` clause if used. The condition used in the create clause must not have side-effects, otherwise the behavior is undefined. For example, ``create(size++ > 20)`` may or may not increase the value of the variable ``size``. It is important to note that when the task is not created and the body runs as-is, the dependencies or data sharing clauses of that task won't have any effect (as there won't be any task). It is the programmer responsibility to ensure that the program it is still correct.