Skip to content

进阶实战 04 · 多线程 threading:批量文件统计器 ​

《LingBuilder 进阶实战系列》第 4 集。第 3 集的异步查询让网络请求不再卡界面,这一集把同样的思想用到本地重活上:统计一个文件夹里有多少文件。你将学会多线程模块的核心三件套——线程_提交进度(后台干活)、进度回调(实时汇报)、完成回调(带回结果),以及多线程编程的第一条铁律:工作线程禁止直接碰控件,共享数据必须加锁。全程约 35 分钟。

前置要求:学完零基础入门系列与进阶前 3 集。本集源码包见文末。

这一集你将学会 ​

  • 线程_提交进度:把耗时任务提交到后台线程池
  • 工作处理器、进度处理器、完成处理器三者各司其职
  • 互斥锁:互斥锁_创建 + 互斥锁_执行,安全地把结果写回共享变量
  • 文件_枚举:一条命令枚举文件夹(可含子目录)
  • 编译器的线程安全警告怎么看、怎么修(本集真机复现并修复)

1. 三十秒理解:为什么需要多线程 ​

第 3 集的异步网络请求开了免提,这一集的「统计文件夹」是本地重活:递归枚举几千个文件同样耗时,如果放在按钮事件里直接干,窗口一样会卡死。多线程模块提供的方案和第 3 集如出一辙,只是角色更多:

角色谁来干在哪个线程能不能碰控件
工作处理器&统计工作后台线程❌ 绝对禁止
进度处理器&统计进度界面线程✅ 直接更新
完成处理器&统计完成界面线程✅ 直接更新

红线记住一句:只有工作处理器在后台,它永远不碰控件;要更新界面,走进度或完成处理器。

2. 新建项目与启用模块 ​

新建窗口项目:名称 批量文件统计器,模板「Windows 界面设计」,创建位置留空:

新建项目

启用两个模块:「多线程模块」(146 条命令,本集主角)和「文件目录模块」(88 条命令,提供 文件_枚举)。

3. 设计界面 ​

控件名称显示内容/占位左距顶距宽度高度
标签说明标签输入文件夹路径,后台线程统计文件数量(界面不卡)303056024
编辑框路径框(显示内容 C:\Windows\Fonts)308045028
按钮统计按钮开始统计495809528
按钮取消按钮请求取消3013010028
标签进度标签进度:等待启动3018056024
标签结果标签结果:—(字体大小 13)3022456028
标签状态标签就绪。3028056024

窗口宽 620、高 360。绑 3 个事件:统计按钮「被单击」、取消按钮「被单击」、窗口「创建完毕」。

设计器全景

4. 认识本集命令 ​

命令作用什么时候用
线程_提交进度(&工作, &进度, &完成, 参数)提交带进度汇报的后台任务耗时且想显示进度
线程_请求取消(任务)请求取消(协作式)用户点「取消」
线程_报告进度(百分比, 说明)工作处理器里汇报进度后台干活时
文件_枚举(目录, 通配符, 含子目录, 结果数组)枚举文件到文本数组,返回数量统计、批量处理
互斥锁_创建() / 互斥锁_执行(锁, &处理器, 参数...)创建锁 / 持锁执行处理器(自动解锁)后台线程写共享变量

5. 写代码 ①:五个程序集成员 ​

程序集变量表加 5 个成员(第 3 集的成员表画法):

名称类型初始值
统计任务线程任务(空)
结果锁线程互斥锁互斥锁_创建()
统计目录文本型""
结果文件数整数型0
结果文本数整数型0

五个成员

两个新面孔:

  • 线程任务:线程_提交* 返回的任务句柄类型,后续取消、查状态都用它。
  • 线程互斥锁:初始值直接写 互斥锁_创建()——窗口创建时这把锁就建好了,全程复用。

6. 写代码 ②:三个处理器 ​

lcpp
事件 _统计按钮_被单击()
    统计目录 = 文本_删首尾空白(控件_取文本(路径框))
    如果 (统计目录 = "")
        信息框("请先填写要统计的文件夹路径", 48, "提示")
        返回()
    如果结束
    控件_设置文本(状态标签, "已在后台线程开始统计,界面不卡…")
    统计任务 = 线程_提交进度(&统计工作, &统计进度, &统计完成, 0)
结束

事件 _取消按钮_被单击()
    线程_请求取消(统计任务)
    控件_设置文本(状态标签, "已发出取消请求,等待工作处理器协作停下")
结束

按钮先把路径存进成员变量 统计目录(工作处理器在后台线程读它),再一句 线程_提交进度 提交任务——提交立即返回,按钮事件瞬间结束,这就是「不卡界面」的全部秘密。

核心的工作处理器:

