Task-local variables are cycle-consistent. In a task cycle, they are written only by a defined task, while all other tasks have read-only access. It is taken into account that tasks can be interrupted by other tasks or can run simultaneously. The cycle consistency also applies above all if the application is running on a system with a multicore processor.
Therefore, using task local global variable lists is one way to automatically achieve a synchronization (by the compiler) when multiple tasks are processing the same variables. This is not the case when using ordinary GVLs. Multiple tasks can write simultaneously to ordinary GVL variables during a cycle.
However, it is imperative to note: The synchronization of task-local variables requires a relatively large amount of time and memory and is not always the best solution for every application. For this reason, see below for more detailed technical information and best practice guidance to help you make the right decision.
In the CODESYS project, the “Variable List (Task-Local)” object is available for defining task-local variables. Syntactically, it corresponds
to a normal GVL, but also contains the information of the task that has write access
to the variables. Then all variables in such a GVL are not changed by another task
during a cycle of a task.
The next section contains a simple example that demonstrates the principle and functionality of task-local variables. It includes a writing program and a reading program. The programs run in different tasks, but they access the same data that is stored in a task-local global variable list so that they are processed cycle-consistently.
Showing functionality in an example
See below for Instructions on reprogramming this sample application.
Sample application
(* task-local GVL, object name: "Tasklocals" *) VAR_GLOBAL g_diaData : ARRAY [0..99] OF DINT; END_VAR PROGRAM ReadData VAR diIndex : DINT; bTest : BOOL; diValue : DINT; END_VAR bTest := TRUE; diValue := TaskLocals.g_diaData[0]; FOR diIndex := 0 TO 99 DO bTest := bTest AND (diValue = Tasklocals.g_diaData[diIndex]); END_FOR PROGRAM WriteData VAR diIndex : DINT; diCounter : DINT; END_VAR diCounter := diCounter + 1; FOR diCounter := 0 TO 99 DO Tasklocals.g_diaData[diIndex] := diCounter; END_FOR
The programs “WriteData” and “ReadData” are called by different tasks.
In the program WriteData
, the array g_diaData
is populated with values. The program ReadData
tests whether or not the values of the array are as expected. If so, then the variable
bTest
yields the result TRUE
.
The array data that is tested is declared via the variable g_diaData
in the object Tasklocals
of type Global Variable List (Task-Local)
. This synchronizes the data access in the compiler and guarantees cycle consistency,
even when the accessing programs are called from different tasks. In the sample program,
this means that the variable test
is always TRUE
in the program ReadData
.
If the variable g_diaData
were declared only as a global variable list in this example, then the test (the
variable test
in the program ReadData
) would yield FALSE
more often. In this case, this is because one of the two tasks in the FOR
loop could be interrupted by the other task, or both tasks could run simultaneously
(multicore controllers). And therefore the values could be changed by the writer while
the reader reads the list.
Constraints in the declaration




NOTICE

