前面我已经写过 AHK_H 的入门、多线程、ahkdllMemoryLoadLibrary。那些文章解决的是“它是什么、怎么跑起来”。这篇不再重复总览,而是补几个更接近实战的问题:线程代码怎么拆、主线程和子线程怎么传数据、子线程怎么回调主线程、线程退出时为什么不能随便强杀。

如果你只是写普通热键、小工具、办公脚本,大多数时候不需要 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 版帮助文档

声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。