开启左侧

[HALCON文档] HDevelop中如何开启多线程执行?

[复制链接]
绝地武士 发表于 2020-6-13 14:20:19 | 显示全部楼层 |阅读模式
【如何开启多线程】
要启动一个新线程,用par_start限定符作为相应操作符或过程调用的前缀:'
  1. par_start <ThreadID> : gather_data()
  2.   ...
复制代码

该调用将基于函数gather_data()作为一个在后台运行的新的子线程并且执行后续的程序行。这个线程标识的变量以ThreadID进行返回。和在HDevelop不一样的是,在HDevEngine中仅在启动线程的函数中给出的ThreadID才是有效的。

注意,par_start不是一个实际的操作符,而是一个修改调用形为的限定符,因此,不能在算子窗口取选择它!
如果启动一个新的子线程将超过系统配置的最大线程数,将会引发异常抛出!

你也可以一个函数或算子调用开始子线程,通过下面的算子窗口:
gui_op_parallel.png

如果要开启子线程,需要开启下方的“高级并行选项”并勾选复选框:
2.png
它支持在一个循环中启动多个线程。在这种情况下,需要收集线程标识,以便以后可以引用所有线程:
  1. ThreadIDs := []
  2.   for Index := 1 to 5 by 1
  3.     par_start <ThreadID> : gather_data()
  4.     ThreadIDs := [ThreadIDs, ThreadID]
  5.   endfor
复制代码

同时,我们可以在向量变量中收集这些线程标识将会非常有用:
  1. for Index := 1 to 5 by 1
  2.     par_start <ThreadIDs.at(Index - 1)> : gather_data()
  3.   endfor
复制代码

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

同样,必须确保多线程不会干扰它们的结果。假设过程gather_data以如上所示的多个线程启动,但在输出控制变量中返回数据:
for Index := 1 to 5 by 1
    par_start <ThreadIDs.at(Index - 1)> : gather_data(Result)  // BEWARE!!!
  endfor
在上面的例子中,所有的线程都会在同一个变量中返回它们的结果,这肯定不是我们想要的。结果的最终值将是最后完成的线程的(不可预测的)返回值,所有其他结果都将丢失。

这个问题的一个简单解决方案是收集向量变量中的返回数据,如前面的线程标识所示:
  1. for Index := 1 to 5 by 1
  2.     par_start <ThreadIDs.at(Index - 1)> : gather_data(Result.at(Index - 1))
  3.   endfor
复制代码

这里,每次调用收集数据都会在向量变量result的一个唯一槽中返回结果。

 楼主| 绝地武士 发表于 2020-6-13 14:35:25 | 显示全部楼层
【等待多线程执行完成】
使用算子par_join来等待单个线程或一组线程的完成。

举个例子说明为什么这是必要的,假设我们想调用一个在后台执行一些神奇计算的过程,并返回一个计数结果。在随后的程序行中,我们希望使用这个数字进行进一步的计算。
  1. par_start <ThreadID> : count_objects(num)
  2.   ...
  3.   for i := 1 to num by 1     // BEWARE: num might be uninitialized
  4.     ...
  5.   endfor
复制代码

仅仅依靠子线程足够快很可能会失败。因此,需要事先明确调用par_join。
  1. par_start <ThreadID> : count_objects(num)
  2.   ...
  3.   par_join(ThreadID)
  4.   for i := 1 to num by 1
  5.     ...
  6.   endfor
复制代码

请注意,在HDevelop中,并不严格要求使用par_join,因为主线程总是比子线程长。然而,如果程序要在HDevEngine中执行(见程序员指南)或导出到一种编程语言,省略它可能会导致麻烦。同样,如果要导出程序,对全局变量的访问可能需要一些额外的同步。

参考上面帖子中的示例,等待循环中启动的所有线程的结束是使用下面的行来实现的。
  1. convert_vector_to_tuple(ThreadIDs, Threads)
  2.   par_join(Threads)
复制代码

请注意,线程标识是在一个向量变量中收集的。因此,转换成元组对于par_join正常工作是必要的。

par_join运算符会阻止调用它的过程的进一步执行,直到所有指定的线程都完成为止。在随后的程序行中,可以可靠地访问相应线程的结果。
 楼主| 绝地武士 发表于 2020-6-13 14:47:47 | 显示全部楼层
