前阵子翻自己硬盘上那些易语言旧笔记,又看到一批关于线程API模块的整理记录,其中最常用的就是CreateThread线程创建启动这一块。恰好那段时间帮几个群友排查过线程相关的崩溃问题,很多现象最后都能绕回到对CreateThread理解不够深上。所以干脆把这些内容重新过一遍,把易语言里封装API线程模块、创建线程、传递参数、管理线程句柄这些环节,按实际操作的顺序完整写出来。
这套内容适合正在写易语言模块、需要做后台任务、或者想搞明白“子程序指针到线程入口”到底是怎么回事的人。看完可以直接抄代码,也能避开一些我踩过的坑。
1. 为什么放着现成的启动线程不用,非要自己封装CreateThread
1.1 内置“启动线程”命令的真实边界
易语言核心库自带一个“启动线程”命令,很多新手第一反应都是直接用它。这个命令的签名大概是:启动线程(子程序指针, 参数, 线程ID),用起来确实方便,三行代码就能起一个后台任务。比如想在后台循环处理一堆数据,把要执行的子程序地址传进去,再传一个整数参数,线程就起来了。
但用久了就会发现它有几个卡手的地方。第一,它不直接给你线程句柄。没有句柄就意味着你没办法安全地等待这个线程结束,也没办法挂起它、恢复它、设置它的优先级。第二,创建标记控制不了。你没法用CREATE_SUSPENDED让它先挂起,等初始化完成再启动。第三,底层细节黑盒化之后,一旦线程出问题,排查手段也少。比如你想确认线程是不是真的起来了,只能靠线程ID和全局标记去猜,没法通过句柄去查线程状态。
不是说内置命令不好,而是它面向的是“快速完成任务”的场景。一旦你开始写模块、写工具,需要同时管理多个线程、控制线程启停顺序的时候,内置命令就不太够用了。CreateThread是Windows系统最底层的线程创建接口,易语言里的启动线程本质上也是走到这一层,只是帮你挡了一层。直接调CreateThread,不是更复杂,而是把控制权拿回自己手里。
1.2 API模块适合的场景和收益
那什么场景下我会推荐自己封装一套线程API模块?
如果你要做一个长期跑的后台线程池,或者要做等待多个线程全部结束之后再统一汇总的程序,这时候你必须拿到每一个线程的句柄。句柄是线程对象的“身份证”,WaitForSingleObject、WaitForMultipleObjects、SetThreadPriority、SuspendThread这些API全都认这个句柄。没有句柄,这些高级控制全都免谈。
还有一个场景是传参。内置启动线程允许传一个整数参数,但如果我想传一段文本、一个自定义数据结构呢?内置命令没有直接办法。用API配合申请内存、指针到文本,就能把任意复杂度的参数传给线程函数。这个自由度的提升是很明显的。
再有就是封装成模块这件事本身。作为模块作者,你肯定希望把线程创建、等待、关闭句柄、设置优先级这些功能做成一套稳定接口,让其他调用者不用每次重新处理Windows API的细节。用同样的逻辑写一次,以后项目里直接引用模块就行。这也是“API模块”这个标题里最核心的那层意思。
2. 搭一个线程操作API模块的底座
2.1 CreateThread函数签名与易语言DLL声明
要封装CreateThread,首先要理解它的C语言原型。这个是Windows SDK里固定的:
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );六个参数分别是什么意思呢?
第一个参数lpThreadAttributes是安全描述符,通常传NULL,也就是0,让系统用默认安全属性。第二个参数dwStackSize是新线程栈大小,传0表示使用系统默认值,在大多数情况下默认就够用,不需要调。第三个参数lpStartAddress才是关键,它是线程入口函数的地址,在易语言里,这一步就是子程序指针。第四个参数lpParameter是传给这个入口函数的参数,本质上是一个void指针,在易语言中对应整数型内存地址,可以是0,也可以是一个有意义的数据地址。第五个参数dwCreationFlags控制创建行为,传0表示创建后立即运行,传CREATE_SUSPENDED也就是4,表示创建后先挂起。第六个参数lpThreadId是一个输出参数,函数会在这里写入新线程的ID。
C语言原型清楚了,易语言的DLL命令声明就照猫画虎:
.版本 2 .DLL命令 CreateThread, 整数型, "kernel32", "CreateThread" .参数 lpThreadAttributes, 整数型 .参数 dwStackSize, 整数型 .参数 lpStartAddress, 整数型 .参数 lpParameter, 整数型 .参数 dwCreationFlags, 整数型 .参数 lpThreadId, 整数型, 传址注意最后那个lpThreadId,声明里面一定要写成“传址”。C指针本质上是把变量的地址传进去,函数往里写值。不写传址,函数拿到的是线程ID的值,但写不回来,你拿不到线程ID。这个问题我见过不少新手栽过:返回值句柄明明不为0,线程也起来了,但取出来的线程ID永远是0,查来查去最后才发现是传址漏了。
返回值是线程句柄,这个句柄不是普通的窗口句柄,而是内核对象句柄,后面要用CloseHandle关闭。
2.2 几个必须配套的API:句柄关闭、等待、挂起恢复
光有CreateThread不够,管理线程必须配套其他API。我最常用的几个,一起声明在同一个模块里,日常管理线程就够用了。
CloseHandle负责关闭句柄。CreateThread返回的句柄不会因为线程结束就自动消失,你不主动关闭它,它会一直占用句柄表项和内核对象引用。声明是这样的:
.DLL命令 CloseHandle, 整数型, "kernel32", "CloseHandle" .参数 hObject, 整数型WaitForSingleObject用来等待线程结束。它等待的核心句柄,如果句柄对应的线程正在运行,等待函数就阻塞;线程结束,函数返回。第二个参数dwMilliseconds是超时时间,传0xFFFFFFFF也就是-1,表示无限等待。这个函数返回值里,0代表对象有信号,也就是等待成功;0x00000102代表超时;-1代表调用失败。
ResumeThread和SuspendThread是一对,用来恢复和挂起线程。用CREATE_SUSPENDED方式创建的线程,需要ResumeThread才能开始运行。SuspendThread可以用来暂停一个正在运行的线程,但这玩意儿非常容易死锁,后面我会专门说。
SetThreadPriority用于设置线程优先级,比如把后台低优先级任务调低一点,避免抢主界面资源。
这些API的声明大同小异,全部放在一个“线程API模块”里,后续所有子程序引用同一个声明,维护起来很清爽。
2.3 对外封装:线程_创建、线程_等待、线程_关闭
API层有了,接下来封装成易语言人能直接调用的子程序。我个人习惯的接口设计是三层:底层DLL声明、中间封装子程序、上层应用调用。中间封装尽量把公共逻辑处理掉。
最核心的封装就是“线程_创建”。它对外暴露四个参数:线程函数地址、线程参数、返回线程ID、创建标志。线程函数地址这里用整数型,调用的时候传“到整数(&线程函数)”,这样最不会出幺蛾子。
.子程序 线程_创建, 整数型, 公开 .参数 线程函数, 整数型 .参数 线程参数, 整数型 .参数 线程ID, 整数型, 传址 .参数 创建标志, 整数型, 可空 .局部变量 线程句柄, 整数型 线程句柄 = CreateThread (0, 0, 线程函数, 线程参数, 创建标志, 线程ID) 返回 (线程句柄)这里返回的就是线程句柄。调用方拿到这个句柄后,要么等待结束再关闭,要么立即关闭句柄让线程自己跑。这两个路径都合法,区别在于你还想不想获得线程的结束信号。
再封装一个“线程_等待”:
.子程序 线程_等待, 整数型, 公开 .参数 线程句柄, 整数型 .参数 超时时间, 整数型, 可空 .局部变量 等待时间, 整数型 .如果 (是否已提供 (超时时间)) 等待时间 = 超时时间 .否则 等待时间 = -1 .如果结束 返回 (WaitForSingleObject (线程句柄, 等待时间))使用“是否已提供”判断可空参数,这是在易语言里处理默认值最稳妥的方式。如果调用者不传超时时间,默认无限等待;传了就用传的值。
再有一个“线程_关闭”:
.子程序 线程_关闭, 整数型, 公开 .参数 线程句柄, 整数型 返回 (CloseHandle (线程句柄))这三件套封装好,基础模块就算搭完了。后面不管写什么线程场景,底层都是这套东西在干活。
3. 真正把线程创建出来跑起来
3.1 线程任务子程序必须带一个整数参数
很多第一次直接用CreateThread的人,最容易犯的错是:线程子程序写成了无参子程序。
比如我见过有人这样写:
.子程序 我的任务 ' 执行一些操作然后把“到整数(&我的任务)”传给CreateThread。结果程序一跑起来,要么闪退,要么线程起来后莫名其妙崩溃,还有的是先正常运行几次然后突然崩溃。原因在于CreateThread回调线程入口函数时,Windows会按固定调用约定传入一个参数,参数值就是你传给它的lpParameter。如果入口函数声明无参数,调用栈帧里的这个参数没人清理,栈就被破坏掉,崩溃只是时间问题。
正确写法是,任何准备交给CreateThread的子程序,都必须带一个整数型参数,名字随意,实际上这个参数接收到的就是创建线程时传的lpParameter值:
.子程序 后台任务 .参数 参数数据, 整数型 ' 在这里写你的任务逻辑就算你根本不想传参数,创建线程时传0,这个子程序也要保留这个参数位置,否则栈平衡会出问题。这一点怎么强调都不过分,是API线程能不能稳定跑起来的前提。
3.2 传递整数参数与传递文本参数的两种做法
线程参数传什么、怎么传,这里有一套固定打法。
最简单的是传整数。比如我想让线程知道自己的序号,直接传一个整数就行:
线程句柄 = 线程_创建 (到整数 (&后台任务), 10, 线程ID, 0)线程那边从参数数据里拿到的就是10。这种传参方式干净利落,不需要考虑内存释放。
传文本就没这么直接了。因为文本不是单个整数,它是一段连续内存里的字符序列,你传给线程的应该是指向这段内存的指针。在易语言里,最可靠的办法是手动申请一块内存,把文本内容写进去,然后把内存地址作为整数参数传给线程,线程拿到地址后读出来,用完释放。
主线程那边这样写:
.子程序 按钮_启动线程_被单击 .局部变量 任务文本, 文本型 .局部变量 内存地址, 整数型 .局部变量 文本字节集, 字节集 .局部变量 线程ID, 整数型 .局部变量 线程句柄, 整数型 任务文本 = “线程任务-编号001” 文本字节集 = 到字节集 (任务文本) 内存地址 = 申请内存 (取字节集长度 (文本字节集) + 1) 写到内存 (文本字节集, 内存地址, 取字节集长度 (文本字节集)) 写到内存 (0, 内存地址 + 取字节集长度 (文本字节集), 1) 线程句柄 = 线程_创建 (到整数 (&后台任务), 内存地址, 线程ID, 0)这里申请内存的长度比文本字节集长度多1,并在最后补一个0,是为了让这块内存成为合法的零结尾字符串。指针到文本命令靠这个结尾0来识别字符串结束位置,少了这个0,轻则读出一堆乱码,重则越界访问崩溃。
线程那边的子程序这样写:
.子程序 后台任务 .参数 参数数据, 整数型 .局部变量 任务文本, 文本型 任务文本 = 指针到文本 (参数数据) ' 执行任务逻辑 释放内存 (参数数据)关键是,这块内存在线程里读取任务文本之后,要由线程负责释放。不能主线程创建完就马上释放,因为线程什么时候开始读是不确定的,如果主线程先释放了,线程去读一块已经还给系统的内存,就是典型的悬空指针,程序可能立刻崩溃,也可能过一会儿才崩溃。让线程自己释放,内存谁申请谁使用、谁释放,责任边界最清晰。
3.3 一个多线程并发完成计数的完整例子
单个线程跑通之后,我们做一个稍微真实点的例子:创建5个线程,每个线程做一个耗时循环,全部结束后汇总完成数量。
全局变量有一个完成计数:
.全局变量 完成计数, 整数型线程子程序里,循环结束后对完成计数做原子自增,然后通过标签反馈事件通知界面:
.子程序 多线程任务 .参数 参数数据, 整数型 .局部变量 i, 整数型 .局部变量 临时值, 整数型 计次循环首 (2000000, i) 临时值 = i × 2 计次循环尾 () InterlockedIncrement (完成计数) 标签_通知.调用反馈事件 (完成计数, 0)InterlockedIncrement是Windows的原子操作API,它保证在多线程同时修改一个整数时,自增操作不会被其他线程打断。直接用完成计数=完成计数+1这种写法,在单线程下没问题,多线程下可能两个线程同时读取旧值,同时写回新值,最终只加了一次。原子操作就是为了解决这个问题的。
主线程启动5个线程:
.子程序 按钮_并发启动_被单击 .局部变量 线程句柄, 整数型 .局部变量 线程ID, 整数型 .局部变量 n, 整数型 完成计数 = 0 计次循环首 (5, n) 线程句柄 = 线程_创建 (到整数 (&多线程任务), n, 线程ID, 0) 线程_关闭 (线程句柄) 计次循环尾 ()注意这里创建完线程后立即关闭了句柄,因为我并没有打算等待线程结束,线程的结束信号改用标签反馈事件来通知。线程对象不会因为句柄关闭而被销毁,它还在内核里运行,只是外部不再持有这个句柄。这种模式适合“发出去就不管,靠回调收结果”的场景。
标签反馈事件那边显示当前计数值,当数值等于5时,就知道全部线程跑完了。
3.4 后台线程通知界面的标准姿势
后台线程里直接操作窗口组件,是易语言线程开发里最大的雷区。我见过有人在线程里直接给编辑框赋值:
编辑框1.内容 = “结果出来了”这个操作十次里可能八次没事,但偶尔就会让窗口卡死、闪退、甚至整个程序消失。为什么?因为窗口组件绝大部分不是线程安全的,它们依赖窗口线程的消息循环。你用后台线程直接操作组件,相当于有人从外面往你办公桌上扔东西,你正在整理文件,桌上一下子就乱了。这是典型的跨线程UI访问冲突。
我在自己项目里的标准做法是,用隐藏标签的反馈事件来做线程到界面的桥。窗口上放一个标签“标签_通知”,在它的反馈事件里接收参数,然后安全地更新界面:
.子程序 标签_通知_反馈事件, 整数型 .参数 参数一, 整数型 .参数 参数二, 整数型 编辑框1.内容 = 到文本 (参数一) 返回 (0)线程任务里需要通知界面时,调用“标签_通知.调用反馈事件(完成计数, 0)”。这个调用会触发标签的反馈事件,从而把数据送到界面层。使用这种机制,尽量不在后台线程里直接碰窗口组件,界面的稳定性会明显上一个台阶。
4. 线程生命周期里的几个关键节点
4.1 句柄为什么必须关闭,以及什么时机关
CreateThread返回的句柄,很多新手误以为线程结束它就自动消失了。真实情况不是这样。线程对象在内核里有两个东西决定它的存亡:一个是线程本身是否运行结束,另一个是还有没有打开的句柄引用它。只要句柄未关闭,即使线程跑完,线程对象也不会被完全释放,它会在内核里保留退出码等信息。
如果每次创建线程都忘了关闭句柄,程序运行时间长了,系统的句柄表和内核内存就会被逐渐耗尽。任务管理器里看“句柄数”一路飙升,程序越来越慢,最后可能连新线程都创建失败。
但关闭时机有讲究,这个必须要说透。
如果我还想等这个线程结束,那就不能创建完立即关闭句柄。同一时间我既等它,又手里没句柄,那是空等。正确顺序是先WaitForSingleObject等待,等返回之后,再CloseHandle。
如果我不需要等它结束,走的是“异步回调通知”模式,那创建完线程后,立即CloseHandle就可以。线程继续跑,句柄提前释放,不会影响线程执行,只影响未来对它的同步控制。
还有一种情况是,线程要运行很长时间,我中间可能需要用SetThreadPriority改它的优先级。那这个句柄就得先留着,直到用完了再关。
一句话总结:句柄关闭前要想清楚,你还需要不需要通过句柄跟这个线程发生关系。不需要,立即关;需要,用完再关。
4.2 挂起方式启动线程、设置优先级后再恢复
CREATE_SUSPENDED是CreateThread第五个参数里一个非常实用的标记,值是4。创建线程时带上它,线程创建后不会立刻执行,而是处于挂起状态。等你想启动它了,再调用ResumeThread。
这个机制适合什么场景?适合在线程真正开始跑之前,先调整它的状态。最常见的操作是设置优先级。比如这个线程要做一个庞大的后台计算,但不希望它抢占界面资源,可以先把线程挂起,设置成低于正常级别,再恢复运行。
操作顺序大概是:
.子程序 挂起启动线程示例 .局部变量 线程句柄, 整数型 .局部变量 线程ID, 整数型 线程句柄 = 线程_创建 (到整数 (&后台任务), 0, 线程ID, 4) SetThreadPriority (线程句柄, -1) ResumeThread (线程句柄) 线程_关闭 (线程句柄)SetThreadPriority第二个参数里,-1表示比正常优先级低一级,0是正常,1是高一级,2是很高,15是最高。后台任务用-1就挺合适。等优先级调好,再ResumeThread让它跑起来。
这里有个细节,CREATE_SUSPENDED创建后,如果中途代码报错没执行ResumeThread,这个线程就会一直挂在内存里不运行,相当于漏了一个僵尸线程。所以“创建挂起”的线程一定要确保在合适的路径里被恢复,所有分支都要覆盖到,否则很恶心。
4.3 WaitForSingleObject等待线程退出的正确用法
等待线程结束看起来简单,就是把句柄传给WaitForSingleObject。但超时参数怎么定,返回值怎么判断,是有讲究的。
超时传-1是无限等待。适合确认线程必然会结束的场景。举个例子,主程序退出前需要等待工作线程完成收尾,用无限等待是合理的,因为线程是一个循环任务、有明确的退出标志。
但程序关闭时的“无限等待”要小心。如果线程因为某些原因卡住了,你又无限等它,那程序就会卡在退出阶段,窗口没了但进程还在后台,任务管理器里显示“正在运行”。我通常会给退出等待加一个超时,比如3000毫秒,超时了就不再死等,直接强制走后续逻辑。
WaitForSingleObject返回0表示线程已结束,返回102表示超时,返回-1表示调用失败。写判断的时候建议别写成“返回值=0就怎么”,而是要区分超时和失败。我自己的习惯是:
.子程序 等待线程安全退出 .参数 线程句柄, 整数型 .局部变量 等待结果, 整数型 等待结果 = WaitForSingleObject (线程句柄, 3000) .如果真 (等待结果 = 0) ' 线程正常结束 .如果真结束等待超时后,我一般会输出一条日志,提醒自己这个线程没有按时退出,再排查它卡在哪。
5. 实操踩坑清单与排查思路
5.1 线程里直接操作窗口组件,界面秒崩
这个坑基本上每个玩易语言线程的人都会踩一次。具体表现有两种:一种是线程里给编辑框赋值后,界面偶尔卡死,多触发几次必然崩溃;另一种是线程里读取组件内容,读出来的值不对,甚至直接崩溃。
核心原因就是跨线程访问组件。窗口组件的内部实现和消息机制都绑定在创建它的线程上,后台线程去操作它,就是非法访问。
我的铁律是:线程里面只处理数据和逻辑,所有界面相关操作全部通过标签反馈事件、消息通知等方式转回主线程执行。如果实在要在线程里拿编辑框内容,也应该先把内容保存到全局变量或内存里,线程去读这个副本,而不是直接读组件。
5.2 参数内存被主线程提前释放,线程读野指针
用申请内存传文本参数的时候,最常见的翻车方式就是主线程在CreateThread之后马上调用释放内存,想着“反正参数传过去了,内存没用了”。结果线程那边还没开始读,内存就已经还给系统。线程一访问这块地址,轻则读到其他程序写进来的一些残废数据,重则直接触发内存访问违例,整个进程崩掉。
正确的资源边界是:谁负责释放,谁就有最终使用权。我在模块设计时的约束是,凡是线程函数收到的地址参数,统一由线程函数体负责释放。主线程申请完内存后,不做任何释放操作,完全信任线程。线程函数的第一件事就是解析参数,然后立刻释放,不给内存泄漏留机会。
5.3 大量线程创建后不关句柄,句柄数暴涨
我之前有段时间写一个并发下载工具,每个下载任务起一个线程,当时图省事,创建完线程就不管了,也不关闭句柄。程序才跑了一个小时,任务管理器里句柄数就涨到了几万,到后面整个程序响应越来越慢,新任务根本起不来。
排查思路很简单,打开任务管理器,找到进程,看句柄数是不是随着任务数量线性增长。如果只增不减,基本就是句柄泄漏。解决方法是按我前面说的,创建线程后不需要等待就立即CloseHandle,需要等待就等待完再CloseHandle,保证每一次CreateThread都配对一次CloseHandle。
5.4 程序退出时线程没停,进程卡住
程序关窗口的时候,如果还有后台线程在循环里跑,易语言默认的退出流程可能不会等这些线程,或者等待逻辑处理不当,导致程序界面关了但进程还在。这个现象很常见,表现是窗口没了,但任务管理器里进程还在,占着CPU。
我的处理方式是在窗口销毁事件里设置一个全局退出标志,然后等待所有工作线程退出。线程循环体每次循环检查这个标志,为真就跳出循环。等待时用有限超时,比如3秒,别无限等。这样做既给了线程收尾的机会,也防止程序退出被卡死。
5.5 线程卡死、疑似死锁的排查方向
线程卡死是另一个老大难问题。有时候程序没崩溃,但某个线程一直不干活,任务管理器里看到CPU占用很高可任务没进展,或者CPU占用是零但线程也没退出。
最容易出现的死锁场景是:线程A持有一个临界区,等待线程B的事件;线程B持有一个事件,又在等待线程A释放临界区。两边互相等,谁也不让谁。排查时候最快的方法是把所有线程的栈信息dump出来看。不过易语言环境里dump线程栈比较麻烦,我一般会先在代码里埋日志,每个线程进入关键区域前、拿到锁后、释放锁后都打印一行记录。只要看日志停在哪个节点,就基本能定位死锁位置。
还有一个隐蔽的场景是SuspendThread挂起了一个正在持有临界区的线程,然后主线程尝试进入同一个临界区,结果主线程也卡住了。这也是我为什么一直提醒慎用SuspendThread的原因。暂停线程不是一个好的暂停方案,它会把线程正在持有的资源也一起冻结,这个冻结状态往往就是灾难的开始。
6. 模块后续还能怎么扩展
6.1 线程同步:临界区、互斥量和原子操作的取舍
CreateThread只负责把线程创建出来,真正复杂的多线程逻辑里,线程之间必然要共享数据,这时候同步就绕不开。
如果多个线程都要读写同一个全局变量,最快的方式是用InterlockedIncrement这类原子操作,适合计数场景,不用加锁,效率高。但如果是写一个复杂的全局数据结构,比如字典、列表,原子操作就不够用了,需要临界区或者互斥量。
临界区在易语言里对应InitializeCriticalSection、EnterCriticalSection、LeaveCriticalSection、DeleteCriticalSection这组API。临界区适合同一进程内多个线程保护共享数据,进入时调用EnterCriticalSection,出来时LeaveCriticalSection,再配合一个退出标志,是易语言里比较稳妥的组合。互斥量CreateMutex则适合跨进程同步,用法和临界区类似,但系统开销更大一点。
我自己的选择标准很简单:同一进程内保护普通数据,用临界区;需要跨进程或者需要等待超时的场景,用互斥量或事件;只是做计数器,一律用原子操作。
6.2 从“单线程创建”到“简单线程池”的演进
模块用熟了之后,你会发现频繁创建、销毁线程本身也是有开销的。线程从创建到销毁,内核要分配线程栈、初始化线程对象,这些不是免费午餐。如果程序里每秒钟要处理几十个任务,每次都现创建线程,性能会很糟。
比较好的演进方向是做一个简单线程池。提前创建固定数量的工作线程,它们都在一个循环里等待任务;主线程把任务放进队列,用信号量或者事件唤醒一个空闲工作线程去处理。任务做完了,工作线程不退出,回到循环继续等下一个任务。
用易语言实现这个结构,核心是三个组件:一个受临界区保护的任务队列、一个用于唤醒线程的信号量或事件、一组停留在等待状态的工作线程。线程池的好处是线程数量可控,不会因为任务暴涨而创建几百个线程把系统资源耗尽。
真要在易语言里做一个通用线程池,还需要处理线程退出、任务分发、空转等待这些边角问题,比单独的CreateThread封装要复杂不少,但底子仍然是这一套API模块。先把线程创建、句柄管理、参数传递、同步机制这几块玩明白,后面再往上搭建就顺理成章了。
我自己把这个线程API模块封装完后,最大的感受是:易语言贴心的内置命令确实好用,可一旦想把程序控制力往前推一步,直接面对API反而没那么可怕。CreateThread的参数虽然比启动线程多几个,但每一个都有明确的含义。把六个参数逐个弄明白,配套的句柄和等待机制再对上号,线程这块基本就通了一半。遇到奇怪的线程崩溃,先别怀疑易语言能力不行,多数时候是我们自己把句柄时机、参数内存、跨线程组件这三件事处理得太草率了。这套模块到现在我还在持续用,每次翻笔记都觉得当初把这几个坑完整记录下来是值得的。