机器视觉 HDevelop语言基础-多线程

Posted 沧海一笑-dj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了机器视觉 HDevelop语言基础-多线程相关的知识,希望对你有一定的参考价值。

00. 目录

01. 概述

HDevelop 语言支持主线程中的子线程并行执行程序和调用算子。 一旦启动,子线程由线程 ID 标识,该线程 ID 是一个取决于操作系统的整数进程号。 子线程的执行独立于它们启动的线程。 因此,无法预测子线程执行结束确切时间点。 如果要访问一组线程返回的数据,则需要显式等待相应的线程完成。

HDevelop 默认将线程数限制为 20。 如果需要,可以在首选项中修改此数字。 完全限制并发线程数的主要原因是为了防止用户由于编程错误而无意中生成大量线程。 在这种情况下,系统负载和内存消耗可能会增长得如此之高,以至于 HDevelop 可能会变得无响应。
在这里插入图片描述

请注意,线程数包括所有“活动”线程。 特别是它还包括已经完成但仍在被变量引用的线程。 在调整线程限制时必须考虑到这一点。 有关线程生命周期的信息,另请参阅第 307 页的“HDevelop 中的线程执行”部分。

02. 启动线程

要启动一个新线程,请在相应的运算符或过程调用前加上 par_start 限定符:

par_start <ThreadID> : gather_data()
...

此调用在后台启动假设过程gather_data() 作为新的子线程并继续执行后续程序行。 线程 ID 在变量 ThreadID 中返回,该变量必须在尖括号中指定。 与 HDevelop 不同,在 HDevEngine 中,给定的 ThreadID 仅在启动线程的过程中有效。

请注意, par_start 不是实际的算子,而只是修改调用行为的限定符。 因此,无法在操作员窗口中选择 par_start。

如果启动新的子线程会超过配置的最大线程数(见上文),则会引发异常。

您还可以从算子窗口中将过程或算子调用作为子线程启动。 为此,打开算子窗口底部的高级并行化选项部分,勾选复选框并输入将保存线程 ID 的变量的名称。 如果双击包含 par_start 限定符的程序,并行化选项也将显示在孙子窗口中。 对于某些程序行(例如,注释、声明、循环或赋值),不支持 par_start,并且相应的选项在孙子窗口中也不可用。

支持在一个循环中启动多个线程。 在这种情况下,需要收集线程 ID,以便以后可以引用所有线程:

ThreadIDs := []
for Index := 1 to 5 by 1
par_start <ThreadID> : gather_data()
ThreadIDs := [ThreadIDs, ThreadID]
endfor

在容器变量中收集线程 ID 通常更方便:

for Index := 1 to 5 by 1
par_start <ThreadIDs.at(Index - 1)> : gather_data()
endfor

当子线程在输出变量中返回数据时,必须特别小心。 特别是,当子线程仍在运行时,不得在其他线程中访问输出变量。 否则无法保证数据有效。

同样,必须确保多个线程不会干扰它们的结果。 假设过程gather_data像上面一样作为多个线程启动,但在输出控制变量中返回数据:

for Index := 1 to 5 by 1
par_start <ThreadIDs.at(Index - 1)> : gather_data(Result) // BEWARE!!!
endfor

在上面的例子中,所有线程都将在同一个变量中返回它们的结果,这肯定不是预期的。 Result 的最终值将是最后完成的线程的(不可预测的)返回值,所有其他结果都将丢失。

这个问题的一个简单解决方案是将返回的数据收集到一个容器变量中,如前面所示,带有线程 ID:

for Index := 1 to 5 by 1
par_start <ThreadIDs.at(Index - 1)> : gather_data(Result.at(Index - 1))
endfor

在这里,每次调用gather_data 都会在容器变量Result 的唯一ID中返回其结果。

03. 等待子线程结束

