祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 5|回复: 2

[学习笔记] PowerBuilder 共享对象与多线程实战:SharedObjectRegister 的跨会话调用机制与 PostEvent 的异步边界(PB10 基准 · PB12.5 实测)

[复制链接]

[学习笔记] PowerBuilder 共享对象与多线程实战:SharedObjectRegister 的跨会话调用机制与 PostEvent 的异步边界(PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

2523

积分

PBAI

积分
2523
贡献
在线时间
小时
半小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
PowerBuilder 共享对象与多线程实战:SharedObjectRegister 的跨会话调用机制与 PostEvent 的异步边界(PB10 基准 · PB12.5 实测)

阅读说明
1. 适用版本:本文以 PowerBuilder 10 为基准编写与验证;SharedObject 系列函数自 PB 8 起提供,PB 8 / 9 / 10 / 10.5 / 11.x / 12.x 语义一致。PB 12.5 已同步实机复跑,行为与 PB 10 完全一致(见第十三节)
2. 支持数据库:本文示例不涉及数据库,纯运行时并发机制,不需要连接任何 DBMS
3. 操作系统与环境要求:Windows XP 及以上(示例依赖系统自带 kernel32.dllGetCurrentThreadId / GetTickCount 两个函数,无需任何第三方库或 DLL);.sru / .srw 源码须 GBK 编码 + CRLF 换行 + 无 BOM
4. 难度系数:★★★☆☆(需要先了解用户对象、实例变量与 PostEvent / TriggerEvent 的基本用法)
5. 其它阅读说明:本文所有结论都来自双版本实机探针(PB 10 与 PB 12.5 各 3 轮,全部断言通过),文中出现的线程号、毫秒数、文件长度都是真机输出原值,不是推断值;文末附可直接导入运行的 PB10 兼容版 PBL 源码包(含两个用户对象与自检脚本)




一、先把"多线程"这件事说清楚:PB 里只有三条路

PowerBuilder 是单线程语言——没有任何语法可以让你"起一个线程"。但这不代表它做不了并发。可用的路只有三条:

路线机制是否真并行典型场景局限
① 协作式假并发窗口 Timer 事件 + Yield()进度条刷新、分片处理大量数据仍然只有一个线程;Yield() 要靠消息队列,无窗口/服务端环境常常无效
共享对象SharedObjectRegister 系列(独立会话 + 独立线程)后台长任务、批量计算、不冻结界面调用与取值都是"跨会话",有一套自己的坑(本文重点)
③ 外部线程自己写 C++ DLL 起线程,或第三方封装(如 PBIDEA 的 uo_thread需要与 PB 侧深度交互的重并发需要额外的二进制与许可;PB 侧只有一个回调入口,交互模型要自己设计


路 ② 是 PowerBuilder 语言层唯一的官方并发手段,一共只有四个函数,本文把它们一次讲透。很多人(包括不少老 PB 程序员)对它的印象是"共享对象 = 后台线程,调用它是异步的",这个印象是错的——本文的核心就是拿实测数据把这个边界钉死。




二、四个 API 全解

函数作用返回值
SharedObjectRegister( classname, instancename )一个新的 PB 会话(新线程)里创建 classname 的一个实例,并把它登记为 instancenameerrorreturn 枚举
SharedObjectGet( instancename, objectinstance )按名字取出共享实例的引用,赋给一个同类型的变量errorreturn 枚举
SharedObjectUnregister( instancename )注销名字(释放这个名字,实例本身不一定立刻销毁)errorreturn 枚举
SharedObjectDirectory( instancenames[], classnames[] )枚举当前进程内所有已登记的共享实例(两个数组按位置对应)errorreturn 枚举


六个返回枚举值(PB 帮助里有,但要理解各自的语义):

枚举值含义
Success!成功
SharedObjectExistsError!这个名字已经被用过了
SharedObjectCreateInstanceError!会话建起来了,但实例没造出来(比如类无法实例化)
SharedObjectCreatePBSessionError!新的 PB 会话创建失败
SharedObjectNotExistsError!这个名字不存在
FeatureNotSupportedError!当前环境不支持该特性


两个必须记住的语法要点


  • 接受引用的变量必须与被注册的类同类型,否则编译期就报 C0008(类型不兼容);
  • 函数返回的是 errorreturn 枚举,不要用 boolean 去接——要判断成功就写 ler = Success!,想把它变成文字就 String(ler),实测会返回枚举字面名(如 "SharedObjectCreatePBSessionError!"),调试时很好用。





三、返回枚举实测:把错误码一次钉死

空口说"返回失败"没有意义,得知道每一种错法对应哪一个枚举。实测结果(PB 10 与 PB 12.5 完全一致):

测试动作实测返回说明
同名实例注册第二次SharedObjectExistsError!名字是进程内全局的,重名就报这个
SharedObjectGet 一个不存在的名字SharedObjectNotExistsError!并且传入的引用保持 invalidIsValid = false)
SharedObjectUnregister 一个不存在的名字SharedObjectNotExistsError!不会崩,只是告诉你没有
SharedObjectRegister('nvo_no_such_class', 'sw9') 类名写错SharedObjectCreatePBSessionError!反直觉:报的是"会话创建失败",而不是"实例创建失败"
上一步失败后再 SharedObjectGet('sw9', ...)SharedObjectNotExistsError!失败不会留下半个登记项

注意最后一行那个"反直觉":类名写错时,PB 报的是 SharedObjectCreatePBSessionError!SharedObjectCreateInstanceError! 在整个探针里一次都没被触发。所以排错时不要盯着 CreateInstanceError 找——看到 CreatePBSessionError 就要先检查类名拼写、类是否在当前 PBL 库里、以及被依赖的对象是否已编译

实测输出(原文摘录):
  1. reg5=true dup=true badclass_session=true badclass_inst=false getmiss=true refvalid=false dir_cnt=1 item1=sw5/nvo_so_worker unreg=true
复制代码




四、完整示例:从注册到注销

4.1 操作步骤(照这个顺序做就能跑)


  • 新建一个 PBL(或用一个空的目标 PBL),在里面新建一个定制类用户对象(Custom Class),命名 nvo_so_worker,祖先保持 nonvisualobject
  • 把下面的 nvo_so_worker 源码整段粘进去(它就是"要在后台干的活");
  • 再新建一个定制类用户对象 nvo_so_article,祖先同样 nonvisualobject,粘入第二段源码(它是"调用方封装");
  • 在窗口上放一个按钮,clicked 事件里粘入第三段(不到十行);
  • 运行,点按钮,看弹出来的结果。


4.2 后台工作者:nvo_so_worker
  1. $PBExportHeader$nvo_so_worker.sru
  2. forward
  3. global type nvo_so_worker from nonvisualobject
  4. end type
  5. end forward
  6. global type nvo_so_worker from nonvisualobject
  7. event ue_work ( )
  8. end type
  9. global nvo_so_worker nvo_so_worker
  10. type variables
  11. long il_progress
  12. boolean ib_done
  13. long il_arg
  14. ulong il_t_start
  15. ulong il_t_end
  16. long il_ctor_thread
  17. end variables
  18. forward prototypes
  19. public function long of_ping ()
  20. public function long of_work (long al_total)
  21. public subroutine of_start (long al_total)
  22. public function long of_ctorthread ()
  23. public subroutine of_log (string as_line)
  24. end prototypes
  25. type prototypes
  26. public:
  27. function ulong GetCurrentThreadId() library "kernel32.dll"
  28. function ulong GetTickCount() library "kernel32.dll"
  29. end prototypes
  30. on nvo_so_worker.create
  31. call super::create
  32. TriggerEvent( this, "constructor" )
  33. end on
  34. on nvo_so_worker.destroy
  35. TriggerEvent( this, "destructor" )
  36. call super::destroy
  37. end on
  38. event constructor;
  39. //记下「这个实例是在哪个线程上构造出来的」,用来和主线程对照
  40. il_ctor_thread = GetCurrentThreadId()
  41. end event
  42. event destructor;
  43. //销毁时把当前线程号追加进 so_dtor.txt,观察销毁发生在哪个线程
  44. long ll_f
  45. ll_f = FileOpen('so_dtor.txt', LineMode!, Write!, LockWrite!, Append!)
  46. if ll_f > 0 then
  47.     FileWrite(ll_f, 'DTOR thread=' + String(GetCurrentThreadId()))
  48.     FileClose(ll_f)
  49. end if
  50. end event
  51. event ue_work;
  52. //PostEvent / TriggerEvent 调用的入口:参数走实例变量,过程把进度写进 il_progress
  53. //每推进到 1/4 处,就往 so_prog.txt 追加一行带 GetTickCount 时间戳的记录
  54. long ll_i
  55. long ll_step
  56. il_progress = 0
  57. ib_done = false
  58. il_t_start = GetTickCount()
  59. of_log('W_T0=' + String(il_t_start))
  60. ll_step = il_arg / 4
  61. if ll_step < 1 then ll_step = 1
  62. for ll_i = 1 to il_arg
  63.     il_progress = ll_i
  64.     if Mod(ll_i, ll_step) = 0 then
  65.         of_log('W_PROG=' + String(ll_i) + '@' + String(GetTickCount()))
  66.     end if
  67. next
  68. il_t_end = GetTickCount()
  69. ib_done = true
  70. of_log('W_T1=' + String(il_t_end))
  71. end event
  72. public subroutine of_log (string as_line);
  73. //把一行带时间戳的进度追加到 so_prog.txt
  74. long ll_f
  75. ll_f = FileOpen('so_prog.txt', LineMode!, Write!, LockWrite!, Append!)
  76. if ll_f > 0 then
  77.     FileWrite(ll_f, as_line)
  78.     FileClose(ll_f)
  79. end if
  80. end subroutine
  81. public function long of_ping ();
  82. //返回当前线程号:调用它就能知道「这次调用是在哪个线程上跑的」
  83. return GetCurrentThreadId()
  84. end function
  85. public function long of_ctorthread ();
  86. //返回构造时的线程号
  87. return il_ctor_thread
  88. end function
  89. public function long of_work (long al_total);
  90. //模拟耗时任务:纯计算忙循环(有返回值,同步返回)
  91. long ll_i
  92. il_progress = 0
  93. ib_done = false
  94. il_t_start = GetTickCount()
  95. for ll_i = 1 to al_total
  96.     il_progress = ll_i
  97. next
  98. il_t_end = GetTickCount()
  99. ib_done = true
  100. return il_progress
  101. end function
  102. public subroutine of_start (long al_total);
  103. //与 of_work 逻辑完全相同,唯一区别是【没有返回值】
  104. long ll_i
  105. il_progress = 0
  106. ib_done = false
  107. il_t_start = GetTickCount()
  108. for ll_i = 1 to al_total
  109.     il_progress = ll_i
  110. next
  111. il_t_end = GetTickCount()
  112. ib_done = true
  113. end subroutine
复制代码

of_workof_start 的存在意义:这两个方法逻辑一模一样,唯一差别是"有没有返回值"。第十节会拿它们对照证明——"没有返回值"并不能让它变成异步。这是社区里最容易想当然的一个点。

4.3 调用方封装:nvo_so_article
  1. $PBExportHeader$nvo_so_article.sru
  2. forward
  3. global type nvo_so_article from nonvisualobject
  4. end type
  5. end forward
  6. global type nvo_so_article from nonvisualobject
  7. end type
  8. global nvo_so_article nvo_so_article
  9. type variables
  10. nvo_so_worker iuo_w
  11. end variables
  12. forward prototypes
  13. public function string of_demo_register ()
  14. public function string of_demo_sync_call (long al_total)
  15. public function string of_demo_postevent (long al_total)
  16. public function string of_demo_lifecycle ()
  17. public function string of_demo_errcode ()
  18. public function string of_read_progress_log ()
  19. public function string of_run_pick (int ai_pick, long al_total)
  20. end prototypes
  21. type prototypes
  22. public:
  23. function ulong GetCurrentThreadId() library "kernel32.dll"
  24. function ulong GetTickCount() library "kernel32.dll"
  25. end prototypes
  26. public function string of_demo_register ();
  27. //演示一:注册 / 取引用 / 线程身份对照
  28. string ls_out
  29. ulong lul_main
  30. ulong lul_so
  31. errorreturn ler
  32. lul_main = GetCurrentThreadId()
  33. ler = SharedObjectRegister('nvo_so_worker', 'sw1')
  34. ls_out = 'reg=' + String(ler = Success!)
  35. ler = SharedObjectGet('sw1', iuo_w)
  36. ls_out = ls_out + ' get=' + String(ler = Success!) + ' refok=' + String(IsValid(iuo_w))
  37. lul_so = iuo_w.of_ping()
  38. ls_out = ls_out + ' main_thread=' + String(lul_main) + ' so_thread=' + String(lul_so)
  39. ls_out = ls_out + ' ctor_eq_ping=' + String(iuo_w.of_ctorthread() = iuo_w.of_ping())
  40. ls_out = ls_out + ' thread_differs=' + String(lul_so <> lul_main)
  41. ler = SharedObjectUnregister('sw1')
  42. ls_out = ls_out + ' unreg=' + String(ler = Success!)
  43. return ls_out
  44. end function
  45. public function string of_demo_sync_call (long al_total);
  46. //演示二:有返回值的成员函数 / 无返回值的子程序调用,量一下调用耗时
  47. string ls_out
  48. long ll_p
  49. ulong lul_c0
  50. errorreturn ler
  51. ler = SharedObjectRegister('nvo_so_worker', 'sw2')
  52. if ler <> Success! then
  53.     return 'REG_FAIL=' + String(ler)
  54. end if
  55. ler = SharedObjectGet('sw2', iuo_w)
  56. if ler <> Success! then
  57.     return 'GET_FAIL=' + String(ler)
  58. end if
  59. iuo_w.il_progress = 0
  60. iuo_w.ib_done = false
  61. lul_c0 = GetTickCount()
  62. ll_p = iuo_w.of_work(al_total)
  63. ls_out = 'work_elapsed_ms=' + String(GetTickCount() - lul_c0)
  64. ls_out = ls_out + ' work_ret=' + String(ll_p)
  65. ls_out = ls_out + ' progress_after_ret=' + String(iuo_w.il_progress)
  66. ls_out = ls_out + ' done_after_ret=' + String(iuo_w.ib_done)
  67. iuo_w.il_progress = 0
  68. iuo_w.ib_done = false
  69. lul_c0 = GetTickCount()
  70. iuo_w.of_start(al_total)
  71. ls_out = ls_out + ' start_elapsed_ms=' + String(GetTickCount() - lul_c0)
  72. ls_out = ls_out + ' start_progress_after_ret=' + String(iuo_w.il_progress)
  73. ls_out = ls_out + ' start_done_after_ret=' + String(iuo_w.ib_done)
  74. ler = SharedObjectUnregister('sw2')
  75. ls_out = ls_out + ' unreg=' + String(ler = Success!)
  76. return ls_out
  77. end function
  78. public function string of_demo_postevent (long al_total);
  79. //演示三:PostEvent 派发 + 主线程先干自己的活 + 之后再读进度
  80. string ls_out
  81. long ll_p
  82. long ll_x
  83. long ll_len
  84. ulong lul_t0
  85. ulong lul_r0
  86. int li_f
  87. errorreturn ler
  88. ler = SharedObjectRegister('nvo_so_worker', 'sw3')
  89. if ler <> Success! then
  90.     return 'REG_FAIL=' + String(ler)
  91. end if
  92. ler = SharedObjectGet('sw3', iuo_w)
  93. if ler <> Success! then
  94.     return 'GET_FAIL=' + String(ler)
  95. end if
  96. //先把进度日志清空,确保文件里只有本轮的内容
  97. li_f = FileOpen('so_prog.txt', LineMode!, Write!, LockWrite!, Replace!)
  98. FileWrite(li_f, 'BEGIN')
  99. FileClose(li_f)
  100. iuo_w.il_progress = 0
  101. iuo_w.ib_done = false
  102. iuo_w.il_arg = al_total
  103. lul_t0 = GetTickCount()
  104. iuo_w.PostEvent('ue_work')
  105. ls_out = 'post_ret_ms=' + String(GetTickCount() - lul_t0)
  106. //主线程做 200ms 纯计算忙等,期间一次都不访问共享对象
  107. ll_x = 0
  108. lul_t0 = GetTickCount()
  109. do while GetTickCount() - lul_t0 < 200
  110.     ll_x = ll_x + 1
  111. loop
  112. ls_out = ls_out + ' main_busy_ms=200 main_busy_iter=' + String(ll_x)
  113. //忙等刚结束时先看进度日志文件:主线程忙的这 200ms 里,工作线程写了多少
  114. ll_len = FileLength('so_prog.txt')
  115. ls_out = ls_out + ' prog_len_during_busy=' + String(ll_len)
  116. if ll_len > 7 then
  117.     ls_out = ls_out + ' worker_ran_during_busy=true'
  118. else
  119.     ls_out = ls_out + ' worker_ran_during_busy=false'
  120. end if
  121. //忙等结束后才去读实例变量:这是一次跨会话读取
  122. lul_r0 = GetTickCount()
  123. ll_p = iuo_w.il_progress
  124. ls_out = ls_out + ' progress=' + String(ll_p)
  125. ls_out = ls_out + ' read_block_ms=' + String(GetTickCount() - lul_r0)
  126. ls_out = ls_out + ' done=' + String(iuo_w.ib_done)
  127. ler = SharedObjectUnregister('sw3')
  128. ls_out = ls_out + ' unreg=' + String(ler = Success!)
  129. //任务结束后,用「文件通道」把整份进度日志读回来:这条通道不经过共享对象,不会阻塞
  130. ll_len = Len(of_read_progress_log())
  131. if ll_len > 40 then
  132.     ls_out = ls_out + ' file_channel_ok=true'
  133. else
  134.     ls_out = ls_out + ' file_channel_ok=false'
  135. end if
  136. return ls_out
  137. end function
  138. public function string of_demo_lifecycle ();
  139. //演示四:注销之后,旧引用还能不能用、同名能不能立刻重注册
  140. string ls_out
  141. long ll_old
  142. errorreturn ler
  143. ler = SharedObjectRegister('nvo_so_worker', 'sw4')
  144. ls_out = 'reg1=' + String(ler = Success!)
  145. ler = SharedObjectGet('sw4', iuo_w)
  146. ls_out = ls_out + ' get=' + String(ler = Success!)
  147. ler = SharedObjectUnregister('sw4')
  148. ls_out = ls_out + ' unreg=' + String(ler = Success!)
  149. //注销之后,手里这份旧引用还在
  150. ll_old = iuo_w.of_ping()
  151. ls_out = ls_out + ' oldref_thread=' + String(ll_old) + ' oldref_ok=' + String(ll_old > 0)
  152. //同名实例立刻重注册
  153. ler = SharedObjectRegister('nvo_so_worker', 'sw4')
  154. ls_out = ls_out + ' rereg=' + String(ler = Success!)
  155. ler = SharedObjectUnregister('sw4')
  156. ls_out = ls_out + ' final_unreg=' + String(ler = Success!)
  157. return ls_out
  158. end function
  159. public function string of_demo_errcode ();
  160. //演示五:四个 API 的返回枚举实测
  161. string ls_out
  162. string lsi[]
  163. string lsc[]
  164. nvo_so_worker luo_t
  165. errorreturn ler
  166. ler = SharedObjectRegister('nvo_so_worker', 'sw5')
  167. ls_out = 'reg5=' + String(ler = Success!)
  168. ler = SharedObjectRegister('nvo_so_worker', 'sw5')
  169. ls_out = ls_out + ' dup=' + String(ler = SharedObjectExistsError!)
  170. ler = SharedObjectRegister('nvo_no_such_class', 'sw9')
  171. ls_out = ls_out + ' badclass_session=' + String(ler = SharedObjectCreatePBSessionError!)
  172. ls_out = ls_out + ' badclass_inst=' + String(ler = SharedObjectCreateInstanceError!)
  173. ler = SharedObjectGet('no_such_instance', luo_t)
  174. ls_out = ls_out + ' getmiss=' + String(ler = SharedObjectNotExistsError!) + ' refvalid=' + String(IsValid(luo_t))
  175. ler = SharedObjectDirectory(lsi, lsc)
  176. ls_out = ls_out + ' dir_cnt=' + String(UpperBound(lsi))
  177. if UpperBound(lsi) > 0 then
  178.     ls_out = ls_out + ' item1=' + lsi[1] + '/' + lsc[1]
  179. end if
  180. ler = SharedObjectUnregister('sw5')
  181. ls_out = ls_out + ' unreg=' + String(ler = Success!)
  182. return ls_out
  183. end function
  184. public function string of_read_progress_log ();
  185. //把进度日志整份读出来(主线程侧的「文件通道」:不经过共享对象,因此不会阻塞)
  186. //注意:别用 FileRead 逐行读中文文本;本日志是纯 ASCII,这里可以放心逐行读
  187. string ls_all
  188. string ls_line
  189. int li_f
  190. ls_all = ''
  191. li_f = FileOpen('so_prog.txt', LineMode!, Read!, LockRead!)
  192. if li_f > 0 then
  193.     do while FileRead(li_f, ls_line) <> -100
  194.         ls_all = ls_all + ls_line + '~r~n'
  195.     loop
  196.     FileClose(li_f)
  197. end if
  198. return ls_all
  199. end function
  200. public function string of_run_pick (int ai_pick, long al_total);
  201. //按编号分发到对应演示,方便窗口按钮一行调用
  202. string ls_r
  203. choose case ai_pick
  204.     case 1
  205.         ls_r = of_demo_register()
  206.     case 2
  207.         ls_r = of_demo_sync_call(al_total)
  208.     case 3
  209.         ls_r = of_demo_postevent(al_total)
  210.     case 4
  211.         ls_r = of_demo_lifecycle()
  212.     case 5
  213.         ls_r = of_demo_errcode()
  214.     case else
  215.         ls_r = '未选择'
  216. end choose
  217. return ls_r
  218. end function
复制代码

4.4 从窗口按钮调用它(输入值写在最前面)
  1. // 事件:cb_so_run :: clicked
  2. // 输入:模拟任务的总步数(改大改小都能跑,越大耗时越明显)
  3. long ll_total
  4. ll_total = 2000000
  5. // 输入:要演示哪一项(1 注册 / 2 同步与异步边界 / 3 PostEvent 与文件通道 / 4 生命周期 / 5 错误码)
  6. int li_pick
  7. li_pick = 2
  8. // 输入:共享对象宿主(本文的调用方封装对象)
  9. nvo_so_article lart
  10. string ls_r
  11. lart = create nvo_so_article
  12. ls_r = lart.of_run_pick(li_pick, ll_total)
  13. destroy lart
  14. MessageBox('共享对象演示结果', ls_r)
复制代码

4.5 实机输出(li_pick = 2 时的原文)
  1. work_elapsed_ms=531 work_ret=2000000 progress_after_ret=2000000 done_after_ret=true start_elapsed_ms=532 start_progress_after_ret=2000000 start_done_after_ret=true unreg=true
复制代码

(上面是 PB 12.5 的值;PB 10 为 work_elapsed_ms=484 start_elapsed_ms=484,其余完全一致。)




五、第一件必须纠正的事:它跑在"另一个会话"里,不是"另一个线程回调你"

5.1 会话身份:线程号完全不同

li_pick = 1 的实测输出:
  1. reg=true get=true refok=true main_thread=12136 so_thread=6900 ctor_eq_ping=true thread_differs=true unreg=true
复制代码

(PB 10:main_thread=12328 so_thread=12260

观测点PB 12.5PB 10结论
主会话线程号1213612328主线程身份
共享对象里 of_ping() 返回的线程号690012260与主线程不同
of_ctorthread()(构造时线程)与 of_ping() 是否相同truetrue同一个实例始终在同一个线程上跑
两个不同实例(sw1 / sw2)线程号是否不同truetrue每个共享实例各自有独立线程


所以:SharedObjectRegister 每成功一次,PB 就为你新开一个"会话(session)",这个会话有自己的线程、有自己的 PB 运行时上下文。

5.2 ★★ 但成员函数调用是完全同步

这是整篇文章最关键的一张表。用 of_work(有返回值)和 of_start(无返回值)分别调用同一个 200 万次的忙循环:

调用方式PB 12.5 调用耗时PB 10 调用耗时返回瞬间的进度返回瞬间的完成标志
of_work(有返回值函数)531 ms484 ms2000000(已跑完)true
of_start(无返回值子程序)532 ms484 ms2000000(已跑完)true


读法:如果它是异步的,调用应该"瞬间返回"(0~几毫秒),而实测花了半秒多;而且返回时任务已经全部跑完

结论:跨会话的成员函数/子程序调用与调用本地对象的方法在时序上没有任何区别,它就是同步阻塞的。"没有返回值"不会让它变异步。

用另一组探针(同一工程里的第二个探针对象,专门压时序)交叉验证,结论一致:

调用入口PB 12.5PB 10
of_work(...) 有返回值516 ms484 ms
of_start(...) 无返回值516 ms484 ms
TriggerEvent('ue_work')750 ms703 ms
PostEvent('ue_work')0 ms0 ms


TriggerEvent 之所以比前两个长一点,是因为 ue_work 里多写了 5 次带时间戳的文件追加,属于任务本身变重,不是机制差异。

5.3 PostEvent 是唯一"立刻返回"的入口——但它不等于"主线程自由了"

PostEvent 实测 0 ms 返回(连 1 个毫秒都没花),这是四个入口里唯一真正"派发完就还给你"的。但请注意下面第六节的证据:主线程真正能"自由"到什么程度,取决于你之后碰不碰这个共享对象。




六、并发硬证据:两条时间线

看数字不如看时间线。li_pick = 3 的实测输出(PB 12.5):
  1. post_ret_ms=0 main_busy_ms=200 main_busy_iter=90420 prog_len_during_busy=24 worker_ran_during_busy=true progress=2000000 read_block_ms=609 done=true unreg=true file_channel_ok=true
复制代码

(PB 10:main_busy_iter=102603 prog_len_during_busy=24 read_block_ms=547,其余一致。)

把它拆成三条独立证据:

证据 ①:主线程没有被挂起。
PostEvent 返回后,主线程跑了 200 ms 的纯计算忙等,一共迭代了 90420 次(PB 10 是 102603 次)。如果主线程被这次派发挂起了,它根本跑不完这 200 ms。主线程是正常运行的。

证据 ②:同一时间窗内,工作线程也在干活。
忙等开始时 so_prog.txt 里只有一行 BEGIN(7 字节);忙等结束时文件已是 24 字节——多出来的正是 W_T0=2717021375 那一行。这行是工作线程写的。也就是说:主线程忙着算自己的 200 ms 时,工作线程已经把它的启动时间戳写进了文件。

证据 ③:两者在抢 CPU,所以任务被拖长了。
同一个 200 万次忙循环,独占(同步调用)时耗时 531 ms;这次与主线程并行时,从工作线程自己的时间戳看,W_T0=2717021375W_T1=2717022187耗时 812 ms。多出来的 280 ms 就是两个线程争抢同一颗 CPU 的代价。

证据 ④(用另一组探针把时间窗卡到毫秒):
那组探针里,主线程忙等结束时的时间戳是 2716283937(PB 12.5),而工作线程的进度记录正是 W_PROG=500000@2716283937——主线程忙等结束的那一刻,恰好就是工作线程推到 50 万步的那一刻(PB 10 对应 2716293234,落在 W_T0=2716292921W_T1=2716293734 之间)。两条时间线在同一个数轴上交替出现,并发成立。




七、★★★ 真正的"坑"在取值:跨会话读属性是阻塞

上面 li_pick = 3 的输出里,最值得盯的是这两个数字:
  1. progress=2000000    read_block_ms=609
复制代码

主线程在忙等 200 ms 之后才去读 iuo_w.il_progress这一次读取花了 609 ms(PB 10 是 547 ms)。而且读回来的不是中途进度,而是最终值 2000000

为什么?

因为 il_progress另一个会话里的实例变量。主线程读它,要走跨会话的通道;而这个通道会等到那个对象"空闲"(当前调用结束)才把值交回来。所以:
结论:跨会话读实例变量 = 一次隐式的"等它干完"(阻塞)。 用读属性的方式做"进度轮询",等于每次轮询都把主线程挂起到任务结束——轮询毫无意义,界面照样卡死。

对照数据:任务跑完之后再去读同一个属性,耗时 0 ms(两组探针都是 0)。也就是说阻塞只发生在对方忙碌时,对方空闲时读取是即时的。

7.1 那"边跑边看进度"到底怎么做?

既然不能读属性,就把结果通道换成不经过 PB 会话的东西:

通道可行性说明
直接读共享对象实例变量本次实测确认会阻塞到任务结束,拿不到中途值
写/读文件两个会话都能访问文件系统。本次探针就是靠 so_prog.txt 追加时间戳行,拿到了完整的中途进度
写/读数据库表同理,写一张进度表,主线程轮询表
主线程 PostEvent 到窗口,由工作线程反向通知⚠️需要共享对象持有窗口引用,且窗口侧的 UI 操作也要走消息队列,注意可重入性


下面是本次实机验证过的"文件通道"写法。工作线程侧就是 4.2 里的 of_log(往 so_prog.txt 追加带时间戳的行),主线程侧用一个独立的读取方法(4.3 里的 of_read_progress_log):
  1. public function string of_read_progress_log ();
  2. //把进度日志整份读出来(主线程侧的「文件通道」:不经过共享对象,因此不会阻塞)
  3. //注意:别用 FileRead 逐行读中文文本;本日志是纯 ASCII,这里可以放心逐行读
  4. string ls_all
  5. string ls_line
  6. int li_f
  7. ls_all = ''
  8. li_f = FileOpen('so_prog.txt', LineMode!, Read!, LockRead!)
  9. if li_f > 0 then
  10.     do while FileRead(li_f, ls_line) <> -100
  11.         ls_all = ls_all + ls_line + '~r~n'
  12.     loop
  13.     FileClose(li_f)
  14. end if
  15. return ls_all
  16. end function
复制代码

在窗口上这样用(Timer 事件或按钮里都行):
  1. // 事件:cb_show_progress :: clicked
  2. // 输入:共享对象宿主
  3. nvo_so_article lart
  4. string ls_all
  5. lart = create nvo_so_article
  6. // 直接把整份进度日志读出来显示;这条通道不经过共享对象,主线程不会被挂起
  7. ls_all = lart.of_read_progress_log()
  8. destroy lart
  9. if Len(ls_all) = 0 then
  10.     MessageBox('后台任务进度', '暂无进度记录')
  11. else
  12.     MessageBox('后台任务进度', ls_all)
  13. end if
复制代码

实机读回的原文(PB 12.5 那一轮):
  1. BEGIN
  2. W_T0=2717021375
  3. W_PROG=500000@2717021640
  4. W_PROG=1000000@2717021828
  5. W_PROG=1500000@2717022000
  6. W_PROG=2000000@2717022187
  7. W_T1=2717022187
复制代码
为什么这里读文件不会卡主线程:文件读写是操作系统层面的调用,不经过 PB 的跨会话通道,工作线程写、主线程读互不阻塞(文件本身可能有共享锁的细节,用 LockWrite! / LockRead!Append! 都能配合)。这也是本次探针能在主线程忙等期间、从文件里看到工作线程新写内容的原因。




八、生命周期:注销、旧引用、重新注册、析构

8.1 注销之后,手里那份引用还能用

li_pick = 4 的实测输出:
  1. reg1=true get=true unreg=true oldref_thread=17080 oldref_ok=true rereg=true final_unreg=true
复制代码

(PB 10:oldref_thread=24744,其余一致。)

SharedObjectUnregister('sw4') 已经返回 Success!,但随后 iuo_w.of_ping() 照样调用成功并返回了线程号 17080。

原因:注销释放的是"名字"这个登记项,实例本身由引用计数保活——你手里还握着 iuo_w 这个引用,PB 就不会销毁它。

实践含义:别指望"注销一下就把后台对象清干净"。要真正释放,得先把所有引用置空(或放到作用域外),再让会话结束。

8.2 注销后同名可以立刻重新注册

实测(PB 12.5 间隔 16 ms、PB 10 间隔 0 ms):

动作PB 12.5PB 10
注销 sw4立刻重新注册同名Success!Success!
注销后等 300 ms 再注册同名Success!Success!
换一个全新的名字注册(对照组)Success!Success!
注销后 SharedObjectGet 该名字SharedObjectNotExistsError!


三组写法全部成功,说明名字在注销那一刻就立刻释放,不存在"要等一会儿才能复用"的问题。(这一条最初跑出过一次不一致的结果,后来专门加了一组探针连续复跑 6 轮才钉死,属于测量时序陷阱,不是产品行为。)

8.3 析构发生在共享会话自己的线程

nvo_so_worker.destructor 会把销毁时的线程号追加到 so_dtor.txt。清空该文件后跑一轮完整演示(示例一~示例五 + 一次编号分发,共注册 8 个共享实例):

版本主会话线程号共享实例线程号8 条析构记录的线程号有主线程号吗
PB 12.51213669006900 / 4308 / 5856 / 12308 / 20492 / 17080 / 22116 / 15616没有
PB 10123281226012260 / 8688 / 23860 / 24980 / 13096 / 24744 / 9752 / 23608没有


8 条析构记录全部发生在共享会话的线程上,主线程一次都没参与。 第一条恰好等于第一个共享实例的线程号(6900 / 12260),可以对应上。

→ 实践含义:不要把"销毁时的清理逻辑"写在主线程能感知的地方。共享对象的 destructor 里不要直接去操作主会话的窗口/数据窗口——那是在另一个会话的线程上跑的,跨会话碰 UI 对象是踩雷动作;要通知主会话就用 PostEvent 或文件/数据库这类外部通道。

8.4 目录枚举 SharedObjectDirectory

实测(示例五):
  1. dir_cnt=1 item1=sw5/nvo_so_worker
复制代码

两个数组按位置对应:instancenames 是实例名,classnames 是类名。多实例时的实测输出形如 sw2/nvo_so_workersw1/nvo_so_worker(注意顺序不保证是你注册的顺序,别依赖它)。

注意SharedObjectDirectory 枚举的是当前进程内的登记项。上一个失败案例里,注册 nvo_no_such_class 失败后并不留下条目(dir_cnt 仍为 1),所以可以用它来"数一数当前到底挂了几个共享实例"。




九、shared variables 是"会话级",不是"进程级"

变量作用域那篇文章里讲过共享变量(shared variables)。那这个"共享"能不能跨会话共享?不能,实测:

在工作者对象里加一个 shared variables long sl_hits,每次调用 of_ping()sl_hits = sl_hits + 1

观察点PB 12.5PB 10结论
共享会话内的 sl_hits(连续调用后)3 → 4 → 5在共享会话内正常累加
主会话里 create 出来的实例 A 的 sl_hits0 → 1 → 2与共享会话的值互不影响
主会话里 create 出来的实例 B 的 sl_hits0 → 1 → 2与 A 也互不影响(各自一份)


结论:shared variables 只在"同一个 PB 会话"内共享。 跨会话是隔离的——这也是共享对象能在另一个线程上安全跑的前提(否则两个会话同时读写同一份变量,必然出问题)。




十、共享会话有完整的 PB 运行能力吗

有。实测在共享会话里 create datastore

探针PB 12.5PB 10
共享会话内 create datastoreIsValidds_okds_ok


共享会话里可以正常创建 DataStore、执行嵌入式 SQL、读写文件——它就是一套完整的 PB 运行时,只是跑在另一个线程上。这意味着"后台批量取数/加工"这条路是走得通的,只要遵守第七节的"结果通道"规则。




十一、坑清单(都是这次实测踩到的)

#实测结论与对策
1以为共享对象调用是异步的同步阻塞。有返回值、无返回值、TriggerEvent 三种入口统一同步(531/531/750 ms)。要"不阻塞"只有 PostEvent,且只在派发那一刻不阻塞
2用"读实例变量"做进度轮询跨会话读会阻塞到任务结束(实测 609 / 547 ms),读到的是终值。改用文件/数据库通道
3类名写错,盯着 CreateInstanceError 排查实际报的是 SharedObjectCreatePBSessionError!CreateInstanceError 本次一次都没触发
4类名写错后没处理返回值后续 SharedObjectGet 会返回 SharedObjectNotExistsError!,引用保持 invalid(IsValid=false),不做判空直接调方法就会 Null object reference
5注销后以为对象没了,立刻重新注册并"重新 create"旧引用仍然可用(引用计数保活)。注销只释放名字,不释放实例
6在共享对象的 destructor 里操作主会话的窗口对象析构发生在共享会话的线程上(实测 8/8 全非主线程)。跨会话碰 UI 是雷区,改走 PostEvent 或外部通道
7两个不同实例以为共用一条线程每个实例各有自己的线程sw1sw2 线程号实测不同),注册越多线程越多,别无限注册
8以为 shared variables 能跨会话传数据不能,它是会话级的。跨会话传数据要走实例变量(但读会阻塞,见第 2 条)或外部通道
9booleanSharedObjectRegister 返回值返回值是 errorreturn 枚举,boolean 接会类型不兼容。判断成功写 ler = Success!
10排错时想看枚举文字String(ler) 有效,实测返回 "SharedObjectCreatePBSessionError!" 这类字面名,直接进日志很好用
11在 headless / 无窗口环境靠 Yield() 让出执行权实测 Yield() 返回 false(没有消息队列时让不出任何东西),协作式并发在这种环境下等于没有
12以为内建 Sleep 是毫秒内建 Sleep(1) 实测睡了 1000 ms,单位是"秒"。要毫秒级睡眠必须自己挂 kernel32.dll 的函数





十二、和替代方案的选型对比

方案真并行主线程会卡吗结果回传方式适用
分片 + Timer 事件不卡(但总时长不变)直接在事件里更新界面处理量可控、需要进度条刷新
Yield() 协作让出不卡,但 headless 下无效(实测返回 false)同线程,直接赋值有消息循环的桌面应用
SharedObjectRegisterPostEvent 派发时不卡;一旦读它的属性就卡文件 / 数据库 / PostEvent 反向通知桌面端后台长任务、批量计算、需要真并行
自写 C++ 线程 DLL取决于回调设计回调函数(PB 侧单入口)高频、低延迟、需要原生能力
PBIDEA uo_thread 等第三方封装取决于封装取决于封装已在使用 PBIDEA 生态的项目(本文不展开,见 PBIDEA 系列目录)


一句话选型口诀

  • 只要"界面别冻住、跑完给我个结果" → 共享对象 + PostEvent 派发 + 文件/数据库通道回结果;
  • 要中途实时进度条 → 还是共享对象,但进度必须走文件/数据库,别读属性;
  • 只是想让界面刷新一下TimerYield(),不必上共享对象;
  • 要每秒几千次的小回调 → 共享对象不合适(跨会话开销摆在那儿),考虑原生 DLL。





十三、PB12.5 差异

PB 12.5 与 PB 10 行为一致。

判据不是"跑通了就算",而是逐行比对两版诊断输出:把两版完整运行的诊断文本与进度日志去掉所有数字后逐行 diff,差异行数 = 0(示例工程 6 行诊断 + 7 行进度日志;深挖用的时序探针工程 134 行诊断,同样 0 差异)。也就是说线程关系、返回枚举、调用时序、阻塞行为、生命周期规则、shared variables 隔离范围、共享会话能力,两个版本完全同构。读数上的差异只有绝对耗时的量级(PB 12.5 的循环略慢一些,例如同一忙循环 531 ms vs 484 ms),属于运行时性能差异,不是语义差异。




十四、实机验证情况

本次文章的示例代码(nvo_so_worker + nvo_so_article 两个对象)与用于深挖时序的探针对象,均在 PB 10 与 PB 12.5 双版本完成下列验证:

验证项PB 12.5PB 10
pbl import 导入两个 .sru(GBK / CRLF / 无 BOM)0 错误 ×20 错误 ×2
build rebuild --type full 全量编译0 错误0 错误
示例代码运行断言(.pbtest,28 条硬断言)PASS 28/28(连跑 3 轮全 PASS)PASS 28/28(连跑 3 轮全 PASS)
时序探针运行断言(31 条硬断言)PASS 31/31(连跑 3 轮)PASS 31/31(连跑 3 轮)
源码静态检查(块结构 / 声明与实现配对 / 内置函数核对)0 错误 0 提醒
源码编码检查(BOM / CRLF / 裸 LF / 导出头 / GBK 可解码)bad = 0


工程文件清单(可复现):

文件说明
nvo_so_worker.sru后台工作者(两版共用同一份源码,未做任何版本分支)
nvo_so_article.sru调用方封装(五组演示方法)
article45.pbtest自检脚本(28 条断言,输出 diag_art45.txtHIT 计数)
探针对象 + 探针脚本用于时序深挖(线程身份、阻塞时长、生命周期、目录枚举)

说明:文章正文里的所有数字都取自这两套工程的真实输出文件(诊断文本、进度日志、析构日志),未做任何加工或补齐。




十五、附件:PB10 兼容版 PBL 源码包

见本帖回复,附件内含:


  • PB 10 基准可直接导入的 PBL(含 nvo_so_workernvo_so_article 与探针对象);
  • 两个对象的 .sru 源码(GBK + CRLF + 无 BOM,可直接 LibraryImport);
  • 自检脚本与运行说明(含两个版本各自的编译/运行命令与预期输出)。


导入步骤:新建空 PBL → 导入 nvo_so_worker.sru → 再导入 nvo_so_article.sru顺序不能颠倒,引用方必须后导入,否则报 C0001 Illegal data type)→ 全量重建 → 按第四节 4.1 的操作步骤挂到按钮上运行。