An online change of the application is not possible after changes in declarations in the list of task-local variables.
Note the following when declaring a global task-local variable list:
-
Do not assign direct addresses by means of an AT declaration.
-
Do not map to task-local variables in the controller configuration.
-
Do not declare any pointers.
-
Do not declare any references.
-
Do not instantiate any function blocks.
-
Do not declare any task-local variables as
PERSISTENT
andRETAIN
at the same time.
The compiler reports write access in a task without write access as an error. However, not all write-access violations can be detected. The compiler can only assign static calls to a task. However, the call of a function block by means of a pointer or an interface is not assigned to a task, for example. As a result, any write access is not recorded there either. Moreover, pointers can point to task-local variables. Therefore, data can be manipulated in a read task. In this case, a runtime error is not issued. However, values that are modified by means of pointer access are not copied back in the shared reference of variables.
Properties of task-local global variables and possible behavior
The variables are located at a different address in the list for each task. For read
access, this means: ADR(variable name)
yields a different address in each task.
The synchronization mechanism guarantees the following:
-
Cycle consistency
-
Freedom from locked states: A task never waits for an action from another task at any time.
With this method, however, no time can be determined when a reading task securely receives a copy of the writing task. Fundamentally, the copies can deviate. In the example above, it cannot be concluded that each written copy is processed one time by the reader. For example, the reading task can edit the same array over multiple cycles, or the contents of the array can skip one or more values between two cycles. Both can occur and have to be considered.
The writing task can be paused for one cycle between two accesses to the shared reference
by each reading task. This means that when n
reading tasks exist, the writing task can have n
cycles of delay until the next update of the shared reference.
In each task, the writing task can prevent a reading task from getting a reading copy. As a result, no maximum number of cycles can be specified after which a reading task will definitely receive a copy.
In particular, this can become problematic if very slow running tasks are involved.
Assuming a task runs only every hour and cannot access the task-local variables during
this time, then the task works with a very old copy of the list. Therefore, it can
be useful to insert a time stamp in the task-local variables so that the reading tasks
can at least determine whether or not the list is up-to-date. You can set a time stamp
as follows: Add a variable of type LTIME
to the list of task-local variables and add the following code to the writing task,
for example: tasklocal.g_timestamp := LTIME();
.
Best practice
Task-local variables are designed for the use case "Single writer - multiple readers".
When you implement a code that is called by different tasks, using task-local variables
is a significant advantage. For example, this is the case for the sample application
appTasklocal
as described above when it is extended by multiple reading tasks that all access
the same array and use the same functions.
Task-local variables are especially useful on multicore systems. On these systems, you cannot synchronize tasks by priority. Then other synchronization mechanisms become necessary.
Do not use task-local variables when a reading task always has to work on the newest copy of the variable. Task-local variables are not suitable for this purpose.
A similar issue is the "Producer - Consumer" dilemma. This happens when a task produces data and another task processes the data. Choose another type of synchronization for this configuration. For example, the producer could use a flag to notify that a new date exists. Then the consumer can use a second flag to notify that it has processed its data and is waiting for new input. In this way, both can work on the same data. This removes the overhead for cyclic copying of data, and the consumer does not lose any data generated by the producer.
Monitoring
At runtime, multiple different copies of the task-local variable list may exist in memory. When monitoring a position, not all values can be displayed. Therefore, the values from the shared reference are displayed for inline monitoring, in the watch list, and in the visualization for a task-local variable.
When you set a breakpoint, the data of the task is displayed that ran to the breakpoint and was halted as a result. Meanwhile, the other tasks continue running. Under certain circumstances, the shared copy can be changed. In the context of the halted task, however, the values remain unchanged and are displayed as they are. You need to be aware of this.
Background: Technical implementation
For a list of task-local variables, the compiler creates a copy for each task, as well as a shared reference copy for all tasks. This creates a structure that contains the same variables as the list of task-local variables. Moreover, an array with this structure is created in which an array dimension is created for each task. As a result, an array element is indexed for each task. If a variable in the list is accessed now in the code, then the task-local copy of the list is actually accessed. Furthermore, it is determined in which task the block is currently running and the access is indexed accordingly.
For example, the line of code diValue := TaskLocals.g_diaData[0];
from the above example is replaced by:
diValue := __TaskLocalVarsArray[__CURRENTTASK.TaskIndex].__g_diarr[0];
__CURRENTTASK
is an operator that is available in CODESYS V3.5 SP13 and later in order to determine the current task index quickly.
At runtime, at the end of the writing task, the contents of the task-local list are written to the global list. For a reading task at the beginning, the contents of the shared reference are copied to the task-local copy. Therefore, for n tasks, there are n+1 copies of the list: One list serves as a shared reference and every task also has its own copy of the list.
A scheduler controls the time-based execution of multiple tasks and therefore also task switching. The strategy, which is tracked by the scheduler in order to control the allocation of the execution time, has the goal of preventing a task from being blocked. The synchronization mechanism is therefore optimized to the properties of task-local variables to prevent blocking states (lock states) and at no time does a task wait for the action of another task.
Synchronization strategy:
-
As long as the writing task writes a copy back to the shared reference, none of the reading tasks gets a copy.
-
As long as a reading task gets a copy of the common reference, the writing task does not write back a copy.
Instructions for creating the sample application as described above
Aim: With a program ReadData
, you want to access the same data that is written by a program WriteData
. Both programs should run in different tasks. You make the data available in a task-local
variable list so that it is processed automatically in a cycle-consistent manner.
Requirement: A brand new standard project is created and open in the editor.
-
Rename the application from
Application
toappTasklocal
. -
Below
appTasklocal
, add a program in ST namedReadData
. -
Below
appTasklocal
, add another program in ST namedWriteData
. -
Below the object
Task Configuration
, rename the default task fromMainTask
toRead
. -
In the “Configuration” dialog of the task
Read
, click the “Add Call” button to call the programReadData
. -
Below the “Task Configuration” object, add another task named
Write
, and add the call of the programWrite
to this task.Now there are two tasks
Write
andRead
in the task configuration which call the programsWriteData
andReadData
, respectively. -
Select the application
appTasklocal
and add an object of type “Global Variable List (Task-Local)”.The “Add Global Variable List (Task-Local)” dialog opens.
-
Specify the name
Tasklocals
. -
Select the
Write
task from the “Task with write access” list box.The object structure for using task-local variables within an application is complete. Now you can code the objects as described in the example above.