马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_thread 把耗时任务丢到后台,界面不再卡死
阅读说明
1. 适用版本:PB 9 及以上(本文按 PB 12.5 + PBIDEA 1.x 验证;涉及 websuite.pbl 一个库)
2. 支持数据库:无(本文示例不连接任何数据库,纯演示线程调度)
3. 操作系统与环境要求:Windows 7+,已安装 PBIDEA 运行库(PbIdea.dll 等);应用库需包含 websuite.pbl
4. 难度系数:★★★★☆
5. 其它阅读说明:前置知识为 PowerScript 基本语法、NVO(自定义类)与自定义事件;建议先看 Day 4《PBIDEA:用 uo_httpclient 发 HTTP 请求》了解 PBIDEA 对象的引入方式
一、先看问题:耗时操作把界面拖死了
PowerBuilder 是单线程消息循环:界面重绘、按钮点击、你的业务代码全都挤在同一个线程里跑。当你在按钮里执行一个 HTTP 大文件下载、批量解析、或者几百万次的循环,消息循环被长时间占用,窗口就"假死"了——拖不动、点不了、标题栏显示"未响应"。用户等得不耐烦,直接关进程。
解决办法不是把循环写得更快,而是把耗时操作挪到另一个线程去跑,界面线程只负责接收进度和结果。PBIDEA 的 websuite.pbl 里封装了一对现成的对象:uo_thread(线程)和 uo_thread_factory(线程工厂),就是干这个的。
二、两个对象怎么分工
先认识成员(均对照本机导出源码 uo_thread.sru / uo_thread_factory.sru 核实):
| 对象 | 常用成员 | 作用 | | uo_thread | StartThread(对象名, 函数名, 参数...)(在工厂上) | 启动一个后台线程,自动实例化对象并调用指定函数,返回线程句柄 | | InvokeFactory(例程名, 参数...) | 在线程里回调主线程的 factory 事件/函数(如汇报进度) | | SetGlobalVar(名, 值) / GetGlobalVar(名) | 跨线程读写全局值(值类型为 any) | | Lock() / UnLock() | 跨线程访问前的资源锁 | | ExitThread() | 线程入口函数退出前调用,关闭本线程(不调则 factory 一直等待) | | GetThreadName() | 返回当前线程的实例名 | | uo_thread_factory | GetThreadCount() | 当前还有几个线程在运行 | | GetThreadList(ref string[]) | 取线程名列表,返回个数 | | WaitThreads(毫秒) | 等待线程结束 | | CloseThread(线程ID) | 关闭一个线程(内部自动 SharedObjectUnregister) |
工作流一句话:主线程 StartThread 起线程 → 后台自动 new 出线程体对象并执行入口函数 → 线程内 InvokeFactory 回调主线程更新界面 → SetGlobalVar 存结果 → ExitThread 收尾。
三、最小可运行示例:后台跑 5 步,界面不卡
下面用"模拟耗时任务"演示完整链路:后台每步睡 1 秒,共 5 步;期间界面保持响应,进度通过回调显示,完成后弹窗。
步骤 1:建线程体对象 uo_worker(继承 uo_thread)
新建 Custom Class(NVO),父类选 uo_thread。它要跑在后台线程里,入口函数由 StartThread 自动调用。
前置:工程库列表已包含 websuite.pbl;在 PBIDEA 的 library painter 里新建对象,父类选择 uo_thread,命名为 uo_worker。
步骤:1) 在 uo_worker 的声明区(Declare → Local External Functions)贴入下面的 WinSleep 声明,模拟耗时用;2) 新建公共函数 of_work(long al_times),贴入函数体;3) 保存。 - // ===== uo_worker 声明区:声明 Windows 睡眠函数(模拟耗时)=====
- Subroutine WinSleep(ulong dwMilliseconds) LIBRARY "kernel32.dll"
- // ===== of_work(long al_times):线程入口函数,由 StartThread 自动调用 =====
- // 示例输入:耗时次数(由主线程 StartThread 的第 3 个可变参数传入)
- long ll_times
- ll_times = al_times
- int li_i
- for li_i = 1 to ll_times
- // 模拟耗时操作:每步睡 1 秒(真实场景换成 HTTP 下载 / 大文件解析 / 批量计算)
- WinSleep(1000)
- // 回调主线程:触发 factory 上的 ue_progress 事件,汇报进度
- InvokeFactory("ue_progress", li_i, ll_times)
- next
- // 跨线程写结果:先加锁再写,避免多线程同时写冲突
- Lock()
- SetGlobalVar("gs_worker_result", "后台任务完成,共处理 " + string(ll_times) + " 步")
- UnLock()
- // 读回刚写入的全局值,确认写入成功
- string ls_check
- Lock()
- ls_check = GetGlobalVar("gs_worker_result")
- UnLock()
- // 输出结果(MessageBox 放在 ExitThread 之前,确保能弹出)
- MessageBox('后台线程', GetThreadName() + ':' + ls_check)
- // 退出函数前必须调用 ExitThread(),否则 factory 会一直等待本线程
- ExitThread()
复制代码
步骤 2:建线程工厂子类 uo_myfactory,加进度回调事件
uo_thread_factory 本身只带 ue_start / ue_exit 两个事件。要接收我们自定义的进度回调,就继承它建一个子类,把回调事件加在子类上。回调事件运行在主线程,在这里更新界面是安全的。
前置:新建对象 uo_myfactory,父类选择 uo_thread_factory;在其上新增自定义事件 ue_progress(integer ai_done, integer ai_total)。
步骤:1) 把下面代码贴进 ue_progress 事件;2) 保存。示例中 st_progress 是主窗口 w_demo 上的静态文本控件(w_demo 为单实例窗口,直接用默认实例引用)。 - // ===== uo_myfactory 的自定义事件 ue_progress(ai_done, ai_total) =====
- // 由后台线程 InvokeFactory 触发,运行于主线程,可安全操作界面控件
- // 更新进度文本
- w_demo.st_progress.text = '进度:' + string(ai_done) + '/' + string(ai_total)
- // 全部完成时弹窗提示
- if ai_done = ai_total then
- MessageBox('任务完成', '后台共处理 ' + string(ai_total) + ' 步,期间界面保持响应')
- end if
复制代码
步骤 3:主窗口启动线程
窗口 w_demo 放一个按钮 cb_start(启动)和一个静态文本 st_progress(显示进度)。
前置:w_demo 上已放置 cb_start、st_progress;工厂实例 factory 声明为 w_demo 的实例变量,类型 uo_myfactory。
步骤:把代码贴进 cb_start 的 clicked 事件,运行窗口点按钮,观察界面可拖动、进度逐秒更新、5 秒后弹完成框。 - // ===== cb_start.clicked:把耗时任务丢到后台线程 =====
- // 工厂实例只创建一次(窗口 open 事件创建,或在此判空创建)
- if not IsValid(factory) then
- factory = create uo_myfactory
- end if
- // 示例输入:要后台执行的耗时步数
- long ll_times
- ll_times = 5
- // 启动线程:自动实例化 uo_worker 并调用 of_work(5),返回线程句柄
- long ll_handle
- ll_handle = factory.StartThread("uo_worker", "of_work", ll_times)
- // 输出结果:句柄 + 当前运行中的线程数
- MessageBox('线程已启动', '句柄=' + string(ll_handle) + ',运行中=' + string(factory.GetThreadCount()) + ' 个')
复制代码
步骤 4:查询线程状态
按钮 cb_query 的 clicked 里随时查一下还有几个线程在跑、叫什么名。
- // ===== cb_query.clicked:查看当前运行中的线程列表 =====
- string ls_names[]
- int li_count
- li_count = factory.GetThreadList(ls_names)
- int li_i
- string ls_out
- for li_i = 1 to li_count
- ls_out = ls_out + ls_names[li_i] + '~r~n'
- next
- MessageBox('线程列表', '运行中 ' + string(li_count) + ' 个:' + '~r~n' + ls_out)
复制代码
四、四个坑,写线程前先记住
- uo_thread 的 destructor 里绝对不要写业务代码。 源码注释原话:这是一个坑,PB 的 BUG,析构事件里写任何东西都可能让程序崩溃。destructor 只保留一行 OnDestroy(),其他清理放线程入口函数末尾做。
- ExitThread() 必须调用。 线程入口函数退出前不调它,factory 就会一直傻等这个线程,后续 WaitThreads 收不了尾。
- 后台线程不要直接碰界面控件。 跨线程操作 UI 会崩。要把结果带回界面,就通过 InvokeFactory 回调主线程的 factory 事件,在事件里更新控件。
- 跨线程传参只传标量。 StartThread / SetTask 的可变参数传 long、string 这类标量;PB 不允许跨线程传对象(线程池源码注释同样强调这一点)。
五、小结
一套组合拳:uo_thread_factory.StartThread 起线程 → 线程体继承 uo_thread、在入口函数里干活 → InvokeFactory 回调主线程更新界面 → SetGlobalVar/GetGlobalVar 加锁传值 → ExitThread 收尾;uo_thread.destructor 只写 OnDestroy()。界面卡死这类问题,用上面 4 个步骤的示例改改就能解决。 |