针对易语言+大漠插件(COM)多线程的暂停/恢复/停止需求,核心原则是:绝对不能使用 SuspendThread/ResumeThread 强制挂起线程

大漠是COM组件,工作线程初始化为STA模式(CoInitializeEx(0,2)),强制挂起会破坏COM公寓状态,导致死锁、崩溃、资源泄漏。行业标准方案是协作式状态机 + Win32事件对象,让线程在安全点主动检测状态并控制自身执行。


一、核心设计思路

  1. 状态标记:每个线程独立维护「运行/暂停/停止」三种状态
  2. 安全检查点:线程在每次取任务前、任务内部循环中,主动检查自身状态
  3. 事件等待:暂停时线程阻塞在系统事件上,不占用CPU,恢复时通过事件唤醒
  4. 安全退出:停止时线程完整执行COM资源释放流程,杜绝泄漏

二、前置准备:API与常量定义

先在易语言中声明以下系统API和常量:

.版本 2

' ========== Win32 API 声明 ==========
.DLL命令 CreateEvent, 整数型, "kernel32.dll", "CreateEventA"
    .参数 lpEventAttributes, 整数型, , 安全属性,传0即可
    .参数 bManualReset, 逻辑型, , 是否手动重置事件
    .参数 bInitialState, 逻辑型, , 初始是否有信号
    .参数 lpName, 文本型, , 事件名称,传空即可

.DLL命令 SetEvent, 逻辑型, "kernel32.dll", "SetEvent"
    .参数 hEvent, 整数型, , 事件句柄

.DLL命令 ResetEvent, 逻辑型, "kernel32.dll", "ResetEvent"
    .参数 hEvent, 整数型, , 事件句柄

.DLL命令 WaitForSingleObject, 整数型, "kernel32.dll", "WaitForSingleObject"
    .参数 hHandle, 整数型, , 等待对象句柄
    .参数 dwMilliseconds, 整数型, , 超时时间(毫秒),-1为无限等待

.DLL命令 CloseHandle, 逻辑型, "kernel32.dll", "CloseHandle"
    .参数 hObject, 整数型, , 对象句柄

.DLL命令 WaitForMultipleObjects, 整数型, "kernel32.dll", "WaitForMultipleObjects"
    .参数 nCount, 整数型, , 等待对象数量
    .参数 lpHandles, 整数型, 数组, 对象句柄数组
    .参数 bWaitAll, 逻辑型, , 是否等待全部
    .参数 dwMilliseconds, 整数型, , 超时时间

' ========== 常量定义 ==========
.常量 线程状态_运行, 整数型, 0
.常量 线程状态_暂停, 整数型, 1
.常量 线程状态_停止, 整数型, 2
.常量 WAIT_OBJECT_0, 整数型, 0
.常量 INFINITE, 整数型, -1

三、数据结构改造

在你的 线程信息 数组结构体中,新增3个成员: | 成员名 | 类型 | 作用 | |--------|------|------| | 状态 | 整数型 | 存储当前线程状态(运行/暂停/停止) | | 暂停事件 | 整数型 | Win32事件对象句柄,用于暂停/恢复通知 | | 线程句柄 | 整数型 | 保存启动线程的句柄,用于停止时等待退出 |


四、完整代码实现

1. 启动线程(改造版)

.版本 2
.支持库 spec
.支持库 EThread

.子程序 _按钮_启动_被单击
.局部变量 账号总数, 整数型
.局部变量 线程数, 整数型
.局部变量 i, 整数型

线程数 = 到整数 (编辑框_线程数量.内容)
.如果真 (线程数 = 0)
    信息框 (“请设定线程数量”, 0, , )
    返回 ()
.如果真结束

账号总数 = 取数组成员数 (线程信息)
.如果真 (线程数 > 账号总数)
    线程数 = 账号总数
    编辑框_线程数量.内容 = 到文本 (线程数)
.如果真结束

游戏_取信息 ()
账号_循环绑定句柄 (账号总数, 线程数, gHwnd)

' 初始化每个线程的事件对象和初始状态
.计次循环首 (线程数, i)
    ' 创建手动重置事件,初始无信号
    线程信息 [i].暂停事件 = CreateEvent (0, 真, 假, )
    线程信息 [i].状态 = #线程状态_运行
.计次循环尾 ()

调试输出 (线程数)

' 启动线程并保存句柄
.计次循环首 (线程数, i)
    线程信息 [i].线程句柄 = 启动线程 (&工作总线程, i, )
.计次循环尾 ()

2. 工作总线程(加入状态检查)

在核心循环的任务开始前加入安全检查点,这是最安全的暂停/退出时机(无COM调用、无未释放内存)。

.版本 2
.支持库 spec

.子程序 工作总线程
.参数 线程编号
.局部变量 hwnd
.局部变量 pid, 整数型
.局部变量 dm_ret, 整数型
.局部变量 dm, dmsoft
.局部变量 ptr, 整数型
.局部变量 diy, 参数结构
.局部变量 队列索引, 整数型
.局部变量 waitResult, 整数型

' 初始化COM
CoInitializeEx (0, 2)
线程信息 [线程编号].dm.创建 ()
dm = 线程信息 [线程编号].dm
dm.EnableShareDict (1)
dm.SetPath (“c:\test_game”)

' 绑定窗口
hwnd = 线程信息 [线程编号].窗口句柄
pid = 线程信息 [线程编号].pid
dm.SetWindowState (hwnd, 1)
dm_ret = dm.BindWindow (hwnd, “dx”, “dx2”, “windows”, 0)

