Asynchronous Job Execution

The critical factor in many applications is the cycle time of a task. The smaller this can be, the more frequently the signals of the processes can be checked, which are to be controlled by the program steps of this task. This minimizes the application's response time for changes in process signals. In particular the Cycle time may be then as short as possible, if the number of instructions processed in one cycle corresponds exactly to the number of instructions strictly necessary to meet the given requirements. If a particular job does not have to be completed in one cycle, and the transfer of this job to another task with a longer cycle time is not an option, the individual calculation steps are assigned to different groups of operations (states) and the execution of each group (state) is moved to one of the following cycles. This means that only the computing time for one statement group is required in the current cycle. This approach reaches its limit when a statement group needs an unpredictable period of time for its calculation, or under certain circumstances can even block the complete program execution. In this case, such a group of statements must be attached to a background task so that the cycle time of the foreground task is not affected. A transport mechanism is required so that the parameters, states, and the calculation results can be exchanged consistently between the foreground and the background task.

Applications in CODESYS

Distribution of Work Load over more the one Cycle

The CODESYS Common Behaviour Model provides a set of function blocks based on the CBML.IActionProvider interface. The typical structure of the CyclicAction method for distributing the work load over more the one cycle my be look like this:

METHOD CyclicAction
VAR_INPUT
    itfTimingController : CBML.ITimingController;
END_VAR
VAR_OUTPUT
    xComplete : BOOL;
    iErrorID : INT;
END_VAR
REPEAT
    // working to reach the ready condition
    // ⇒ xComplete := TRUE
    // if the maximum invocation time is reached
    // ⇒ xTimeLimit := TRUE
    // if the maximum operating time is reached
    // ⇒ xTimeOut := TRUE
    // if an error condition is reached
    // ⇒ set iErrorID to a value other than 0 (Zero)
    itfTimingController.ControllerCheckTiming(
        xTimeOut=>xTimeOut,
        xTimeLimit=>xTimeLimit
    );

    xComplete := TRUE;
    iErrorID := ERROR.NO_ERROR;

UNTIL xComplete OR
      xTimeOut OR xTimeLimit OR
      iErrorID <> ERROR.NO_ERROR
END_REPEAT

The CyclicAction is running until either xComplete is TRUE or iErrorID ≠ 0 (Zero). This holds the fact that as long as xComplete equals FALSE and eErrorID is zero, the method is called in every cycle and can do a little bit of work on a bigger task with every call. With itfTimingController ≠ 0 (Zero) it is possible to check the current invocation time (see: ITimingController.ControllerCheckTiming). Function blocks with a udiTimeLimit input variable can then be implement in such a way that the current invocation is exited when the consumed time for this invocation has exceeded the settings from udiTimeLimit.

This is the recommended procedure if the statements used in relation to the calling program do not have a blocking effect.

CmpAsyncMgr

The component CmpAsyncMgr of the CODESYS runtime system provides the infrastructure for implementing asynchronous job execution. It is possible to define the name and the priority of the background task, enqueue a reference to a method of an function block instance together with its actual parameter set and a specific job state report variable.

The attached CODESYS project illustrates how to handle the different functions and data structures.

From a library developers point of view, the current state of the implementation of the CmpAsyncMgr has some drawbacks.

  • The asynchronous executed method is not executed in the context of an IEC task.

  • It is not possible to remove a pending job from the job queue.

  • Every background task has its own job queue. It is not possible to link more the one task to one job queue

  • It is not possible to assign a background task to a specific task group. So the multi core support is limited.

AsyncJobManager

See: CDS-59649

To solve these issues the new AsyncJobManager.library was specified. The provided infrastructure is completely independent of the CmpAsyncMgr component. The functionality of the CmpIecTask component was utilised to realize a background task with an IEC context (IBackGroundTask).

FUNCTION_BLOCK BackGroundTask EXTENDS CBML.LConC IMPLEMENTS IBackGroundTask, FBF.IInstance
VAR_INPUT CONSTANT
    tgTaskGroup : TASK_GROUP;
    anAppName : APP_NAME;
    tnTaskName : TASK_NAME;
    usiTaskPrio : USINT;
    udiTaskInterval : UDINT;
    xWatchdogEnabled : BOOL;
    udiWatchdogTime : UDINT;
    usiWatchdogSensitivity : USINT;
END_VAR
VAR_INPUT
    itfParams : SHD.ISharedQueue;
    itfAction : IAsyncActionProvider;
END_VAR
VAR_OUTPUT
    eErrorID : ERROR;
    itfResults : SHD.ISharedArea;
    dwCurrentCycleTime : DWORD;
    dwAverageCycleTime : DWORD;
    dwMaxCycleTime : DWORD;
    dwMinCycleTime : DWORD;
    dwCycleCounter : DWORD;
END_VAR
INTERFACE IBackGroundTask EXTENDS __SYSTEM.IQueryInterface
METHOD TaskDisableScheduling : BOOL
METHOD TaskEnableScheduling : BOOL
METHOD TaskDisableWatchdog : BOOL
METHOD TaskEnableWatchdog : BOOL
METHOD TaskResetStatistics : BOOL

Separated from the implementation of a function block for abstracting a background task, a additional interface specification for actions (IAsyncActionProvider) was made.

