PB多线程
PB多线程主要通过下面几个系统函数实现:
SharedObjectRegister
SharedObjectGet
SharedObjectUnregister
通过这两个函数创建线程对象后,便可以像一般对象一样调用其中的方法,post调用线程对象的方法时代码会以独立的线程执行。
使用多线程的好处是:
1.对于IO密集型操作,以独立线程执行可以避免程序卡顿或失去响应。
2.对于计算密集型操作,可以利用CPU的多核并行计算,提升效率。
对于PB来说,多线程在编码上和一般的单线程代码并没有不同之处,完全可以单线程一样多线程地调用函数或事件。但是PB的所有可视对象都是绑定在主线程上的,并且创建线程时,线程对象与主线程是完全隔离的,无法直接访问主线程上的信息,包括主线程上的全局变量,这给编程带来了麻烦。另外,如果试图在子线程里直接调用可视对象,程序会直接挂掉;如果在一个子线程对象正在执行代码时试图操作这个对象,程序也会直接挂掉。
PB要实现子线程对主线程的交互,只能通过不可视对象。主线程上创建子线程对象后,传递一个不可视对象给子线程,子线程可以通过调用这个不可视对象上的函数或事件来间接地对主线程通讯。一个简单的例子是将datastore设置共享数据给datawindow,再将其传递给子线程,在子线程中操作这个datastore,效果会立即反映在datawindow上。
在实际操作上,PB多线程代码常常由于编码失误等原因导致程序死锁或挂掉。多线程代码并不能以一般单线程的思维去编写,需要花费额外成本解决线程协同问题,并且子线程代码不可断点调试,问题常常不能重现,这给开发造成了很大的困难。
针对这些问题,参考其他编程语言,本文设计了一个解决方案:线程池。编写一套线程池类和抽象的线程执行类,线程池解决线程协同控制问题,具体的业务代码实现线程执行类。本文设计的线程池基础结构如下图:
n_thread_pool有以下属性:
int corePoolSize = 8 //核线程数,即线程池中常驻线程数量
int maxPoolSize = 16 //最大线程数,即线程池中最大的线程数量
int maxQueueSize = 64 //队列池容量,即线程池中排队等待执行的任务上限
string execObject = 'n_thread_run' //线程执行实现类名,需继承n_thread_run
string instancePrefix = "thread_" //线程实例名前缀,多个线程池同时存在情况需分配不同实例名前缀
boolean dbConnect //线程是否连接数据库,设置为true时线程会自动连接主线程SQLCA事务连接的数据库
powerobject receiver //线程回调对象,可以是可视对象,也可以是不可视对象
any threadInitParam //线程初始化参数,设置后参数会传递给线程执行类的init事件
性能测试
这里只测试多线程执行计算密集型操作,实际上一般情况下IO密集型操作使用多线程对效率的提升会更加显著。测试执行的代码为另一篇文章里的MD5算法。测试程序将对25个文本文件计算MD5值。
测试数据放在D盘目录下:
读取测试数据:
单线程运行,用时37797ms:
使用线程池默认的8线程运行,用时12766ms:
源代码
测试程序代码基于PB12.5版本,完整的程序及测试数据下载链接:百度网盘,提取码:jza9
核心源代码如下
发现BUG请留言或私信,以便修正(QQ:768310524 TEL:18649713925)
这部分代码拷贝到文本编辑器,另存为 n_thread_pool.sru
forward
global type n_thread_pool from nonvisualobject
end type
type stparam from structure within n_thread_pool
end type
type strunner from structure within n_thread_pool
end type
end forward
type stparam from structure
any value
boolean available
end type
type strunner from structure
n_thread_run runner
boolean available
end type
global type n_thread_pool from nonvisualobject
event thread_begin ( integer idx, any param )
event thread_end ( integer idx, any param, any info )
event thread_error ( integer idx, any param, string errtext )
event thread_msg ( integer idx, any param, any msg )
end type
global n_thread_pool n_thread_pool
type variables
public:
string instancePrefix = "thread_"
boolean dbConnect
powerobject receiver
any threadInitParam
string execObject = 'n_thread_run'
int corePoolSize = 8
int maxPoolSize = 16
int maxQueueSize = 64
private:
stParam execParams[]
long paramSetIdx,paramGetIdx
stRunner execRunners[]
int threadCount,runningCount
end variables
forward prototypes
private function integer _get_available_idx (boolean available)
public function integer exec (any param)
private function long _task_dequeue (ref any param)
private function long _task_enqueue (any param)
public function integer runningcount ()
private subroutine _thread_del (integer idx)
private subroutine _thread_run (integer idx, any param)
private function integer _thread_init ()
end prototypes
event thread_begin(integer idx, any param);if isvalid(receiver) then
receiver.dynamic event thread_begin(instancePrefix+string(idx),param)
end if
runningCount += 1
end event
event thread_end(integer idx, any param, any info);if isvalid(receiver) then
receiver.dynamic event thread_end(instancePrefix+string(idx),param,info)
end if
//execRunners[idx].available = true
runningCount -= 1
any paramTemp
if _task_dequeue(paramTemp) < 0 then
if threadCount > corePoolSize then
threadCount -= 1
post _thread_del(idx)
else
execRunners[idx].available = true
end if
else
_thread_run(idx,paramTemp)
end if
end event
event thread_error(integer idx, any param, string errtext);if isvalid(receiver) then
receiver.dynamic event thread_error(instancePrefix+string(idx),param,errtext)
end if
//execRunners[idx].available = true
runningCount -= 1
any paramTemp
if _task_dequeue(paramTemp) < 0 then
if threadCount > corePoolSize then
threadCount -= 1
post _thread_del(idx)
else
execRunners[idx].available = true
end if
else
_thread_run(idx,paramTemp)
end if
end event
event thread_msg(integer idx, any param, any msg);if isvalid(receiver) then
receiver.dynamic event thread_msg(instancePrefix+string(idx),param,msg)
end if
end event
private function integer _get_available_idx (boolean available);int i
for i = 1 to upperbound(execRunners)
if execRunners[i].available then
return i
end if
next
return -1
end function
public function integer exec (any param);int idx
any paramDequeue
if threadCount < corePoolSize then
idx = _thread_init()
threadCount += 1
_thread_run(idx,param)
return 0
end if
if maxQueueSize <= 0 then
idx = _get_available_idx(true)
if idx <= 0 then goto _EXTRA
_thread_run(idx,param)
return 0
end if
if _task_enqueue(param) < 0 then
goto _EXTRA
end if
idx = _get_available_idx(true)
if idx <= 0 then return 0
_task_dequeue(paramDequeue)
_thread_run(idx,paramDequeue)
return 0
_EXTRA:
if threadCount < maxPoolSize then
idx = _thread_init()
threadCount += 1
_thread_run(idx,param)
return 0
end if
return -1
end function
private function long _task_dequeue (ref any param);if maxQueueSize <= 0 then return -1
if paramGetIdx >= maxQueueSize then
paramGetIdx -= maxQueueSize
end if
if upperbound(execParams) <= paramGetIdx then return -1
if not execParams[paramGetIdx+1].available then return -1
paramGetIdx += 1
param = execParams[paramGetIdx].value
execParams[paramGetIdx].available = false
return paramGetIdx
end function
private function long _task_enqueue (any param);if maxQueueSize <= 0 then return -1
if paramSetIdx >= maxQueueSize then
paramSetIdx -= maxQueueSize
end if
if upperbound(execParams) <= paramSetIdx then execParams[paramSetIdx+1].available = false
if execParams[paramSetIdx+1].available then return -1
paramSetIdx += 1
execParams[paramSetIdx].value = param
execParams[paramSetIdx].available = true
return paramSetIdx
end function
public function integer runningcount ();return runningCount
end function
private subroutine _thread_del (integer idx);sharedobjectunregister(instancePrefix+string(idx))
destroy execRunners[idx].runner
execRunners[idx].available = false
end subroutine
private subroutine _thread_run (integer idx, any param);execRunners[idx].available = false
execRunners[idx].runner.post event exec(param)
end subroutine
private function integer _thread_init ();int i,idx
for i = 1 to upperbound(execRunners)
if not isvalid(execRunners[i].runner) then idx = i
next
if idx <= 0 then idx = upperbound(execRunners) + 1
sharedobjectregister(execObject,instancePrefix+string(idx))
sharedobjectget(instancePrefix+string(idx),execRunners[idx].runner)
//transaction property
execRunners[idx].runner.AutoCommit = SQLCA.AutoCommit
execRunners[idx].runner.Database = SQLCA.Database
execRunners[idx].runner.DBMS = SQLCA.DBMS
execRunners[idx].runner.DBParm = SQLCA.DBParm
execRunners[idx].runner.DBPass = SQLCA.DBPass
execRunners[idx].runner.Lock = SQLCA.Lock
execRunners[idx].runner.LogID = SQLCA.LogID
execRunners[idx].runner.LogPass = SQLCA.LogPass
execRunners[idx].runner.ServerName = SQLCA.ServerName
execRunners[idx].runner.UserID = SQLCA.UserID
execRunners[idx].runner.dbConnect = dbConnect
execRunners[idx].runner.initParam = threadInitParam
execRunners[idx].runner.in_pool = this
execRunners[idx].runner.idx = idx
execRunners[idx].available = true
return idx
end function
on n_thread_pool.create
call super::create
TriggerEvent( this, "constructor" )
end on
on n_thread_pool.destroy
TriggerEvent( this, "destructor" )
call super::destroy
end on
event destructor;int i
for i = 1 to upperbound(execRunners)
if isvalid(execRunners[i].runner) and execRunners[i].available then
threadCount -= 1
_thread_del(i)
end if
next
end event
这部分代码拷贝到文本编辑器,另存为 n_thread_run.sru
forward
global type n_thread_run from nonvisualobject
end type
end forward
global type n_thread_run from nonvisualobject
event thread_msg ( any msg )
event thread_begin ( )
event thread_end ( any retmsg )
event thread_error ( string errtext )
event exec ( any param )
event run ( any param, ref any retmsg )
event init ( any param )
end type
global n_thread_run n_thread_run
type variables
public:
int idx
n_thread_pool in_pool
boolean dbConnect
any initParam
//transaction property
boolean AutoCommit
string Database,DBMS,DBParm,DBPass,Lock,LogID,LogPass,ServerName,UserID
private:
boolean initialized
any execParam
end variables
event thread_msg(any msg);if isvalid(in_pool) then
in_pool.event thread_msg(idx,execParam,msg)
end if
end event
event thread_begin();if isvalid(in_pool) then
in_pool.event thread_begin(idx,execParam)
end if
end event
event thread_end(any info);if isvalid(in_pool) then
in_pool.event thread_end(idx,execParam,info)
end if
end event
event thread_error(string errtext);if isvalid(in_pool) then
in_pool.event thread_error(idx,execParam,errtext)
end if
end event
event exec(any param);any execRet
execParam = param
if isvalid(in_pool) then
in_pool.event thread_begin(idx,execParam)
end if
if not initialized then
try
event init(initParam)
initialized = true
catch(RuntimeError e_thread_init)
post event thread_error(e_thread_init.text)
return
end try
end if
try
event run(execParam,execRet)
post event thread_end(execRet)
catch(RuntimeError e_thread_exec)
post event thread_error(e_thread_exec.text)
return
end try
return
end event
event run(any param, ref any retmsg);//to be inherited
return
end event
event init(any param);//connect database
if dbConnect then
SQLCA.AutoCommit = AutoCommit
SQLCA.Database = Database
SQLCA.DBMS = DBMS
SQLCA.DBParm = DBParm
SQLCA.DBPass = DBPass
SQLCA.Lock = Lock
SQLCA.LogID = LogID
SQLCA.LogPass = LogPass
SQLCA.ServerName = ServerName
SQLCA.UserID = UserID
connect using SQLCA;
if SQLCA.SQLCode < 0 then
runtimeerror e
e = create runtimeerror
e.text = SQLCA.SQLErrText
throw e
end if
end if
//to be inherited
return
end event
on n_thread_run.create
call super::create
TriggerEvent( this, "constructor" )
end on
on n_thread_run.destroy
TriggerEvent( this, "destructor" )
call super::destroy
end on