任务本地变量具有周期一致性。在一个任务周期中,它们只能由一个已定义的任务写入,而所有其他任务都只能读取。考虑到任务可能会被其他任务打断或同时运行。如果应用程序在多核处理器系统上运行,周期一致性也同样重要。
因此,在多个任务处理相同变量时,使用任务本地全局变量列表是(编译器)自动实现同步的一种方法。使用普通龙胆紫时,情况并非如此。在一个周期内,多个任务可同时写入普通 GVL 变量。
然而,必须指出的是任务本地变量的同步需要相对较多的时间和内存,并不总是每个应用程序的最佳解决方案。因此,请参阅以下更详细的技术信息和最佳实践指南,以帮助您做出正确的决定。
在CODESYS 项目中, “Variable List (Task-Local)” 对象可用于定义任务本地变量。从语法上讲,它与普通的 GVL 相对应,但也包含有变量写访问权限的任务信息。这样,在一个任务周期内,该 GVL 中的所有变量都不会被另一个任务改变。
下一节包含一个简单的示例,演示任务本地变量的原理和功能。它包括写入计划和读取计划。这些程序在不同的任务中运行,但它们访问的是存储在任务本地全局变量列表中的相同数据,因此它们的处理周期是一致的。
举例说明功能
有关重新编程此示例应用程序的说明,请参阅下文。
申请样本
(* 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
“WriteData” 和“ReadData” 这两个程序被不同的任务调用。
在程序WriteData
中,数组g_diaData
已被填入数值。程序ReadData
会测试数组的值是否符合预期。如果是这样,那么变量bTest
会产生结果TRUE
。
被测试的数组数据是通过对象 Tasklocals
中的变量 g_diaData
声明的,对象类型为 Global Variable List (Task-Local)
。这就在编译器中同步了数据访问,并保证了周期一致性,即使访问程序是从不同的任务中调用的。在示例程序中,这意味着变量 test
在程序 ReadData
中始终是 TRUE
。
如果在本例中只将变量 g_diaData
声明为全局变量列表,那么测试(程序 ReadData
中的变量 test
)将更频繁地产生 FALSE
。在这种情况下,这是因为FOR
循环中的两个任务之一可能被另一个任务中断,或者两个任务同时运行(多核控制器)。因此,作者可以在读者读取列表时更改值。
声明中的限制条件




注意

更改任务本地变量列表中的声明后,无法在线更改应用程序。
在声明全局任务-本地变量列表时,请注意以下几点:
-
不要通过 AT 声明分配直接地址。
-
不要映射到控制器配置中的任务本地变量。
-
不要声明任何指针。
-
请勿申报任何参考资料。
-
不要实例化任何功能块。
-
不要同时将任何任务本地变量声明为
PERSISTENT
和RETAIN
。
编译器会将没有写访问权限的任务中的写访问报告为错误。不过,并非所有违规写入都能被检测到。编译器只能为任务分配静态调用。但是,通过指针或接口调用的功能块并不分配给任务等。因此,这里也不会记录任何写入访问。此外,指针还可以指向任务本地变量。因此,数据可以在读取任务中进行操作。在这种情况下,不会出现运行时错误。不过,通过指针访问修改的值不会在变量的共享引用中复制回来。
任务本地全局变量的属性和可能的行为
每个任务的变量都位于列表中不同的地址。对于读取访问,这意味着:ADR(variable name)
在每个任务中产生不同的地址。
同步机制可确保
-
周期一致性
-
摆脱锁定状态:一个任务在任何时候都不会等待另一个任务的操作。
然而,使用这种方法无法确定读取任务何时安全地收到写入任务的副本。从根本上说,副本可能会出现偏差。在上面的例子中,不能断定每份书面副本都是由读者处理一次。例如,读取任务可以在多个周期内编辑同一个数组,或者数组的内容可以在两个周期之间跳过一个或多个值。这两种情况都可能发生,必须加以考虑。
在每个读取任务两次访问共享引用之间,写入任务可以暂停一个周期。这意味着,当n
存在读取任务时,写入任务可能会有n
个周期的延迟,直到共享引用的下一次更新。
在每个任务中,写入任务都可以阻止读取任务获得读取副本。因此,无法规定读取任务一定会收到副本的最长周期数。
特别是在涉及运行速度非常慢的任务时,这可能会成为问题。假设任务每隔一小时才运行一次,并且在此期间无法访问任务本地变量,那么该任务就会使用一份非常旧的列表副本。因此,在任务本地变量中插入时间戳可能很有用,这样读取任务至少可以确定列表是否是最新的。设置时间戳的方法如下:在任务本地变量列表中添加类型为LTIME
的变量,并在编写任务中添加以下代码,例如:tasklocal.g_timestamp := LTIME();
.
最佳做法
任务本地变量是为 "单个写入器-多个读取器 "的使用情况而设计的。当你执行一个由不同任务调用的代码时,使用任务本地变量是一个很大的优势。例如,当示例应用程序appTasklocal
由多个读取任务扩展时就会出现这种情况,这些读取任务都访问同一个数组并使用相同的函数。
任务本地变量在多核系统中尤其有用。在这些系统上,无法按优先级同步任务。那么就需要其他同步机制。
当读取任务必须始终使用变量的最新副本时,请勿使用任务本地变量。任务本地变量不适合用于此目的。
类似的问题还有 "生产者-消费者 "的两难选择。当一个任务产生数据,另一个任务处理数据时,就会出现这种情况。为该配置选择另一种同步类型。例如,制作者可以使用一个标志来通知新日期的存在。然后,消费者可以使用第二个标志来通知它已经处理完数据,正在等待新的输入。这样,两者就可以处理相同的数据。这样就消除了循环复制数据的开销,消费者也不会丢失生产者生成的任何数据。
监测
运行时,内存中可能存在多个不同的任务本地变量列表副本。监控位置时,无法显示所有数值。因此,共享引用的值会显示在任务本地变量的内联监控、监控列表和可视化中。
设置断点时,会显示运行到断点并因此停止的任务数据。与此同时,其他任务继续运行。在某些情况下,共享副本可以更改。但是,在停止任务的情况下,数值保持不变,并按原样显示。你必须意识到这一点。
背景介绍技术实施
对于任务本地变量列表,编译器会为每个任务创建一个副本,并为所有任务创建一个共享引用副本。这将创建一个包含与任务本地变量列表相同变量的结构。此外,还创建了一个具有这种结构的数组,其中为每个任务创建了一个数组维度。因此,每个任务都有一个数组元素索引。如果现在在代码中访问列表中的变量,那么实际上访问的是列表的任务本地副本。此外,还能确定该程序块当前在哪个任务中运行,并据此对访问进行索引。
例如,上述示例中的代码行diValue := TaskLocals.g_diaData[0];
被替换为
diValue := __TaskLocalVarsArray[__CURRENTTASK.TaskIndex].__g_diarr[0];
__CURRENTTASK
是CODESYS V3.5 SP13 及更高版本中的运算符,用于快速确定当前任务索引。
运行时,在写入任务结束时,任务本地列表的内容会被写入全局列表。对于开始时的读取任务,共享引用的内容会复制到任务本地副本。因此,对于 n 个任务,列表有 n+1 份副本:一个清单作为共享参考,每个任务也有自己的清单副本。
调度程序控制多个任务按时间执行,因此也控制任务切换。该策略由调度程序跟踪,以控制执行时间的分配,其目标是防止任务被阻塞。因此,同步机制根据任务局部变量的特性进行了优化,以防止阻塞状态(锁定状态)的出现,而且任务在任何时候都不会等待其他任务的操作。
同步策略:
-
只要编写任务将副本写回共享引用,就不会有任何读取任务获得副本。
-
只要读取任务获得了普通参考文献的副本,写入任务就不会写回副本。
创建上述示例应用程序的说明
目标通过程序ReadData
,您希望访问的数据与程序WriteData
写入的数据相同。这两个程序应在不同的任务中运行。您可以在任务本地变量列表中提供数据,以便以循环一致的方式自动处理。
要求:在编辑器中创建并打开一个全新的标准项目。
-
将应用程序从
Application
重命名为appTasklocal
。 -
在
appTasklocal
下面,在 ST 中添加一个名为ReadData
的程序。 -
在
appTasklocal
下面,在 ST 中添加另一个名为WriteData
的程序。 -
在对象
Task Configuration
下方,将默认任务从MainTask
重命名为Read
。 -
在“任务配置” 对话框
Read
中,单击“添加调用” 按钮调用程序ReadData
。 -
在“任务配置” 对象下面,添加另一个名为
Write
的任务,并在该任务中添加程序Write
的调用。现在,任务配置中有两个任务
Write
和Read
,它们分别调用程序WriteData
和ReadData
。 -
选择应用程序
appTasklocal
并添加类型为“全局变量列表(任务-本地)的对象” 。将打开“Add Global Variable List (Task-Local)” 对话框。
-
指定名称
Tasklocals
。 -
从“有写入权限的任务”列表框中选择
Write
任务。在应用程序中使用任务本地变量的对象结构已经完成。现在,您可以按照上面示例中的描述对对象进行编码。