第七节:在多个线程时空中,把各自的代码塞到一个指定的线程时空运行
以 Ado 为例,常见的方法是拖一个 AdoConnection 在窗口上(或 DataModule 中),
再配合 AdoQuery ,DataSoure, DbGrid 等,就可以实现数据库的访问操作。
这种方式,可以理解为在主线程时空中访问数据库。
如果某一个查询或更新操作耗时较长,那么,界面将会假死。
1.在线程时空中访问数据库
其实,上文中提到的这个 AdoConnection 不单只能在主线程时空中访问,
亦可以在另一个多线程时空里访问。但有一个重要的前提:
即:任意时刻,只能是一个线程访问 AdoConnection !
可以是多线程时空访问,也可以是主线程时空访问,它们可以交替访问,但不能同时访问。
故此,我们可以定义一个 DataThread 线程时空,把其它线程时空中需要访问数据库的操作,
全部塞入到 DataThread 时空中。具体做法是在 DataThread 中设计一个接口,如下:
unit
uJooThread;
interface
uses
Classes, uFooThread;
type
TJooThread =
class
(TFooThread)
public
procedure
Synchronize(AProc: TThreadMethod);
end
;
implementation
{ TJooThread }
procedure
TJooThread
.
Synchronize(AProc: TThreadMethod);
begin
ExecProcInThread(AProc);
// 再设计一个等待 AProc 执行结果功能。
end
;
end
.
这样既简化了其它线程访问数据库的操作,也避免了因为数据库操作耗时引起界面假死。
另外,如果访问数据库发生了异常,重新 Create AdoConnection 然后再连接的操作也在这一个线程中进行,
就不必考虑数据库操作的先后问题。如:不会出现需要访问数据库时, AoConnection 是损坏状态的情况。
也就是说,用这个线程,把访问数据库的操作进行了排队。
定义一个工作线程A, 先从数据库中取参数,再进行计算,然后把结果再更新到数据库。
如果有10个线程A实例,那么线程A访问数据库的时候,只需要把访问操作塞进 DataThread 中即可。
有朋友肯定会有疑问,为何不为每一个线程指定一个 AdoConnection 。这样编程将会变得简单。
一、数据库的连接是会消耗资源的,连接数是有上限的。
二、如果每个线程的 AdoConnection 产生的操作都是访问同一个表。
那么数据库自身会上锁,将它们变成串行执行,并不能提高效率。
2.多个客户端,同时从数据库取参数会如何?
假设每个客户端通过一个 AdoStoredProcduce (存储过程)来获取参数,我们可以在此存储过程中,
加入数据库的 sp_getapplock (以 MSSQL为例)来让客户端按顺序获取到参数。更新亦同理。
由此可见,学习了多线程的锁,亦能推而广之理解数据库的锁。
以下是详细代码,至此,本教程的任务基本完成。
本例的调用方法就不写了。如果你读懂了所有的教程,应该会写了。
如果没读懂,写了又有什么意义呢?请恶补面向对象的基础知识。
以后的其它教程,均以本多线程教程为基础。
unit
uJooThread;
interface
uses
Classes, SyncObjs, uFooThread, uFooList;
type
PSyncRec = ^TSyncRec;
TSyncRec =
record
Method: TThreadMethod;
// 这是类的方法
Proc: TThreadProcedure;
// 这是匿名方法
// 本例只写了类的方法。需要匿名方法,请自行重载 Sync 与 Queue
Signle: TEvent;
Queue:
boolean
;
end
;
TSyncRecList =
class
(TFooList<PSyncRec>)
//用于装执行代码的 List
protected
procedure
FreeItem(Item: PSyncRec); override;
end
;
TJooThread =
class
(TFooThread)
private
FSyncRecList: TSyncRecList;
procedure
Check;
public
constructor
Create(ACanAccessCom:
boolean
);
destructor
Destroy; override;
procedure
Synchronize(AProc: TThreadMethod);
// 阻塞到 AProc执行完毕才返回。
procedure
Queue(AProc: TThreadMethod);
// 塞入线程后立即返回。
end
;
// 本例就是前面单节讲的知识的综合运用。
// TEvent,FooThread,FooList,全都用上了。
// 并构建了一个新的线程功能。
// 当我写完以后发现,与系统源码中,
// 窗口接收 WM_NULL 消息后的处理UI操作的功能,几乎是一模一样的。
// 不同的是,本例是在线程时空,系统源码是在主线程时空。
implementation
{ TJooThread }
procedure
TJooThread
.
Check;
var
p: PSyncRec;
begin
FSyncRecList
.
Lock;
// 所有要执行的代码,都在这个 List 中了。
// 此处是线程时空,故从List 中取出并执行代码即可。
try
p :=
nil
;
if
FSyncRecList
.
Count >
0
then
// 每次取 List 的第一个来执行。
begin
p := FSyncRecList[
0
];
FSyncRecList
.
Delete(
0
);
end
;
finally
FSyncRecList
.
Unlock;
end
;
if
Assigned(p)
then
begin
if
Assigned(p
.
Method)
then
p
.
Method
else
if
Assigned(p
.
Proc)
then
p
.
Proc();
if
not
p
.
Queue
then
// 如果是阻塞,就置信号。
p
.
Signle
.
SetEvent;
Dispose(p);
ExecProcInThread(Check);
end
;
end
;
constructor
TJooThread
.
Create(ACanAccessCom:
boolean
);
begin
inherited
;
FSyncRecList := TSyncRecList
.
Create;
end
;
destructor
TJooThread
.
Destroy;
begin
FSyncRecList
.
Free;
inherited
;
end
;
procedure
TJooThread
.
Queue(AProc: TThreadMethod);
var
p: PSyncRec;
begin
FSyncRecList
.
Lock;
try
new(p);
FSyncRecList
.
Add(p);
p
.
Method := AProc;
p
.
Queue :=
true
;
ExecProcInThread(Check);
finally
FSyncRecList
.
Unlock;
end
;
end
;
procedure
TJooThread
.
Synchronize(AProc: TThreadMethod);
var
p: PSyncRec;
o: TEvent;
begin
FSyncRecList
.
Lock;
try
new(p);
FSyncRecList
.
Add(p);
p
.
Method := AProc;
o := TEvent
.
Create(
nil
,
true
,
false
,
''
);
p
.
Signle := o;
p
.
Queue :=
false
;
ExecProcInThread(Check);
//触发线程启动
finally
FSyncRecList
.
Unlock;
end
;
o
.
WaitFor;
// 等待 AProc 执行完毕的信号
o
.
Free;
end
;
{ TSyncRecList }
procedure
TSyncRecList
.
FreeItem(Item: PSyncRec);
begin
inherited
;
if
Assigned(Item
.
Signle)
then
Item
.
Signle
.
Free;
Dispose(Item);
end
;
end
.