【HDevelop中线程的执行】
通常,只有当程序在按下F5后继续运行时,才会并行执行HDevelop中的线程。在所有其他执行模式中,只有选定的线程被启动,所有其他线程保持停止,除非明确的用户交互推进它们的执行。活动断点、停止指令、运行时错误或未捕获的异常也会导致所有线程停止,以便评估它们的当前状态。这种约定能够实现明确定义的调试过程,因为它消除了来自其他线程的不可控制的副作用。程序窗口中的任何编辑操作也会导致同时运行的程序停止。

线程不能从外部“杀死”。它们可以在操作员呼叫之间或通过中止可中断的操作员来停止。如果任何线程执行一个长时间运行的操作符,而该操作符在HDevelop试图停止程序执行时不能被中断,则相应的消息将显示在状态行中,并且相应的线程将在操作符完成后最终停止。

选中线程
恰好其中一个线程是所谓的选择线程;默认情况下,这对应的程序的主线程。个人计算机的位置、调用堆栈的状态以及变量窗口中变量的状态都链接到所选线程。所选线程可以自动改变为停止的线程,例如通过断点、停止指令、未捕获的异常或绘制算子。

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

线程和实时编译(JIT)程序
函数可以作为编译的字节码来执行,而不是由HDevelop解释器来解释。这在第93页的“即时编译”一节中有所描述。在用编译程序调试线程化的HDevelop程序时,有一个显著的区别。如果程序连续运行,然后被停止(通过用户操作或断点/停止指令),则不能检查编译程序(变量,计算机)的当前状态。您仍然可以进入过程调用,但这将导致相应的线程被重新执行,这可能会导致意外的副作用。请注意,当单步执行线程调用时,这不是问题,因为在这种情况下,过程总是由HDevelop解释器执行。

线程生命周期
只要一个线程仍然被变量引用,它就存在,即使它的执行已经完成。这对于在par_join指令中引用线程,或者将相应线程的计算机设置回调试状态是必要的。然而,如果在调用之前将个人计算机手动设置回程序行,线程的生命周期就结束了。除此之外,当使用F2重置程序时,所有子线程的寿命结束。在其生命周期内,线程会在“线程视图/调用堆栈”窗口中列出,从中可以选择和管理该线程。

已完成,但仍处于“活动”状态的线程可能会导致在以后启动新线程时无意中超出配置的线程数量限制。此外,如果在执行新线程的同时“终止”已完成的线程,也会对运行时行为产生负面影响。要显式“终止”已完成的线程,只需重置引用其线程标识的变量,如下例所示。
  1. for Index := 0 to 4 by 1
  2.     par_start <ThreadIDs.at(Index)> : gather_data()
  3.   endfor
  4.   ...
  5.   convert_vector_to_tuple (ThreadIDs, Threads)
  6.   par_join (Threads)
  7.   ...
  8.   ThreadIDs := {}
  9.   Threads   := []
复制代码


错误处理
每个线程可以指定它自己的错误处理,例如,dev_set_check('~give_error')。新的子线程从其父线程继承错误在一个线程中使用try...catch工作,即在主线程中,不可能捕获在子线程中抛出的异常。

 楼主| 绝地武士 发表于 2020-6-13 15:01:43 | 显示全部楼层
【线程检测】
程序及其线程的当前执行状态显示在组合线程视图/调用堆栈窗口中。选择执行线程视图/调用堆栈或单击工具栏中的(另请参见线程视图/调用堆栈)。窗口的上半部分列出所有现有线程,下半部分显示所选线程的调用堆栈。为了说明与该窗口的交互,请考虑以下(傻瓜式)示例。
  1. for Index:= 1 to 5 by 1
  2.   par_start <ThreadIDs.at(Index - 1)> : wait_seconds(Index)
  3. endfor
  4. wait_seconds(2)
  5. stop()
复制代码

按下F5后,程序将启动五个子线程,并最终到达停止指令,留下一些子线程仍在运行,而其他子线程已经完成。相应的线程视图如下图所示。请注意,由于另一个线程(在这种情况下是由主线程中的停止指令引起的),未完成的线程处于停止状态。
gui_exe_stack_threads_labels.png

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

线程视图列的含义如下:
列名称 描述
ThreadID线程启动时分配给它的唯一编号
State 线程的当前执行状态
Run Mode 上次启动线程的模式
Stop Mode 停止线程的原因
Caller 线程开始的位置(函数和行号)
References 默认情况下隐藏,线程引用的数量

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

 楼主| 绝地武士 发表于 2020-6-13 15:04:03 | 显示全部楼层
【挂起和恢复线程】
线程可以在线程视图窗口中显式挂起和恢复。挂起线程仅在操作符调用之间起作用,即如果线程当前正在运行,当前操作符仍将在线程冻结之前执行。

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

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


您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表