autohotkey 的异步执行是如何工作的?

How does autohotkey's asynchronous execution work?

这是我发现的示例程序,可让您切换某些循环操作:

; This toggles the action
toggle:=false

F12::
; If true is assigned to toggle, loop starts
; It also can assign false even when loop is running
If (toggle := !toggle)
    SetTimer, loop, -1
return

loop:
; Endless loop? Actually not, the value of toggle can be changed by
; another "thread" even when this loop is running
while toggle
{
    Click
    Sleep, 700
}
return

现在我们可以看到有一些超时类的调用启动了无限循环。无限循环显然是同步的,没有回调或同步块或任何东西。

仍然,按 F12 似乎可以正确停止循环,即使它是 运行。

谁能给我解释一下线程在 autohotkey 中是如何执行的?它如何在没有竞争条件的情况下处理多个代码块? SetTimer 电话在这方面有什么作用吗?

TLDR:线程可以相互中断。

看看 AHK docs on Threads:

Although AutoHotkey doesn't actually use multiple threads, it simulates some of that behavior: If a second thread is started -- such as by pressing another hotkey while the previous is still running -- the current thread will be interrupted (temporarily halted) to allow the new thread to become current. If a third thread is started while the second is still running, both the second and first will be in a dormant state, and so on.

新线程与当前线程

因此在您的示例中,如果按下 F12togglefalse,它将 运行 loop 子例程 立即且仅一次-1 期间)。子例程将循环直到 toggle 再次变为 false
技巧来了:再按F12,另一个线程是运行,新线程默认中断当前线程。因此,新线程将停止循环,将 toggle 设置为 false,然后优雅地完成,因为热键子例程无事可做。热键子例程完成后,前一个线程(即我们的 loop 计时器)恢复生机。由于 toggle 现在是 false,它将跳出循环并完成...因此循环完成。请注意 loop 仅被命令 运行 一次,因此不再重复。

线程优先级

新线程只有在 priority 至少等于当前线程的优先级时才能中断当前线程。默认情况下,每个线程都有 0 的优先级,无论它是 Hotkey 线程、定时子程序还是任何其他类型的线程都没有关系。当然也有例外...

使用Sleep

AHK docs on Sleep 说:

While sleeping, new threads can be launched via hotkey, custom menu item, or timer.

如果一个线程正在休眠,它基本上会被中断并释放所有 CPU 时间 给任何其他线程(仅用于它实际休眠的时间)。也就是说,即使是优先级较低的线程也可以在当前线程休眠时 运行 。在您的示例中,有 700 毫秒的大量 Sleep。当然,即使没有睡眠,您的脚本也可以运行,并且 toggle 仍然可以 切换 。但是即使 loop 以更高的优先级被调用,当 loop 正在睡觉时(这实际上是大部分时间),hokey 仍然能够 运行。

示例代码很烂

您发布的代码示例可能有效,但在我看来,它令人困惑且完全是错误的编码。定时器的主要目的是周期性地运行,但是这里我们在定时器中有一个循环,这违背了定时器的全部目的。 如果我们允许热键生成多个线程,我们甚至可以使用这段荒谬但有效的代码:

; Bad example!
#MaxThreadsPerHotkey 2
toggle := false

F12::
    toggle := !toggle
    while(toggle) {
        SoundBeep
        Sleep, 500 ; Would even work without the Sleep
    }
return

将定时器用于它们应该做的事情

以下是我如何实现每 700 毫秒左键单击一次的切换功能:

toggle := false

F12::
    toggle := !toggle
    if(toggle) {
        SetTimer, DoLeftClick, Off
    } else {
        SetTimer, DoLeftClick, 700
    }
return

DoLeftClick:
    Click
return

不要认为这是一个完整的答案。
我只想补充一点,从 v1.1.20 开始,您应该几乎总是使用函数而不是标签。这避免了许多潜在的冲突(标签在全局范围内执行)。
所以最好你会这样做:

F12::
__F12() {
    Static toggle := False
    toggle := !toggle
    SetTimer, DoLeftClick, % toggle ? 700 : "Off"
}

DoLeftClick() {
    Click
}