.如果真 (dm_ret ≠ 1)
    调试输出 (“线程” + 到文本 (线程编号) + “窗口绑定失败!”)
    dm.释放 ()
    CoUninitialize ()
    CloseHandle (线程信息 [线程编号].暂停事件)
    返回 ()
.如果真结束

dm.LockInput (4)

' ==================== 核心循环 ====================
.判断循环首 (真)
    ' ============== 【安全检查点】每次取任务前检查状态 ==============
    .判断开始 (线程信息 [线程编号].状态)
        .案例 (#线程状态_停止)
            跳出循环 ()  ' 直接退出,去清理资源

        .案例 (#线程状态_暂停)
            ' 进入暂停等待循环
            .判断循环首 (真)
                ' 等待事件唤醒,超时100ms以便检测停止信号
                waitResult = WaitForSingleObject (线程信息 [线程编号].暂停事件, 100)

                ' 优先检测停止:暂停状态下也能响应停止
                .如果真 (线程信息 [线程编号].状态 = #线程状态_停止)
                    跳出循环 ()
                .如果真结束

                ' 收到恢复信号:重置事件,退出暂停
                .如果真 (waitResult = #WAIT_OBJECT_0)
                    ResetEvent (线程信息 [线程编号].暂停事件)
                    跳出循环 ()
                .如果真结束
            .判断循环尾 ()

            ' 唤醒后再次检查是否停止
            .如果真 (线程信息 [线程编号].状态 = #线程状态_停止)
                跳出循环 ()
            .如果真结束
    .判断结束

    ' ============== 正常取任务执行业务 ==============
    .如果 (队列.弹出整数 (ptr))
        diy.id = 指针到整数 (ptr)
        diy.name = 指针到文本 (ptr + 8)
        diy.pwd = 指针到文本 (ptr + 16)
        队列索引 = diy.id

        程序_延时 (10, )
        信息反馈 (队列索引, 3, 到文本 (hwnd))
        信息反馈 (队列索引, 4, 到文本 (pid))

        登录 (线程编号)
        刷怪 (线程编号)

        释放内存 (ptr)  ' 务必释放
    .否则
        程序_延时 (100, )  ' 队列空,让出CPU
    .如果结束

    系统_处理事件 ()
.判断循环尾 ()

' ==================== 资源清理(必须完整执行) ====================
dm.UnBindWindow ()
dm.释放 ()
CoUninitialize ()
CloseHandle (线程信息 [线程编号].暂停事件)

3. 暂停按钮

.子程序 _按钮_暂停_被单击
.局部变量 i, 整数型
.局部变量 线程数, 整数型

线程数 = 到整数 (编辑框_线程数量.内容)
.计次循环首 (线程数, i)
    .如果真 (线程信息 [i].状态 = #线程状态_运行)
        线程信息 [i].状态 = #线程状态_暂停
    .如果真结束
.计次循环尾 ()

说明:设置状态后,线程会在当前任务执行完毕后自动进入暂停,不会中途打断COM操作。

4. 恢复按钮

.子程序 _按钮_恢复_被单击
.局部变量 i, 整数型
.局部变量 线程数, 整数型

线程数 = 到整数 (编辑框_线程数量.内容)
.计次循环首 (线程数, i)
    .如果真 (线程信息 [i].状态 = #线程状态_暂停)
        线程信息 [i].状态 = #线程状态_运行
        SetEvent (线程信息 [i].暂停事件)  ' 置位事件,唤醒线程
    .如果真结束
.计次循环尾 ()

5. 停止按钮(安全等待式)

.子程序 _按钮_停止_被单击
.局部变量 i, 整数型
.局部变量 线程数, 整数型
.局部变量 句柄数组, 整数型, , "0"

线程数 = 到整数 (编辑框_线程数量.内容)
.如果真 (线程数 = 0)
    返回 ()
.如果真结束

' 1. 给所有线程下发停止指令
.计次循环首 (线程数, i)
    线程信息 [i].状态 = #线程状态_停止
    SetEvent (线程信息 [i].暂停事件)  ' 唤醒暂停中的线程
    加入成员 (句柄数组, 线程信息 [i].线程句柄)
.计次循环尾 ()

' 2. 等待所有线程自然退出(超时5秒,防止异常卡死)
WaitForMultipleObjects (线程数, 句柄数组, 真, 5000)

' 3. 关闭线程句柄
.计次循环首 (线程数, i)
    CloseHandle (线程信息 [i].线程句柄)
.计次循环尾 ()

' 可选:清空任务队列,释放未处理任务的内存
' 队列_清空 ()

五、进阶优化:细粒度停止响应

如果希望停止时不用等整个刷怪循环执行完,可以在 刷怪 子程序内部也加入状态检查:

.子程序 刷怪
.参数 m
.局部变量 dm, dmsoft
.局部变量 i, 整数型

dm = 线程信息 [m].dm
.计次循环首 (10, i)
    ' 每次操作前检测停止信号
    .如果真 (线程信息 [m].状态 = #线程状态_停止)
        返回 ()
    .如果真结束

    dm.KeyPress (116)
    dm.delay (1000)
.计次循环尾 ()

六、方案优势总结

  1. COM安全:完全规避强制挂起STA线程导致的死锁、崩溃问题
  2. 零CPU占用:暂停时线程阻塞在系统事件上,不消耗CPU
  3. 资源完整:线程退出时必定执行大漠释放、COM反初始化、句柄关闭
  4. 响应可靠:暂停状态下可直接响应停止,不会出现“暂停时点停止没反应”

这是易语言大漠多线程领域公认的工业级稳定方案,彻底解决了COM环境下线程控制的核心痛点。