前面我已经写过 AHK_H 的入门、多线程、ahkdll 和 MemoryLoadLibrary。那些文章解决的是“它是什么、怎么跑起来”。这篇不再重复总览,而是补几个更接近实战的问题:线程代码怎么拆、主线程和子线程怎么传数据、子线程怎么回调主线程、线程退出时为什么不能随便强杀。
如果你只是写普通热键、小工具、办公脚本,大多数时候不需要 AHK_H。AHK_H 更适合真实多线程、DLL 嵌入、打包发布、跨语言调用、源码保护这类场景。换句话说,它不是普通 v1 的必学替代品,而是你真的遇到这些需求时再拿出来的工具。
先跑通最小线程
AHK_H 的多线程入口通常从 AhkThread() 开始。最小理解是:主脚本创建一个新的 AHK 执行环境,让那段代码独立运行。
#Requires AutoHotkey v1.1
#Persistent
t1 := AhkThread("MsgBox, 线程 1")
t2 := AhkThread("MsgBox, 线程 2")
MsgBox, 主线程
return
主线程如果没有热键、GUI、定时器或 #Persistent,可能会很快退出。新手测试线程时,建议先加上 #Persistent,避免误以为线程没创建成功。
不要把长代码全塞进字符串
很多示例会直接把线程代码写进一段字符串或 continuation section。小例子可以这样写,但代码一长,就会遇到转义、引号、反引号、换行、长度和可读性问题。
code =
(
#Persistent
Loop
{
ToolTip, 子线程运行中:%A_Index%
Sleep, 500
}
)
t := AhkThread(code)
这个写法适合演示,不适合长期维护。真正项目里,我更建议把线程代码放到单独文件,或者先读入变量,再统一拼接必要的初始化代码。
FileRead, threadCode, %A_ScriptDir%\thread_worker.ahk directives = ( #NoEnv #Persistent ) t := AhkThread(directives "`n" threadCode)
这里有一个重要点:主脚本里的指令不会自动继承到子线程。比如 #NoEnv、#Persistent、#Include 这些,如果线程里需要,就要在线程代码里重新写,或者像上面这样拼进去。
用文件启动线程
AHK_H 的 AhkThread() 可以让线程从文件启动。这样线程代码就能像普通脚本一样编辑,少很多字符串转义问题。
state := CriticalObject() state.error := "none" t := AhkThread(A_ScriptDir "\thread_worker.ahk", "" (&state), true)
第三个参数用于表示脚本来源是文件。第二个参数可以传给线程,在线程里通过 A_Args 读取。论坛里常见 A_Args.1*1,本质是把字符串形式的地址转成数字,再交给 CriticalObject() 使用。
; thread_worker.ahk
#NoEnv
#Persistent
state := CriticalObject(A_Args.1*1)
Loop
{
if (state.error = "none")
state.value := "worker: " A_Index
Sleep, 500
}
这种写法比把完整线程脚本塞进字符串更清楚。主脚本负责创建共享对象和启动线程,子线程文件只关心自己的工作逻辑。
共享数据用 CriticalObject
多线程最容易出问题的地方是共享数据。普通对象不能随便跨线程共享,AHK_H 提供了 CriticalObject 这类机制。它适合放状态、错误信息、简单任务标记。
state := CriticalObject()
state.value := "ready"
state.error := "none"
t := AhkThread("state:=CriticalObject(" (&state) ")`nLoop`n{`nstate.value := A_Index`nSleep, 500`n}")
Loop
{
ToolTip, % "子线程状态:" state.value
Sleep, 200
}
共享对象不是让你把所有东西都塞进去。我的习惯是只共享必要状态:是否停止、当前进度、错误信息、任务参数。复杂数据可以考虑拆成消息、文件、队列或更明确的通信结构。
不要让线程无脑死循环
很多新手第一次写线程,会让子线程一直 Loop 检查共享变量。这样能跑,但不一定优雅。论坛里也有人建议:如果只是让子线程执行某个函数,不一定要一直轮询,可以考虑 ahkPostFunction() 这类方式,把函数投递到子线程执行。
worker := AhkThread("
(
#Persistent
DoWork(msg)
{
MsgBox, % ""子线程收到:"" msg
}
)")
worker.ahkPostFunction("DoWork", "hello")
轮询适合长期监控,函数投递适合事件驱动。能不用死循环,就别让线程一直空转。
子线程反过来调用主线程
主线程调用子线程函数,常见写法是 thread.ahkFunction()。反过来,子线程想调用主线程函数,可以在线程里通过 AhkExported() 拿到导出的主环境,再调用主脚本里的函数。
#Persistent
workerCode =
(
#Persistent
exe := AhkExported()
exe.ahkFunction["MainCallback", "来自子线程"]
)
t := AhkThread(workerCode)
return
MainCallback(msg)
{
MsgBox, % "主线程函数收到:" msg
}
这个技巧适合子线程完成任务后通知主线程。实际项目里,我仍然建议先设计好方向:哪些事只能主线程做,哪些事放子线程做,不要让两个线程互相乱调。
线程退出不要随便强杀
AHK_H 里可以用 ahkTerminate() 结束线程。普通写法会等待线程正常退出。论坛里讨论过负数参数,比如 ahkTerminate(-100),大意是等待一定时间后强制结束。
; 优先让线程正常退出 state.stop := true Sleep, 500 ; 再尝试结束线程 worker.ahkTerminate()
强制结束不是常规流程。HotKeyIt 在帖子里提醒过:如果线程没有机会释放资源,可能带来泄漏;如果线程里有 COM 对象,还可能没有正常释放对象。比如在线程里创建 Outlook、Excel 这类 COM 对象,更要主动清理。
; 子线程里使用 COM 时,结束前尽量释放引用
xl := ComObjCreate("Excel.Application")
; ... do something ...
xl.Quit()
xl := ""
我的建议是:先用共享状态通知线程自己退出;线程卡死时,再考虑带超时的终止;强制终止只作为兜底,不要当成日常关闭方式。
实战习惯
- 线程代码长了就拆文件,不要硬塞字符串。
- 线程需要的指令、包含文件,要在线程里单独准备。
- 共享状态用
CriticalObject,不要乱传普通对象。 - 长期任务才用循环,短任务优先考虑函数投递。
- 子线程回调主线程前,先想清楚调用方向。
- 退出线程先通知、再等待、最后才兜底强制结束。
- 线程里创建 COM、文件句柄、窗口等资源时,要写清理逻辑。
AHK_H 的难点不在于创建线程那一行,而在于线程之间的边界。哪些数据共享、哪些代码独立、什么时候退出、失败后谁负责恢复,这些想清楚以后,AHK_H 才会从“能开线程”变成真正可维护的工具。
H 版帮助文档
- AHK_H v1 帮助文档总览:查看 H 版新增命令、函数和 DLL 说明。
- AhkThread:H 版多线程入口,创建新的 AHK 执行环境。
- CriticalObject:线程之间共享对象时最常用的安全包装。
- ahkFunction:调用线程中的函数并等待返回。
- ahkPostFunction:异步调用线程中的函数。
- ahkTerminate:结束或释放线程时参考。

评论(0)