使用运算符 par_join 等待单个线程或一组线程的完成。 举个例子,假设我们想调用一个在后台执行一些魔术计算并返回一个计数值作为结果的过程。 在随后的程序行中,我们希望使用该数字进行进一步的计算。

par_start <ThreadID> : count_objects(num)
...
for i := 1 to num by 1 // BEWARE: num might be uninitialized
...
endfor

仅仅依靠子线程足够快很可能会失败。 因此,需要事先显式调用 par_join。

par_start <ThreadID> : count_objects(num)
...
par_join(ThreadID)
for i := 1 to num by 1
...
endfor

请注意,在 HDevelop 中,并不严格要求使用 par_join,因为主线程将始终比子线程寿命更长。 但是,如果程序要在 HDevEngine(参见程序员指南,第 116 页)中执行或导出为编程语言,则省略它可能会导致问题。 同样,如果要导出程序,对全局变量的访问可能需要一些额外的同步。 鉴于上一节中的示例,使用以下几行可以实现等待循环中启动的所有线程完成。

convert_vector_to_tuple(ThreadIDs, Threads)
par_join(Threads)

请注意,线程 ID 已收集在容器变量中。 因此,要使 par_join 正常工作,必须转换为元组。
par_join 运算符会阻止调用它的过程的进一步执行,直到所有指定的线程都完成为止。 在随后的程序行中,可以可靠地访问来自相应线程的结果。

04. HDevelop中线程的执行

一般情况下,HDevelop 中的线程只有在按 F5 后程序连续运行时才会并行执行。 在所有其他执行模式中,只有选定的线程被启动,所有其他线程保持停止,除非明确的用户交互推进它们的执行。 活动断点、停止指令、运行时错误或未捕获的异常也会导致所有线程停止,以便可以评估它们的当前状态。 此约定可实现明确定义的调试过程,因为它消除了来自其他线程的无法控制的副作用。 程序窗口中的任何编辑操作也会导致同时运行的程序停止。

线程不能在外部被“杀死”。 它们可以在算子调用之间或通过中止可中断算子来停止。 如果任何线程在 HDevelop 尝试停止程序执行时执行了无法中断的长时间运行的算子,则状态行中会显示相应的消息,并且相应的线程将在算子完成后最终停止。

选择线程
恰好其中一个线程是所谓的选定线程; 默认情况下,这对应于程序的主线程。 PC的位置、调用栈的状态、变量窗口中变量的状态都与选中的线程相关联。 选定的线程可以自动更改为停止的线程,例如,通过断点、停止指令、未捕获的异常或绘制运算符。

如何选择特定线程在第 308 页的“检查线程”一节中进行了描述。除连续执行之外的所有运行模式仅适用于所选线程。 与所选线程无关的程序行将在程序窗口中显示为灰色。

线程和即时编译过程

程序可以作为编译的字节码执行,而不是由 HDevelop 解释器解释。 这在第 43 页的“即时编译”一节中进行了描述。使用编译过程调试线程化 HDevelop 程序时有一个显着差异。 如果程序连续运行然后被停止(通过用户操作或断点/停止指令),则无法检查已编译过程(变量、PC)的当前状态。 您仍然可以单步执行过程调用,但这会导致重新执行相应的线程,从而可能导致意外的副作用。 请注意,当首先单步进入线程调用时,这不是问题,因为在这种情况下,过程始终由 HDevelop 解释器执行。

线程的生命周期

一个线程只要它仍然被变量引用就存在,即使它的执行已经完成。 这需要在par_join指令中引用线程,或者回退对应线程的PC进行调试。 但是,如果在调用之前手动将 PC 设置回程序行,则线程的生命周期结束。 除此之外,当使用 F2 重置程序时,所有子线程的生命周期都会结束。 在线程的生命周期中,线程会列在“线程视图/调用堆栈”窗口中,从中可以选择和管理它。

