Delphi中的线程类
上一篇 / 下一篇 2007-04-24 12:11:06 / 个人分类:其他
:F#{.h/?4f Rh0文章来源: http://liukun966123.my.gsdn.net/2004/10/22/4797/51Testing软件测试网Iu#zpvD{l't
,X7F`7\j1q
t5s/b|i0Delphi中的线程类 51Testing软件测试网 o [5Xq%`FA^N
^~@ IM](E0转贴于 华夏黑客同盟 http://www.77169.org
M%vm\g1h0Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉都有说到,但基本上都是对
y4X?eDI9p8J0 51Testing软件测试网0xl9l7v8}{TThread类的几个成员作一简单介绍,再说明一下Execute的实现和Synchronize的用法就完了。然而这并不是多线程编51Testing软件测试网"N8YD3wNMJ
程的全部,我写此文的目的在于对此作一个补充。51Testing软件测试网 ]8O9v7V}I-X ^A1N6I
Dm&Bg8H-Lb&|0线程本质上是进程中一段并发运行的代码。一个进程至少有一个线程,即所谓的主线程。同时还可以有多个子线程。51Testing软件测试网|7`
\Qsg'p
当一个进程中用到超过一个线程时,就是所谓的“多线程”。51Testing软件测试网 Z#Ny7[5LN+Vd
那么这个所谓的“一段代码”是如何定义的呢?其实就是一个函数或过程(对Delphi而言)。
&R
LXh a |C4L0如果用Windows API来创建线程的话,是通过一个叫做CreateThread的API函数来实现的,它的定义为:
:\CXb#vp4x*?U0HANDLE CreateThread(
#p8B*By3H5L0 LPSECURITY_ATTRIBUTES lpThreadAttributes, 51Testing软件测试网[O(qaPP7{
\ i}S
DWORD dwStackSize,
(o
K5D
E'tBI7Z%w0 LPTHREAD_START_ROUTINE lpStartAddress, 51Testing软件测试网iz!_O8O+L2R$I1X%BR
LPVOID lpParameter, 51Testing软件测试网&}-Yln)`.Ejq
DWORD dwCreationFlags, 51Testing软件测试网{:X!x{1q
LPDWORD lpThreadId
)H9y@ u.wY[)[&f$p0);51Testing软件测试网#Xy?;A:P4`
Z"l,fb5a0其各参数如它们的名称所说,分别是:线程属性(用于在NT下进行线程的安全属性设置,在9X下无效),堆栈大小,51Testing软件测试网5g5`y9xxj
起始地址,参数,创建标志(用于设置线程创建时的状态),线程ID,最后返回线程Handle。其中的起始地址就是线51Testing软件测试网6LKCv%p PH
程函数的入口,直至线程函数结束,线程也就结束了。
v0sf!w5Q*t0因为CreateThread参数很多,而且是Windows的API,所以在C Runtime Library里提供了一个通用的线程函数(理论上51Testing软件测试网1v9c bt"g{
可以在任何支持线程的OS中使用):
$I
D*bX*y(r0U\9n0unsigned long _beginthread(void (_USERENTRY *__start)(void *), unsigned __stksize, void *__arg);
j/P0@__ c.?0Delphi也提供了一个相同功能的类似函数:51Testing软件测试网)szQR!^
z@\
function BeginThread(
/\v7l&XA(MUzAs!T.Y0 SecurityAttributes: Pointer; 51Testing软件测试网Q Wzp1}
vL!x4b^j!?
StackSize: LongWord; 51Testing软件测试网m7e?
tNPC1~
ThreadFunc: TThreadFunc;
7w
ff6~K8r0 Parameter: Pointer;
6s)GS~9X4kN4K0 CreationFlags: LongWord; 51Testing软件测试网v\8H&U%b&K`{,v
var ThreadId: LongWord
~+X6LcB\\y0): Integer;51Testing软件测试网??jk9C}
9M;d5za3H`;j0
a~}A4c8t$g0 51Testing软件测试网swjB @)o-J)hGk这三个函数的功能是基本相同的,它们都是将线程函数中的代码放到一个独立的线程中执行。线程函数与一般函数的51Testing软件测试网.?9R"vB]-^
最大不同在于,线程函数一启动,这三个线程启动函数就返回了,主线程继续向下执行,而线程函数在一个独立的线51Testing软件测试网K]afE@x@
程中执行,它要执行多久,什么时候返回,主线程是不管也不知道的。51Testing软件测试网;b |*dtam}x
正常情况下,线程函数返回后,线程就终止了。但也有其它方式:51Testing软件测试网~u;q.UA5??$f
Windows API:51Testing软件测试网XMn.Ily
VOID ExitThread( DWORD dwExitCode );
C Runtime Library:
-dC!\8P^Z.d+rJ;~2C0void _endthread(void);
Delphi Runtime Library:51Testing软件测试网/UYL-wx8wg
procedure EndThread(ExitCode: Integer);
.MzcN.|$P2R0为了记录一些必要的线程数据(状态/属性等),OS会为线程创建一个内部Object,如在Windows中那个Handle便是这51Testing软件测试网&wc{"h)W0j7^(A1g
个内部Object的Handle,所以在线程结束的时候还应该释放这个Object。
O
v;CV2M*C0虽然说用API或RTL(Runtime Library)已经可以很方便地进行多线程编程了,但是还是需要进行较多的细节处理,为此51Testing软件测试网:LO
k$g~UyX1l
Delphi在Classes单元中对线程作了一个较好的封装,这就是VCL的线程类:TThread
]INf`9F)?0使用这个类也很简单,大多数的Delphi书籍都有说,基本用法是:先从TThread派生一个自己的线程类(因为TThread51Testing软件测试网
BV4g
~9s/X5L
是一个抽象类,不能生成实例),然后是Override抽象方法:Execute(这就是线程函数,也就是在线程中执行的代码
.c6X.WAKJC0部分),如果需要用到可视VCL对象,还需要通过Synchronize过程进行。关于之方面的具体细节,这里不再赘述,请51Testing软件测试网(WSj|l'u~
参考相关书籍。
$OW&~UymS0本文接下来要讨论的是TThread类是如何对线程进行封装的,也就是深入研究一下TThread类的实现。因为只是真正地51Testing软件测试网PUni C2_$k
了解了它,才更好地使用它。51Testing软件测试网+v7hA(B9H3\
下面是DELPHI7中TThread类的声明(本文只讨论在Windows平台下的实现,所以去掉了所有有关Linux平台部分的代码
7Wl4y!qn2[0):
TThread = class51Testing软件测试网R#y1W0V8?beu
private51Testing软件测试网%mvc2s4Q
FHandle: THandle;51Testing软件测试网,XM"Y7pj7f*i7p{`
FThreadID: THandle;51Testing软件测试网2pXF/U$K;E
FCreateSuspended: Boolean;
gwK7EU$s%PgC0 FTerminated: Boolean;
Nq;VP+QQ0 FSuspended: Boolean;51Testing软件测试网f-Yw3w$]XC
FFreeOnTerminate: Boolean;51Testing软件测试网| tk^H5w/ct?
FFinished: Boolean;51Testing软件测试网St4j;kr
FReturnValue: Integer;
p1A#Os6G0 FOnTerminate: TNotifyEvent;51Testing软件测试网Gw.K/~y7r
FSynchronize: TSynchronizeRecord;51Testing软件测试网8_6XpOpD
FFatalException: TObject;
!i-tSZ,K
}1G5y#M|-I0 procedure CallOnTerminate;
C+K[;oz0 class procedure Synchronize(ASyncRec: PSynchronizeRecord); overload;
3?-t5\2d2SYb0 function GetPriority: TThreadPriority;51Testing软件测试网;W5|laK!_]%zaJ2o
procedure SetPriority(Value: TThreadPriority);
GsSR)A$H.E
V0 procedure SetSuspended(Value: Boolean);51Testing软件测试网 _y
vP&I-Tm&Oq ]
protected
?+P
y9z1BD0 procedure CheckThreadError(ErrCode: Integer); overload;51Testing软件测试网1i2q'I0?J6~Kvf-Tj0y
procedure CheckThreadError(Success: Boolean); overload;
E"A-G S4A'M0 procedure DoTerminate; virtual;
n?7LV){
oa,B+{ P K0 procedure Execute; virtual; abstract;51Testing软件测试网 lh9m1~E;jN9h
procedure Synchronize(Method: TThreadMethod); overload;
)Ot8SC+i*ec0 property ReturnValue: Integer read FReturnValue write FReturnValue;51Testing软件测试网LbvJcJ
property Terminated: Boolean read FTerminated;
|/Y-d Y2HHN;s!?0public51Testing软件测试网4Y)_^4TV5A os/f"}
constructor Create(CreateSuspended: Boolean);51Testing软件测试网4J+k%[0TDJWh
destructor Destroy; override;51Testing软件测试网^(a:z&?VD.r$W'E.k
procedure AfterConstruction; override;51Testing软件测试网H!qe
Lf2~
O? [)K
procedure Resume;
8ux1mk*`tu#c!j0 procedure Suspend;51Testing软件测试网%N"y%b9B[H`d
procedure Terminate;
|aE%`^&o0 function WaitFor: LongWord;
$K;O6DzW%by{rp0 class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload;
P t#x)Y+Y2Z m.@0 class procedure StaticSynchronize(AThread: TThread; AMethod: TThreadMethod);
i}mg(d~0 property FatalException: TObject read FFatalException;
(P)FY@1n,Q0 property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate;
l`+fM
{4n,~.z0 property Handle: THandle read FHandle;51Testing软件测试网o1d PqUw6` K
property Priority: TThreadPriority read GetPriority write SetPriority;51Testing软件测试网(GsR$R4yH
property Suspended: Boolean read FSuspended write SetSuspended;
Eg@,EM0 property ThreadID: THandle read FThreadID;
pdS#\A2Y*C0 property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;51Testing软件测试网
K2N/Mkl9W+i
end;
O
~kZ7pq!f0TThread类在Delphi的RTL里算是比较简单的类,类成员也不多,类属性都很简单明白,本文将只对几个比较重要的类51Testing软件测试网&tx4E!CG
成员方法和唯一的事件:OnTerminate作详细分析。
8J)kt#oH1u$vv0首先就是构造函数:51Testing软件测试网;|d9dVd1|.b
constructor TThread.Create(CreateSuspended: Boolean);
Vj1mKxt2XC0begin51Testing软件测试网Gkm\1Z_,C
@
inherited Create;51Testing软件测试网:?e v}]
AddThread;51Testing软件测试网1B7v
zl_IhN;rJ
FSuspended := CreateSuspended;
_~9e&n)\7^:z:OM2Xe0 FCreateSuspended := CreateSuspended;51Testing软件测试网v3l ^-Ayh#F
P1g{
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
X kX1Dv:c0 if FHandle = 0 then
v%m4R @xId0 raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]);
!n#m8T2`hX0I0end;51Testing软件测试网(L5h.V8Ckl\2Nc"`
虽然这个构造函数没有多少代码,但却可以算是最重要的一个成员,因为线程就是在这里被创建的。51Testing软件测试网9Ug5B0DGf~M&o
在通过Inherited调用TObject.Create后,第一句就是调用一个过程:AddThread,其源码如下:51Testing软件测试网 M/~2Y??&@f
procedure AddThread;51Testing软件测试网H|-W2n6Z
begin
9R/A$E d%?0 InterlockedIncrement(ThreadCount);
cZ)l#JKU0end;51Testing软件测试网_sJ(ea
0fO2A(tH]Sa0同样有一个对应的RemoveThread:51Testing软件测试网z*Is1x5z\hE
procedure RemoveThread;
:xrf`ZI i s0begin51Testing软件测试网I)Xc5QS `:k+u K
InterlockedDecrement(ThreadCount);
b/l_oN{m5\0end;51Testing软件测试网A7zg8qBl1[gT
它们的功能很简单,就是通过增减一个全局变量来统计进程中的线程数。只是这里用于增减变量的并不是常用的
?5J:pwut0Inc/Dec过程,而是用了InterlockedIncrement/InterlockedDecrement这一对过程,它们实现的功能完全一样,都是51Testing软件测试网5d S| HRAs-w
对变量加一或减一。但它们有一个最大的区别,那就是InterlockedIncrement/InterlockedDecrement是线程安全的。
B!],LQm+w0即它们在多线程下能保证执行结果正确,而Inc/Dec不能。或者按操作系统理论中的术语来说,这是一对“原语”操作。
以加一为例来说明二者实现细节上的不同:
6`$D`0c}{w1M0一般来说,对内存数据加一的操作分解以后有三个步骤:
U
d
]0E)N?GsdU?01、 从内存中读出数据51Testing软件测试网"debyV"xq
M
2、 数据加一
'zQ@p8A2@r03、 存入内存51Testing软件测试网
T Wp-moUo
现在假设在一个两个线程的应用中用Inc进行加一操作可能出现的一种情况:51Testing软件测试网yDL~ ];x*t
1、 线程A从内存中读出数据(假设为3)51Testing软件测试网q?FU4jV
2、 线程B从内存中读出数据(也是3)51Testing软件测试网&me#W7PD*S
3、 线程A对数据加一(现在是4)
N8j+B9nT-P'G04、 线程B对数据加一(现在也是4)
4o:V[ Kz1l[05、 线程A将数据存入内存(现在内存中的数据是4)51Testing软件测试网 NR)g;D~G+Ra
6、 线程B也将数据存入内存(现在内存中的数据还是4,但两个线程都对它加了一,应该是5才对,所以这里出现了
;_6th9\:t[l#b8tr&Bq0错误的结果)
(|$y+S,HS0
pQng]/[\*mS0 51Testing软件测试网/~ zw%deC[而用InterlockIncrement过程则没有这个问题,因为所谓“原语”是一种不可中断的操作,即操作系统能保证在一个51Testing软件测试网L xZG)U2~&r!A
“原语”执行完毕前不会进行线程切换。所以在上面那个例子中,只有当线程A执行完将数据存入内存后,线程B才可51Testing软件测试网3hZ,JV\5r#fe1M
以开始从中取数并进行加一操作,这样就保证了即使是在多线程情况下,结果也一定会是正确的。
YCQ8Z6g5Wd Pt0前面那个例子也说明一种“线程访问冲突”的情况,这也就是为什么线程之间需要“同步”(Synchronize),关于这
'`M.Iq%wR}]B,V0个,在后面说到同步时还会再详细讨论。
说到同步,有一个题外话:加拿大滑铁卢大学的教授李明曾就Synchronize一词在“线程同步”中被译作“同步”提出
E:~$?u&q:c0过异议,个人认为他说的其实很有道理。在中文中“同步”的意思是“同时发生”,而“线程同步”目的就是避免这
O-{:q^I@TrJm0种“同时发生”的事情。而在英文中,Synchronize的意思有两个:一个是传统意义上的同步(To occur at the same 51Testing软件测试网vI8n @
xhF{2M
time),另一个是“协调一致”(To operate in unison)。在“线程同步”中的Synchronize一词应该是指后面一种
#h*G8B;D|7M,FZ0意思,即“保证多个线程在访问同一数据时,保持协调一致,避免出错”。不过像这样译得不准的词在IT业还有很多
4dFA$Q8Iz0,既然已经是约定俗成了,本文也将继续沿用,只是在这里说明一下,因为软件开发是一项细致的工作,该弄清楚的
7AK3x6@C`8@ebw0,绝不能含糊。51Testing软件测试网2eZ
\2W,l W$V$s
3E!{z*\^6FJ-I-W0扯远了,回到TThread的构造函数上,接下来最重要就是这句了:51Testing软件测试网,as+m!Y%Z
Jg
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);51Testing软件测试网 I$[4j,k9QU
这里就用到了前面说到的Delphi RTL函数BeginThread,它有很多参数,关键的是第三、四两个参数。第三个参数就是51Testing软件测试网 s]Y7{*zU
前面说到的线程函数,即在线程中执行的代码部分。第四个参数则是传递给线程函数的参数,在这里就是创建的线程
6[ Ew}
W:h0对象(即Self)。其它的参数中,第五个是用于设置线程在创建后即挂起,不立即执行(启动线程的工作是在
+B/T3\quE"@2S0AfterConstruction中根据CreateSuspended标志来决定的),第六个是返回线程ID。51Testing软件测试网1Ze
[7g-B2jo
:S3TO#@r3`^k)@$V0现在来看TThread的核心:线程函数ThreadProc。有意思的是这个线程类的核心却不是线程的成员,而是一个全局函数51Testing软件测试网/wC0]Z*JND"N
(因为BeginThread过程的参数约定只能用全局函数)。下面是它的代码:
v*X1]b
LO:G{0function ThreadProc(Thread: TThread): Integer;
$@u2B&Xr0_4I'XF0var51Testing软件测试网j3fQ|G&G
FreeThread: Boolean;
U@f h$PA!F0begin51Testing软件测试网` JlD(eYB+u
try
u.~vc(R;s0 if not Thread.Terminated then51Testing软件测试网f
T
SarZr
try51Testing软件测试网-~&U(f*vx IB
Thread.Execute;
Q|f5e$S.E5Xs0 except
wlAw]\0 Thread.FFatalException := AcquireExceptionObject;
&Ci|;w+h/R/t6V0 end;51Testing软件测试网0b(M7c {n8Q.s+_
finally51Testing软件测试网+G^7Lm-@~G:R8{
FreeThread := Thread.FFreeOnTerminate;
5CD-F$ccV0 Result := Thread.FReturnValue;
,|GI;tZ8v
p0 Thread.DoTerminate;
aX!Xt(xme0 Thread.FFinished := True;51Testing软件测试网'[
U&G.V e:kU
SignalSyncEvent;51Testing软件测试网!~/aT6[l1u
if FreeThread then Thread.Free;
GJm8nO0 EndThread(Result);
`9aGn[y+a0 end;51Testing软件测试网/C*n%_pwB
end;
'\4d3{\Fy^0虽然也没有多少代码,但却是整个TThread中最重要的部分,因为这段代码是真正在线程中执行的代码。下面对代码作51Testing软件测试网)~n(?3Fk
ZX
逐行说明:
6{)uon&O0首先判断线程类的Terminated标志,如果未被标志为终止,则调用线程类的Execute方法执行线程代码,因为TThread51Testing软件测试网.ti-V2t5V
?9O
是抽象类,Execute方法是抽象方法,所以本质上是执行派生类中的Execute代码。51Testing软件测试网4EP%b-K^ Fn
u8n*f&G)P S4I)[C0所以说,Execute就是线程类中的线程函数,所有在Execute中的代码都需要当作线程代码来考虑,如防止访问冲突等。
{\RrFS%D0如果Execute发生异常,则通过AcquireExceptionObject取得异常对象,并存入线程类的FFatalException成员中。
3J3RG
~'X7\0最后是线程结束前做的一些收尾工作。局部变量FreeThread记录了线程类的FreeOnTerminated属性的设置,然后将线
j1rNCvn_7\N0程返回值设置为线程类的返回值属性的值。然后执行线程类的DoTerminate方法。
5G
ea
Z:RK T0DoTerminate方法的代码如下:
\0L6]u)^Y0procedure TThread.DoTerminate;51Testing软件测试网D7Sxx6V hX3C
begin
G"i1z)s c*^4FvW
`}.y0 if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);51Testing软件测试网O#m[+oyXg3d
end;
g'Z1l&en(Y0很简单,就是通过Synchronize来调用CallOnTerminate方法,而CallOnTerminate方法的代码如下,就是简单地调用51Testing软件测试网"J1Iw%z4Pb
OnTerminate事件:
c)S0OL;E;m N)|V0procedure TThread.CallOnTerminate;51Testing软件测试网"a^,iz NsgC&ri1NE
begin51Testing软件测试网TU{jL"h
if Assigned(FOnTerminate) then FOnTerminate(Self);
4v w3BTS)E#Z9D:s-|0end;
因为OnTerminate事件是在Synchronize中执行的,所以本质上它并不是线程代码,而是主线程代码(具体见后面对51Testing软件测试网/{!i\(\[#b
Synchronize的分析)。51Testing软件测试网4N^.JU9Vx%z
fN
*hh$GFx0执行完OnTerminate后,将线程类的FFinished标志设置为True。接下来执行SignalSyncEvent过程,其代码如下:51Testing软件测试网
b7q_TAz*V_
procedure SignalSyncEvent;
},o w$W7n5Qh.Ju*@0begin51Testing软件测试网 p3J(z?Rd
SetEvent(SyncEvent);51Testing软件测试网R;rX|{:`
end;
也很简单,就是设置一下一个全局Event:SyncEvent,关于Event的使用,本文将在后文详述,而SyncEvent的用途将51Testing软件测试网 t]Y.{M-h
在WaitFor过程中说明。
然后根据FreeThread中保存的FreeOnTerminate设置决定是否释放线程类,在线程类释放时,还有一些些操作,详见接51Testing软件测试网#[x!mP/N*U:fyW
下来的析构函数实现。
NV"Z6D
g9?k8}EO0最后调用EndThread结束线程,返回线程返回值。至此,线程完全结束。
c"Eo
~Xh&{+T2a0说完构造函数,再来看析构函数:
{|en9`m5P4y5_I0destructor TThread.Destroy;51Testing软件测试网,um"flQ2_VcTy
begin
aB/mF&I4^0 if (FThreadID <> 0) and not FFinished then begin
COox#}6p[0 Terminate;
"]'GA`]kL6Dq0 if FCreateSuspended then51Testing软件测试网f5Xo I(|gT
Resume;
-Qo9Do4y;Q3H0 WaitFor;51Testing软件测试网F4e}YC9{\!F0\
end;
S8E1|Kr$mXI4Q*VC0 if FHandle <> 0 then CloseHandle(FHandle);51Testing软件测试网QT bk[/z;_8_5KK,P
inherited Destroy;
-LV xl#?
jt'n@)H"~9T0 FFatalException.Free;
*v1K!J;Y/Z[$Y[zX0 RemoveThread;
HV9y:^Lq0end;51Testing软件测试网7{ZW*SxVF8be-^#y
在线程对象被释放前,首先要检查线程是否还在执行中,如果线程还在执行中(线程ID不为0,并且线程结束标志未设
Hv,b5u9a F0置),则调用Terminate过程结束线程。Terminate过程只是简单地设置线程类的Terminated标志,如下面的代码:51Testing软件测试网+v(rN Jw0S
gl\S~3v0procedure TThread.Terminate;51Testing软件测试网rcR]7ni
begin
Hc-VV*nJ0 FTerminated := True;
4b,P0S
Ow2h^0end;51Testing软件测试网9ly(`/i$_Y]S
`v)~l:M q0所以线程仍然必须继续执行到正常结束后才行,而不是立即终止线程,这一点要注意。
!aaID~A@v:V`0Y
V D.@&[V eR0在这里说一点题外话:很多人都问过我,如何才能“立即”终止线程(当然是指用TThread创建的线程)。结果当然是51Testing软件测试网.t9Ac8Ib a%A
不行!终止线程的唯一办法就是让Execute方法执行完毕,所以一般来说,要让你的线程能够尽快终止,必须在51Testing软件测试网8`5et
|+T8u Ir
Execute方法中在较短的时间内不断地检查Terminated标志,以便能及时地退出。这是设计线程代码的一个很重要的原
x#jsU.qH#B3E2@X0则!51Testing软件测试网'|rN5h,O4j
当然如果你一定要能“立即”退出线程,那么TThread类不是一个好的选择,因为如果用API强制终止线程的话,最终51Testing软件测试网ZzM3^)Ni [9nZ.@E
会导致TThread线程对象不能被正确释放,在对象析构时出现Access Violation。这种情况你只能用API或RTL函数来创51Testing软件测试网/s,C~4k*m@F G
建线程。51Testing软件测试网~F9En.QZ/uj
-A2|+I4uU(E"].x0如果线程处于启动挂起状态,则将线程转入运行状态,然后调用WaitFor进行等待,其功能就是等待到线程结束后才继
/~T~"b&Vz7G7az"z0续向下执行。关于WaitFor的实现,将放到后面说明。