lcpp
空 统计工作(整数型 保留参数)
    局部 文本型 文件列表[]
    局部 文本型 文本文件列表[]
    局部 整数型 文件数
    局部 整数型 文本数
    线程_报告进度(30, "正在枚举全部文件…")
    文件数 = 文件_枚举(统计目录, "", 真, 文件列表)
    线程_报告进度(80, "枚举完成,正在统计文本文件…")
    文本数 = 文件_枚举(统计目录, "*.txt", 真, 文本文件列表)
    互斥锁_执行(结果锁, &写入结果, 文件数, 文本数)
    线程_报告进度(100, "统计完成")
结束

后台统计与锁

  • 工作处理器在后台线程执行——这里从头到尾没有碰任何控件。
  • 文件_枚举(目录, "", 真, 文件列表):空通配符匹配全部文件,真 表示连子目录一起递归,命中的完整路径写进文本数组,返回数量。
  • 互斥锁_执行(结果锁, &写入结果, 文件数, 文本数):本集的点睛之笔。工作线程要把结果写进两个共享变量(结果文件数/结果文本数),多线程同时读写共享数据是出错的根源。这行代码「持着锁调用写入结果处理器」,处理器干完活锁自动释放(RAII),从机制上保证写操作不会和界面线程的读操作撞车。
  • 两个局部计数 文件数/文本数 也遵守「先声明、再赋值」——它们依赖 文件_枚举 的执行结果。

配一个只在持锁时才执行的小处理器,专职写共享值:

lcpp
空 写入结果(整数型 文件数, 整数型 文本数)
    结果文件数 = 文件数
    结果文本数 = 文本数
结束

最后是进度和完成处理器——它们在界面线程,可以放心碰控件:

lcpp
空 统计进度(线程任务 任务, 整数型 百分比, 文本型 说明)
    控件_设置文本(进度标签, "进度 " + 到文本(百分比) + "%:" + 说明)
结束

空 统计完成(线程任务 任务)
    控件_设置文本(结果标签, "统计完成:共 " + 到文本(结果文件数) + " 个文件,其中 .txt 文本文件 " + 到文本(结果文本数) + " 个")
    控件_设置文本(状态标签, "统计完成(完成处理器在界面线程执行)")
结束

进度与完成

7. 编译:先看错误列表,再认识线程安全警告 ​

点「编译当前项目」。如果你偷懒没写互斥锁、直接在工作处理器里写 结果文件数 = 文件数,错误列表会出现两条线程安全警告:

工作处理器 统计工作 写入共享值 结果文件数,但未检测到互斥锁保护。

本集制作时就真机复现了这两条警告——它们不阻断编译,但 multi-thread 下不加锁的共享写入是真正的事故来源。按本集写法补上 互斥锁_执行 后警告消失,错误列表归零:

(警告与清除过程见上一节代码对比;本集最终编译错误列表 (0)、输出窗口「编译成功。」)

8. 运行:看着后台统计跑完 ​

点「运行」。初始状态,路径已预填 C:\Windows\Fonts:

初始态

点「开始统计」,约一秒后统计完成——全程窗口可以随便拖动:

统计完成

三个信息各就各位:进度 100%、共 349 个文件(.txt 文本文件 0 个——字体目录里确实没有 txt)、状态栏提示完成处理器在界面线程执行。

关于「请求取消」:取消是协作式的——线程_请求取消 只是发出请求,真正停下来需要工作处理器主动调用 线程_是否请求取消 检查点。本例的 文件_枚举 是一条阻塞命令,枚举期间无法插入检查,所以取消只在两步枚举的间隙生效。需要强取消的长任务,把大任务拆成小步、每步之间检查取消标志即可(多线程模块完整演示项目里有范例)。

9. 常见问题急救表 ​

症状原因解法
错误列表出现「写入共享值…未检测到互斥锁保护」警告工作处理器直接给共享成员赋值用 互斥锁_执行(锁, &处理器, 参数) 持锁写入
在工作处理器里调 控件_设置文本 报错或界面异常工作线程禁碰控件改用 线程_报告进度 或把更新挪进完成处理器
线程_请求取消 没停协作取消需要处理器主动检查 线程_是否请求取消拆小步、在循环里检查
局部变量值总是旧值/0声明带初始化被提升到子程序最前先声明、再赋值
文件_枚举 返回 0目录不存在检查路径;目录不存在时返回 0

本集小结 ​

  • 线程_提交进度(&工作, &进度, &完成, 参数):三处理器分工,工作在后台、进度/完成回界面。
  • 工作线程禁碰控件;共享数据用 互斥锁_创建 + 互斥锁_执行 持锁读写。
  • 文件_枚举 一条命令递归枚举文件;线程安全警告是编译器送的免费审查。
  • 取消是协作式的:请求发出后,工作处理器在检查点才真正停下。

下一集预告:《正则表达式 std.regex》——让程序拥有一双「文本搜索鹰眼」,从一堆日志里批量提取邮箱、链接,学会「宽松解码+正则」的形态校验范式。

回系列总览