PB多线程的实现与线程池设计

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


  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
注意事项: 1、主进程传入的对象或变量不管是不是ref线程内操作传入的引用都会影响主进程的对象或变量,且对象只能是nonvisualobject类型的。 2、基础变量如long等等都不能传引用ref会运行会报错 3、SharedObjectUnregister只是把SharedObjectDirectory中的去掉,实际内存不会释放必须destroy 4、主进程不能直接访问线程中的变量和对象,可以通过处理类私有的办法处理。 5、千万注意释放线程的时候一定要把线程里面的资源释放完,不然百分百卡死。比如一个线程里面有一个timing的计时器,如果不先stop(),直接destroy,百分百卡死。如果连接数据库或者其他接口时千万注意了!!!千万要在uf_stop()(此例子中的释放预留方法)里面把所有的资源都释放干净,资源都释放干净,源都释放干净,都释放干净,释放干净,放干净,干净,净…… 大体设计思路: 1、在主进程中建立一个“任务信息类”数组,其中包含“任务线程类”,一个任务对应一个线程。 2、在主进程中建立一个“任务管理类”,负责处理任务信息类。 简单举例: 1、新建1个“任务管理类”,再新建N“任务信息类”,将“任务信息类”赋值完成加入“任务管理类”,并创建一个“任务线程类”,此时线程开始running。 2、“任务线程类”中有一个内部timing类,监控自己是否执行完成,会改标志。“任务管理类”也有一个timing监控“任务信息类”和“任务线程类”的情况,把完成的结束。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值