触发器的选项中有一个 “顺序执行” 选项(sequential):
而触发器动作也有一个 “异步执行” 选项(asynchronous):
二者的行为和效果有一定的相似之处,容易引起误解,在此简要说明。
触发器插件有一个动作序列,每次触发器被触发时,会按照一定的逻辑将动作注册进动作序列,然后序列中的动作会被分配到不同线程执行。
这两个属性会对这个注册和执行的过程有一定影响,从而影响动作的执行顺序。
顺序执行是一个触发器的属性,而非动作的属性。它改变的是动作的注册方式。
未额外改变任何设置的正常情况下,所有动作会被一起排进动作序列,根据延迟计算执行时间,稍后异步执行。故时间相同的动作之间无法保证先后顺序。
而如果开启顺序执行,后面的动作会在前一个动作执行之后才排进序列,从而保证了这个触发器单次触发的执行过程中,全部动作可以保证顺序。
同步执行是一个触发器动作的属性。它改变的是动作的执行方式。
正常情况下,动作序列中当前已到执行时间的所有动作会一起被分配线程,使用多线程模式异步执行提高效率,而这也是无法保证顺序的原因。
但开启同步执行的情况下,此动作会强制在主线程执行,且执行过程中不会允许其他动作同时执行。
这样即使一个触发器中很多动作被一起排进了序列,也会依次在主线程执行,用另一种方式保证了先后顺序。
触发器整体设为顺序执行,或触发器中每一个动作设为同步执行,二者均会使所有动作按照顺序执行,但实则有一定差异。
-
时间延迟
当触发器设定为顺序执行时,每个动作会在前一个动作结束后才注册进队列。
而有些动作本身略微耗时,如获取全部实体列表、遍历变量,可能使下一个动作的注册产生毫秒级的推迟;
还有些动作耗时更长,如没安装 TTS 插件时的大妈音 TTS、系统蜂鸣声(目前未写为异步执行)、编译执行脚本代码,会产生秒级的推迟。
(这也就是为什么触发器自检中会明确强调必须安装 TTS 插件,否则触发器会出现时序问题。)
所以,对于包含 “延迟若干秒执行某操作” 动作的触发器,如果包含高耗时的动作且使用顺序执行,则会产生一定的时间延迟,对于时间敏感的触发器(如播报一段时间轴)应该避免。
-
冻结线程
根据前文所述,顺序执行的触发器只会使同一次触发中的后续动作延后执行。
而同步执行的动作则会注册到主线程,不仅阻塞同一次触发的后续动作,还会暂停其他所有动作,冻结主线程(包括 UI)。
所以切勿使用同步动作执行高耗时的任务。
-
动态延迟
当一个动作被注册时,会解析延迟时间的表达式,计算它应该触发的时间,并将其排入队列。
这意味着,如果你写了一个根据某些条件而改变的延迟,在前面的动作中计算并设置了一个变量
t
,而后面的动作中延迟t
毫秒,那么错误的设置会产生不符合预期的结果。非顺序模式下,所有动作被一起排入队列并解析延迟时间,而由于此时还没有执行过设置变量
t
,所以实际上并不会延迟t
执行。所以在这种情况下,用顺序执行触发器更合理。
-
多次触发
副本中经常会遇到需要同时连续触发一个或几个触发器,然后用获取的数据继续处理的情况。例如:
-
绝神兵三连桶获取点名推送至列表中,推送三次后立刻排序并标记。
-
绝欧 P5 击退塔出现时,将塔的坐标存储至列表中,根据远近线判断触发 5 或 6 次后立刻计算向量和的角度,确定相对北的方向。
-
P12s 几乎同时生成 12 个多面体、8 个头顶标记、8 个 α/β debuff。我们需要获取全部多面体、自己的标记和 debuff 才可确认后续解法,共触发 8 + 1 + 1 次后才能开始后续处理。
这些触发器中,通常我们希望触发器在重复触发时变量不互相影响,且变量在不同触发器之间也不互相影响。即保证完全处理完某一个触发器的某一次触发的全部动作之后,再继续处理其他触发器的触发。
顺序执行并不会确保我们预期的结果,因为它只保证了同一次触发中动作的顺序,而不能保证其间没有穿插其他触发器的动作。
所以这个情况下,应该使用全同步动作:每次触发时所有动作同时排入队列,并在单一线程上按顺序执行,可以完全保证我们预期的顺序。
-
注 1:
还有一种写法:用一个触发器多次触发获取数据;另一个触发器单次触发,延迟一定时间后播报结果。
但是当你可以确定预期触发的次数,且触发后需要尽早开始处理机制的情况下,请勿使用延迟触发。
延迟触发属于极不稳定的非顺序行为:你永远无法保证你延迟播报的时间点确实已经获取了全部所需的数据。
延迟较低时会因网络波动偶然产生播报早于获取的情况,而延迟较高则会导致太晚看到结果(比如某科技 P5 二运标点的超高延迟)。
-
注 2:
目前请避免使用 “互斥锁” 功能。
此功能自官库更新 1.1.7.1 版本以来一直有疑似死锁的恶性 bug,会炸掉 ACT 进程,始终未修复。
顺序 / 同步这两个功能基本可以代替互斥锁,所以我一直没有尝试使用互斥锁或修复这个问题。
-
有必要在此明确动作结束的含义:
有时,动作结束并不意味着这个动作本身做的事情已经全部结束,而是代表程序上这个动作为标记为“结束”。
举个例子,当使用 TTS 插件时,文本转语音 TTS 动作实际向插件发送了一个 “播报XXXX” 的指令,然后立刻被标记为结束,并腾出资源处理后面的动作;而不是等待这一句话读完才算动作结束。
也就是说,无论下图所示的触发器是否为顺序执行或同步执行,“第二句话” 的实际播报时间都应是第三秒,而不是第五秒。