已完成但仍处于“活动”状态的线程可能会导致在稍后启动新线程时无意中超出配置的线程数限制。 此外,如果在执行新线程的同时“杀死”已完成的线程,则可能会对运行时行为产生负面影响。 要显式“杀死”已完成的线程,只需重置引用其线程 ID 的变量即可,如下例所示。

for Index := 0 to 4 by 1
par_start <ThreadIDs.at(Index)> : gather_data()
endfor
...
convert_vector_to_tuple (ThreadIDs, Threads)
par_join (Threads)
...
ThreadIDs := {}
Threads := []

错误处理

每个线程可以指定自己的错误处理,例如,使用 dev_set_check(‘˜give_error’)。 新的子线程从其父线程继承错误处理模式。 使用 try … catch 的异常处理仅在线程内有效,即在主线程中不可能捕获在子线程中抛出的异常。

05. 监视线程

程序及其线程的当前执行状态显示在组合线程视图/调用堆栈窗口中。 选择 执行 。 线程视图/调用堆栈或单击工具栏中的(另请参阅线程视图/调用堆栈(第 80 页))。 窗口的上半部分列出所有现有线程,而下半部分显示所选线程的调用堆栈。 为了说明与此窗口的交互,请考虑以下(愚蠢的)示例。

for Index:= 1 to 5 by 1
par_start <ThreadIDs.at(Index - 1)> : wait_seconds(Index)
endfor
wait_seconds(2)
stop()

按F5后,程序会启动5个子线程,最终到达stop指令,有的子线程还在运行,有的子线程已经结束了。 相应的线程视图如图 8.4 所示。 请注意,未完成的线程由于另一个线程而处于停止状态(在这种情况下是由主线程中的停止指令引起的)。
在这里插入图片描述

线程视图在一个表中列出了所有线程的属性。 每个线程第一列中的状态图标可视化当前执行状态。 当前选定的线程 (1) 由状态图标中的黄色箭头标记,并以粗体文本突出显示。 其他五个线程是从主线程开始的子线程。
要选择另一个线程,请在线程视图中双击它。 这还将根据所选线程更新 PC、调用堆栈和变量。 所选线程的活动过程将显示在程序窗口中。
在这里插入图片描述

单步执行包含 par_start 的程序行将初始化相应的线程而不实际启动它。 要调试特定线程,请在 PC 位于相应的 par_start 行时按 F7。 这将自动使新子线程成为选定线程。 如果 PC 已经通过调用行,首先在线程视图窗口中选择线程。 这将自动在程序窗口中显示正确的过程,如果线程是由过程调用启动的,则 PC 位于第一行。 对于由上例中的操作符调用启动的线程,PC 将位于相应的调用上,而程序的其余部分将变灰(见图 8.5)。 程序窗口中的通知行 (1) 显示所选子线程的线程 ID,并允许快速访问线程视图窗口 (2)。 单击 (3) 切换回主线程。

06. 挂起和恢复线程

线程可以在线程视图窗口中显式挂起和恢复。 挂起线程仅在算子调用之间起作用,即,如果线程当前正在运行,则在线程冻结之前仍将执行当前算子。

要挂起线程,请右键单击线程条目并选择挂起线程。 挂起相当于在其当前状态下被“冻结”,并将推迟后续运行命令,直到线程再次恢复,即运行命令将更改挂起线程的运行状态,但将阻止实际执行。

要再次恢复挂起的线程,请右键单击线程条目并选择恢复线程。

06. 附录

6.1 机器视觉博客汇总
网址:https://dengjin.blog.csdn.net/article/details/116837497

以上是关于机器视觉 HDevelop语言基础-多线程的主要内容,如果未能解决你的问题,请参考以下文章

机器视觉 HDevelop语言基础-错误处理

机器视觉 HDevelop语言基础-基本类型和常量

机器视觉 HDevelop语言基础-容器和保留字

机器视觉 HDevelop语言基础-变量和表达式

机器视觉 exit算子

机器视觉 dev_get_preferences算子