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