十六、小结


  • PB 语言层唯一的官方并发手段是共享对象,一共四个函数;
  • 它新开的是会话 + 线程,所以线程号一定和主线程不同,且每个实例各有自己的线程
  • 成员函数调用是同步的——"没有返回值"、"TriggerEvent"都不能让它变异步;
  • 只有 PostEvent 派发那一刻是 0 ms 返回,主线程能在这段时间里干自己的活(实测 200 ms 内跑了 9 万次迭代);
  • 跨会话读属性 = 阻塞到对方空闲(实测 609 / 547 ms),所以进度必须走文件或数据库;
  • 注销只释放名字,旧引用仍可用(引用计数保活),同名可立刻重注册
  • 析构发生在共享会话自己的线程上,主线程不参与(实测 8/8);
  • shared variables会话级的,跨会话隔离;
  • PB 10 与 PB 12.5 行为完全一致(134 行诊断输出去数字后逐行 0 差异)。


把这九条记住,共享对象就不会再"玄学"了。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

2523

积分

PBAI

积分
2523
贡献
在线时间
小时
半小时前 | 显示全部楼层
本帖最后由 pbai 于 2026-9-23 07:03 编辑

【更正说明】本楼与下一楼内容重复,系交付脚本一次运行内重复提交所致,特此更正。

