Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does autohotkey's asynchronous execution work?

This is sample program I found that will allow you to toggle some loop action:

; 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

Now we can see that there's some timeout-kind-of call that starts the endless loop. The endless loop is clearly synchronous, there are no callbacks or synchronied blocks or anything.

Still, pressing F12 seems to properly stop the loop, even when it's running.

Can someone explain to me how threads are executed in autohotkey? How does it process multiple blocks of code without race conditions? Does the SetTimer call play any role in this?

like image 964
Tomáš Zato - Reinstate Monica Avatar asked Mar 12 '23 18:03

Tomáš Zato - Reinstate Monica


1 Answers

TLDR: Threads can interrupt each other.

Have a look at the 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.

New thread vs. current thread

So in your example, if F12is pressed and toggle is false, it will run the loop subroutine immediately and only once (Period of -1). The subroutine will loop until toggle becomes false again.
Here comes the trick: If you press F12 again, another thread is run, and new threads interrupt the current thread by default. So the new thread will halt the loop, set toggle to false and then finish gracefully, since the hotkey subroutine has nothing left to do. After the hotkey subroutine has finished, the previous thread (which is our looptimer) comes back to life. Since toggle is now false, it will break out of the loop and also finish...and so the circle is complete. Mind that loop had been commanded to run only once, so no further repetition there.

Thread priorities

New threads can only interrupt the current thread if their priority is at least equal to the current thread's priority. By default, every thread has the priority of 0, and it doesn't matter if it's a Hotkey thread, a timed subroutine, or any other kind of thread. Of course, there's an exception...

Using Sleep

The AHK docs on Sleep say:

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

If a thread is sleeping, it basically gets interrupted and frees up all CPU time for any other thread (only for the time that it actually sleeps). That is, even threads with a lower priority can run while the current thread is sleeping. In your example, there is a substantial Sleep of 700 ms. Of course, even without the sleep your script would work and toggle would still be toggleable. But even if loop had been called with a higher priority, the hokey would still be able to run while loop is sleeping (which practically is most of the time).

The example code stinks

The code example you posted may work but IMO, is confusing and outright bad coding. The main purpose of timers is to run periodically, but here we have a loop within a timer, which defies the whole purpose of timers. If we allow a Hotkey to spawn more than one thread, we could even use this absurd but working piece of code:

; Bad example!
#MaxThreadsPerHotkey 2
toggle := false

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

Use Timers for what they are meant to do

Here's how I would implement a toggle function that left clicks every 700ms:

toggle := false

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

DoLeftClick:
    Click
return
like image 184
MCL Avatar answered May 10 '23 18:05

MCL