The action is modeled as a method of a function block instance (AsyncAction) and will be cyclically called until the return value xComplete indicating a condition that the asynchronous operation is now completed and the method needs no further calling. The input parameter itfParam is a generic pointer to a data structure containing the current parameter values (SHD.IQueueableNode).

INTERFACE IAsyncActionProvider EXTENDS __SYSTEM.IQueryInterface
METHOD AsyncAction
VAR_INPUT
    itfParam : SHD.IQueueableNode;
END_VAR
VAR_OUTPUT
    xComplete : BOOL;
END_VAR
PROPERTY GET AsyncResult : SHD.ISharedArea

The following use cases where in mind while specifying this library.

Simple One-To-One Relationship

  • One action (IAsyncActionProvider) is assigned to one background task.

  • The current foreground task fills the parameter queue with parameter sets.

  • The result of the background task is available via the AsyncResult property.

Some Background Tasks are sharing Parameters and Results

  • Every instance of a BackgroundTask function block is connected to one and the same instance of an IAsyncActionProvider.

  • One parameter queue is connected to the group of BackgroundTasks. Each task will fetch one of the next parameter sets to its context for executing the IAsyncActionProvider.AsyncAction with this set of parameters.

  • The results of these joint efforts are available for the foreground task through the common SHD.ISharedArea.

Several Background Tasks Are Connected In A Row

Like on a conveyor belt, the BackgroundTasks are connected in series via their parameter/result queues. Each task processes a portion of a chain of transformations and passes its result on to its subsequent task.

The Parameter Queue feeding the Background Task

The "SharedData Utilities" library provides the interface SHD.ISharedQueue.

INTERFACE ISharedQueue EXTENDS __SYSTEM.IQueryInterface
METHOD Dequeue : IQueueableNode
VAR_OUTPUT
    /// Insertion point in time
    ltTimeStamp : LTIME;
    eErrorID : ERROR;
END_VAR

METHOD Enqueue : ERROR
VAR_INPUT
    itfNode : IQueueableNode;
END_VAR

A instance of the BackgroundTask function block will get out the parameters (via Enqueue method) for its IAsyncActionProvider instance. One possible implementation can be the SHD.SharedQueue which is defined in the "SharedData Utilities" library.

Example: Connecting Parameters to a BackgroundTask function block

VAR
    sqParameters : SHD.SharedQueue;
    itfActionProvider : AJM.IAsyncActionProvider (* := myActionProvider *);
    bgtBackgroundTask : AJM.BackgroundTask := (
        tgTaskGroup:='IEC-Tasks',
        anAppName:='Application',
        tnTaskName:='BackgroundTask',
        usiTaskPrio:=10,
        udiTaskInterval:=50000,
        itfParams := sqParameters,
        itfAction := itfActionProvider
    );
END_VAR

To feed the BackgroundTask with new parameters the call of the method ISharedQueue.Enqueue is necessary. The related paramter structure is behind the itfNode reference.

INTERFACE IQueueableNode EXTENDS __SYSTEM.IQueryInterface
METHOD NodeDispose
PROPERTY GET IsNodeValid : BOOL

Behind the IQueueableNode any proper implementation is possible. Thus the parameter structure is freely customizable and can be very well adapted to special requirements.

FUNCTION_BLOCK Parameter IMPLEMENTS SHD.IQueueableNode
VAR_INPUT
    (* Any required data structure *);
END_VAR

The method NodeDispose and the property IsNodeValid are utilized to handle resource management and provide the possibility to mark a parameter set as not valid any more while it is staying in the queue.

The Background Task's Result

The SHD.ISharedArea interface is defined in the "SharedData Utilities" library. The implementation behind provides a consistent transport of data structures for example between multiple cores of a processor.

INTERFACE ISharedArea EXTENDS __SYSTEM.IQueryInterface
METHOD AreaSetObserver : ISharedAreaObserver
VAR_INPUT
    itfAreaObserver : ISharedAreaObserver;
END_VAR
VAR_OUTPUT
    eErrorID : ERROR;
END_VAR

One possible implementation of SHD.ISharedAreaObserver can be the SHD.SharedQueue which is defined in the "SharedData Utilities" library.

Example: Handling the results of a BackgroundTask function block

VAR
    sqResults : SHD.SharedQueue;
    xObserved : BOOL;
    itfNode : SHD.IQueueableNode;
    eErrorID : SHD.ERROR;
    bgtBackgroundTask : AJM.BackgroundTask;
END_VAR
bgtBackgroundTask();

IF bgtBackgroundTask.xBusy THEN
    IF NOT xObserved THEN
        bgtBackgroundTask.itfResult.AreaSetObserver(sqResult);
        xObserved := TRUE;
    END_IF

    itfNode := sqResult.Dequeue(eErrorID=>eErrorID);
    __QUERYINTERFACE(itfNode, itfSharedAreaRef);
    IF itfSharedAreaRef <> 0 THEN

        (* Process the results *)

        itfNode.NodeDispose();
        itfNode := 0;
        itfSharedAreaRef := 0;
    END_IF
END_IF
Note

With CDS-58218 a new operator will be available.

__CurrentTask

It gives access to a structure containing two members:

  • TaskIndex and

  • pTaskInfo.

TaskIndex is a zerobased index identifying the task, pTaskInfo can be assigned to a POINTER TO Task_Info2 from the CmpIecTask library to get detailed information about the executing task.

Using the IecTaskGetCurrent function was until today the usual way to get information about the currently running task.