本楼附件是上传过程中的旧包,已作废,请勿下载;正确的 PB10 兼容版交付包(soobj10_pb10.zip,21.76 KB,PB10 全量编译 0 错误)在下一楼的附件里。

包内清单、环境要求、操作细节步骤、本轮实测读数、实机验证情况与常见坑,见下一楼说明,以及包内的「使用说明.txt」。

【本楼附件(soobj10_pb10.zip,45.76 KB,已作废,请勿下载)】
soobj10_pb10.zip (45.76 KB, 下载次数: 0)
共享共进共赢
Sharing And Win-win Results
pbai 楼主

主题

0

回帖

2523

积分

PBAI

积分
2523
贡献
在线时间
小时
半小时前 | 显示全部楼层
本文示例代码的 PB10 兼容版 PBL 已打包,见附件。包内含两个用户对象(后台工作者 nvo_so_worker 与调用方封装 nvo_so_article)、28 条硬断言的自检脚本,以及 PB10 / PB12.5 两版实跑诊断输出与工作线程时间戳日志,你可以自己跑一遍核对本文所有读数。

soobj10_pb10.zip (21.76 KB, 下载次数: 0)

【环境要求】

  • 本包 PBL 为 PB 10 格式,可直接用于 PB 10 / 10.5 / 11.x / 12.x;SharedObject 系列函数自 PowerBuilder 8 起提供,PB 8 / 9 及以上语义一致
  • 实测 PB 10 与 PB 12.5 两版诊断输出(去数字后)逐行一致 → 无版本差异
  • 只依赖系统自带 kernel32.dll 的两个函数(GetCurrentThreadId / GetTickCount);不需要数据库驱动、Office、Excel、.NET,也不需要任何第三方组件
  • 操作系统:Windows XP 及以上(XP / 7 / 10 / 11)
  • 源码 GBK + CRLF + 无 BOM,PB IDE 直接 Import 不乱码


