Skip to content

v2.1: Added A_ThreadId and a thread id parameter to Exit()#339

Open
Descolada wants to merge 5 commits intoAutoHotkey:alphafrom
Descolada:2.1-Threads
Open

v2.1: Added A_ThreadId and a thread id parameter to Exit()#339
Descolada wants to merge 5 commits intoAutoHotkey:alphafrom
Descolada:2.1-Threads

Conversation

@Descolada
Copy link
Copy Markdown
Contributor

A_ThreadId

A_ThreadId returns a nearly unique id for the running pseudo-thread. It can be used to differentiate pseudo-threads (eg by using a Map object), or with Exit() to terminate a specific thread (see down below for explanations). The auto-execute section id is always 65537 ((1 << 16) | 1).

Implementation: A_ThreadId is a 64-bit integer, where the lower 16 bits store the number of active threads, and the next 32 bits is the number of total threads created at the moment of the current pseudo-thread creation. Upmost 16 bits are reserved for future applications.
64 bits was chosen over 32 bits because it drastically increases the chance that the id is unique: with 32 bits I would've split it into 20 and 12 bits, which would overflow after ~1 million pseudo-threads, a number which is feasibly reached. Additionally the bit arithmetic looked uglier: A_ThisThread & 0xFFF instead of A_ThisThread & 0xFFFF. Furthermore, as a 32-bit integer overflow is highly unlikely during the lifetime of a script, it can be used to differentiate newer threads from older ones.

Exit [ExitCode, ThreadId]

Additionally this PR modifies Exit [ExitCode] to Exit [ExitCode, ThreadId] and adds a return value to it. ThreadId may be either a specific thread id returned by A_ThisThread, or an index number in the current stack of pseudo-threads. For example, Exit(, 1) marks the first (oldest) pseudo-thread in the stack for termination; Exit(, (A_ThisThread & 0xFFFF) - 1 marks the underlying thread; Exit(, (1 << 16) | 1) marks the auto-execute section. A thread marked for termination will exit once the stack unwinds to it, the thread resumes execution, and starts executing the next line. This means that if it was interrupted during a chained expression, then the last parts of the expression will still execute. Such behavior was chosen to limit the number of checks for whether to stop thread execution, and gives a bit more flexibility to the user: if some action should unconditionally be executed after another, then the user can chain it in an expression instead of a new line to prevent another thread from stopping it.
The behavior of Exit [ExitCode] is almost unchanged and causes the running thread to immediately exit, handling __delete and finally afterwards if needed. One exception is that calling DllCall(CallbackCreate((*) => Exit(), "F")) will now cause the thread which called DllCall to exit, because Fast mode doesn't create a new thread.

Exit returns the terminated thread id, or 0 if no thread was terminated.

Some test cases:
1.

try {
	Exit
} catch {
} finally {
    MsgBox "Reached"
}
MsgBox "Not reached, script terminates, exit code = 0"
Exit(1)
MsgBox "Not reached, script terminates, exit code = 1"
SetTimer((*) => Exit(1, 1), -1000) ; Auto-execute section is exited in 1 second, exit code = 1

Loop {
	ToolTip "Thread is running: " A_ThreadId
	Sleep 100
}
SetTimer((*) { 
	__ := {}.DefineProp("__Delete", {call:(*) => MsgBox("Delete is called, script exits")})
	Exit()
}, -1000)
SetTimer((*) { 
	DllCall(CallbackCreate(MyExit))
	MsgBox "This thread is not exited"
}, -1000)

MyExit() => Exit()
SetTimer((*) { 
	DllCall(CallbackCreate(MyExit, "F"))
	MsgBox "This is not reached, because F mode is used and the code is ran in the existing thread"
}, -1000)

MyExit() => Exit()
SetTimer((*) { 
	DllCall(CallbackCreate(MyExit, "F"))
	MsgBox "This is not reached even with multiple F mode threads"
}, -1000)

MyExit() => (DllCall(CallbackCreate(MyExit2, "F")), MsgBox("This is still evaluated, then the script exits"))
MyExit2() => Exit()
F1::{
	MsgBox "Press F1 first, then press F2"
	MsgBox "This is not reached"
}

F2::{
	MsgBox "This kills the underlying thread with id " Exit(, (A_ThreadId & 0xFFFF) - 1)
}

@RaptorX
Copy link
Copy Markdown

RaptorX commented Feb 22, 2025

This might be extremely handy in certain situations if implemented.
Consider this situation:

F1::
{
    global woking := true
    Loop lv.GetCount() ; this could be thousands for lines
    {
        if !working
            break
        SavePerson(a_index) ; this function performs many actions
    }
}

SavePerson(row)
{
    ; get data
    ; open browser
    ; navigate
    ; open record
    ; add information
    ; save record
    ; update related tables
    ; and many other actions
}

In this situation i need to

  1. have a variable that is shared between threads (global is easier but not preferred)

  2. if checking for said variable inside the loop then i have to wait until ALL the actions are performed before breaking which causes a delay between the user clicking the GUI and the actual stopping of the loop.

  3. if I want it more responsive then i need to check for the variable before performing each action inside the SavePerson function.

This new A_ThreadId would be god sent because then when my user presses the GUI button to stop the action i can send an Exit(, ThreadId) and simply exit that loop immediately!

Also if this could be extended for pausing a thread that would also be amazing for similar situations!

All in all I am all for having some sort of pseudo thread identification / manipulation to handle certain UI actions that right now need some clunky workarounds.

@iseahound
Copy link
Copy Markdown

The naming is certainly tragic. In other languages this might be called an execution context or a task. I wonder if it's too late to change it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants