I did some debugging last night and think I got to the bottom of this.
The docs wrote:Unlike high-priority threads, events that occur during a critical thread are not discarded. For example, if the user presses a hotkey while the current thread is critical, the hotkey is buffered indefinitely until the current thread finishes or becomes noncritical, at which time the hotkey is launched as a new thread.
This is implemented by AutoHotkey's message loop being picky about which messages it removes from the queue. Messages < WM_HOTKEY are removed from the queue and processed, whilst other messages are left in the queue until the critical thread finishes. This ListView problem goes something like:
- You start a Critical thread.
- User clicks on the ListView, sending it a WM_LBUTTONDOWN message.
- ListView raises some events and posts them to AutoHotkey's message queue.
- ListView enters into its own message loop, doing only Microsoft knows what. Probably checking whether the user is initiating drag-drop or the selection marquee.
- ListView's message loop retrieves and dispatches each message, calling AutoHotkey's MainWindowProc().
- MainWindowProc() knows that it only gets called for those messages when some other message loop is running; therefore, it posts the message back into the queue and calls MsgSleep() to handle the message.
- MsgSleep() sees that the current thread is uninterruptible, so it applies a filter and only handles messages < WM_HOTKEY. In other words, it probably does nothing.
- Control returns to ListView's message loop, which sees the message again and dispatches it back to MainWindowProc().
- MainWindowProc() posts the message back into the queue, etc.
In theory, this can happen with other message loops, like those run by modal dialogs or menus. MsgBox and other AutoHotkey dialogs work around the issue by
making the current thread interruptible. Menus work around it by setting a flag which causes certain messages to be discarded rather than buffered.
I couldn't reproduce the problem with
DllCall("MessageBox"...), so I'd guess that it depends on exactly what the active message loop does. Discarding GUI events while the thread is uninterruptible
and some other message loop (such as the ListView's) is running avoids the problem, but it goes against the documented behaviour of buffering events. (Although discarding the events is better than freezing up.) Unfortunately, it also breaks some other scripts, such as my MessageBox test script, where the event correctly remains buffered until the critical thread completes. (It's possible that the event was actually being re-posted repeatedly, but this did not cause visible CPU usage.)
So in short, the problem is a design flaw of AutoHotkey in combination with the ListView, and
only applies to Critical threads. If I remove
Critical, your test script works just fine. Adding the following also works, reaffirming that the problem is triggered by LButton handling in the ListView (only tested with your second test script):
Code: Select all
OnMessage(0x201, "OnLButtonDown")
;...
OnLButtonDown(wParam, lParam, msg, hwnd) {
if (A_GuiControl != "ListControl")
return
static LVM_SUBITEMHITTEST := 0x1039
VarSetCapacity(hti, 24, 0)
NumPut(lParam & 0xFFFF, hti, 0, "short")
NumPut(lParam >> 16, hti, 4, "short")
SendMessage LVM_SUBITEMHITTEST, 0, &hti,, ahk_id %hwnd%
if (r := NumGet(hti, 12, "int") + 1) {
GuiControl Focus, ListControl
LV_Modify(0, "-Select -Focus")
LV_Modify(r, "Select Focus")
return true
}
}
Known issue: It prevents header clicks.
The reason I added the Sleep is because I suspected that Critical is not atomic and doesn't come into effect instantly,
Critical comes into effect instantly; however, calling Sleep
before Critical will flush any messages out of the queue. If you disable the control's g-label but there are already messages of the wrong type in the queue, the deadlock can still occur. In this case "the wrong type" probably means GUI event messages, menu item click messages and WM_TIMER. I think hotkey, hotstring and clipboard change messages are essentially discarded if the thread is critical (and someone else's message loop is running).