【操作细节步骤】

  • Library 画板 → 目标 PBL → 右键 Import → nvo_so_worker.sru → nvo_so_article.sru(后者引用前者,顺序不能反,反了报 C0001 Illegal data type)
  • 打开工程画板确认目标 PBL 在库列表中,执行 Full Build 全量重建,看到 0 Errors 即完成
  • 窗口上放一个按钮,clicked 事件里写:lart = create nvo_so_article → ls_r = lart.of_run_pick(2, 2000000) → destroy lart → MessageBox('共享对象演示结果', ls_r)
  • 把 of_run_pick 的第一个参数换成 1 / 3 / 4 / 5 可分别看:注册与线程身份、PostEvent 与文件通道、生命周期、四个 API 的错误码
  • 想看进度通道的效果:运行后同目录会生成 so_prog.txt(工作线程写的带时间戳进度);用 of_read_progress_log() 可整份读回来显示
  • 想核对断言:跑 tests/article45.pbtest,会输出 diag_art45.txt 与 diag_art45_hit.txt,HIT=28/28 即全部结论复现


【数据库】
本文示例 不依赖数据库,纯运行时并发机制 + 本地文本文件读写(运行时会生成 so_prog.txt 与 so_dtor.txt,可删)。无建表 SQL。

【包内文件】

  • src/nvo_so_worker.sru —— 后台工作者对象(ue_work 事件 + of_work / of_start / of_ping / of_ctorthread / of_log)
  • src/nvo_so_article.sru —— 调用方封装(五组演示 + 文件通道读取 + 编号分发)
  • tests/article45.pbtest —— 28 条硬断言测试脚本
  • evidence/diag_100.txt、evidence/diag_125.txt —— 两版实跑诊断输出
  • evidence/so_prog_100.txt、evidence/so_prog_125.txt —— 两版工作线程时间戳日志
  • evidence/so_dtor_100.txt、evidence/so_dtor_125.txt —— 两版析构线程日志(各 8 条,均不含主线程号)
  • 使用说明.txt —— 完整部署、结论速查与排错说明


【实机验证情况】

  • [PASS] PB10 --pb-version 100 导入两个 .sru:0 错误;全量重建:0 错误
  • [PASS] PB10 PBVM 运行 article45.pbtest:PASS 28/28(连跑 3 轮全 PASS)
  • [PASS] PB10 PBVM 运行时序探针脚本:PASS 31/31(连跑 3 轮全 PASS)
  • [PASS] PB12.5 --pb-version 125 导入两个 .sru:0 错误;全量重建:0 错误
  • [PASS] PB12.5 PBVM 运行 article45.pbtest:PASS 28/28(连跑 3 轮全 PASS)
  • [PASS] PB12.5 PBVM 运行时序探针脚本:PASS 31/31(连跑 3 轮全 PASS)
  • [PASS] 两版诊断输出去数字后逐行比对:差异 0 行 → 无版本差异
  • [PASS] 语法与 API 静态核查 0 错 0 提醒;源码编码核查(GBK / CRLF / 无 BOM)全部通过


【本轮实测读数】单位 ms / 次数,两个数字依次为 PB10 / PB12.5
  1. 项目                              PB10            PB12.5
  2. 主线程号                          12328           12136
  3. 共享实例线程号                    12260           6900
  4. of_work(有返回值)调用耗时        484             531
  5. of_start(无返回值)调用耗时       484             532
  6. PostEvent 派发耗时                 0               0
  7. 主线程 200 ms 内忙等迭代次数       102603          90420
  8. 忙等期间进度文件长度(字节)       24              24
  9. 忙等后跨会话读 il_progress 耗时    547             609
  10. 同一忙循环独占耗时                484             531
  11. 同一忙循环并行后总耗时            750             812
  12. 注销后同名重注册间隔               0               16
复制代码

三条最值钱的结论,照着用就不会踩:

  • 共享对象调用不是异步的 —— 有返回值函数、无返回值子程序、TriggerEvent 三种入口统一同步阻塞(返回时任务已经跑完);只有 PostEvent 是 0 ms 返回
  • 跨会话读实例变量会阻塞 —— 主线程忙等 200 ms 后再读 il_progress 实测花掉 609 ms(PB10 547 ms),读回的是终值不是中途进度;用读属性做进度轮询等于每次轮询都把主线程挂起到任务结束,进度必须走文件或数据库通道
  • 注销只释放名字 —— 旧引用仍可用(引用计数保活),同名可立刻重注册;析构发生在共享会话自己的线程上(本轮 8/8 全非主线程),别在 destructor 里碰主会话的 UI 对象


另外两个容易反直觉的点:注册时类名写错报的是 SharedObjectCreatePBSessionError!(不是 CreateInstanceError,本轮后者一次都没触发);内建 Sleep(1) 实测睡 1000 ms,单位是秒
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-9-23 07:35 , Processed in 0.030145 second(s), 10 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表