在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
线程本质上是进程中一段并发运行的代码.一个进程至少有一个线程,即所谓的主线程.同时还可以有多个子线程.当一个进程中用到超过一个线程时,就是所谓的"多线程". 那么这个所谓的"一段代码"是如何定义的呢?其实就是一个函数或过程(对Delphi而言). 如果用Windows API来创建线程的话,是通过一个叫做CreateThread的API函数来实现的,它的定义为: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程属性(用于在NT下进行线程的安全属性设置,在9X下无效), DWORD dwStackSize, //堆栈大小 LPTHREAD_START_ROUTINE lpStartAddress, //起始地址 LPVOID lpParameter, //参数 DWORD dwCreationFlags, //创建标志(用于设置线程创建时的状态) LPDWORD lpThreadId 线程ID ); 最后返回线程Handle.其中的起始地址就是线程函数的入口,直至线程函数结束,线程也就结束了. 因为CreateThread参数很多,而且是Windows的API,所以在C Runtime Library里提供了一个通用的线程函数(理论上可以在任何支持线程的OS中使用): unsigned long _beginthread(void (_USERENTRY *__start)(void *), unsigned __stksize, void *__arg); Delphi也提供了一个相同功能的类似函数: function BeginThread( SecurityAttributes: Pointer; StackSize: LongWord; ThreadFunc: TThreadFunc; Parameter: Pointer; CreationFlags: LongWord; var ThreadId: LongWord ): Integer; 这三个函数的功能是基本相同的,它们都是将线程函数中的代码放到一个独立的线程中执行.线程函数与一般函数的最大不同在于,线程函数一启动,这三个线程启 动函数就返回了,主线程继续向下执行,而线程函数在一个独立的线程中执行,它要执行多久,什么时候返回,主线程是不管也不知道的. 正常情况下,线程函数返回后,线程就终止了.但也有其它方式: 下面是DELPHI7中TThread类的声明(本文只讨论在Windows平台下的实现,所以去掉了所有有关Linux平台部分的代码): Code TThread类在Delphi的RTL里算是比较简单的类,类成员也不多,类属性都很简单明白,本文将只对几个比较重要的类成员方法和唯一的事件:OnTerminate作详细分析. 首先就是构造函数:
1 constructor TThread.Create(CreateSuspended: Boolean);
2 begin 3 inherited Create; 4 AddThread; 5 FSuspended := CreateSuspended; 6 FCreateSuspended := CreateSuspended; 7 FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), Create_SUSPENDED, FThreadID); 8 if FHandle = 0 then 9 raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]); 10 end; 虽然这个构造函数没有多少代码,但却可以算是最重要的一个成员,因为线程就是在这里被创建的. 在通过Inherited调用TObject.Create后,第一句就是调用一个过程:AddThread,其源码如下:
procedure AddThread;
begin InterlockedIncrement(ThreadCount); end; 同样有一个对应的RemoveThread:
procedure RemoveThread;
begin InterlockedDecrement(ThreadCount); end; 它们的功能很简单,就是通过增减一个全局变量来统计进程中的线程数.只是这里用于增减变量的并不是常用的Inc/Dec过程,而是用了 InterlockedIncrement/InterlockedDecrement这一对过程,它们实现的功能完全一样,都是对变量加一或减一.但它 们有一个最大的区别,那就是interlockedIncrement/InterlockedDecrement是线程安全的.即它们在多线程下能保证 执行结果正确,而Inc/Dec不能.或者按操作系统理论中的术语来说,这是一对"原语"操作. 以加一为例来说明二者实现细节上的不同: (因为BeginThread过程的参数约定只能用全局函数).下面是它的代码:
1 function ThreadProc(Thread: TThread): Integer;
2 var 3 FreeThread: Boolean; 4 begin 5 try 6 if not Thread.Terminated then 7 try 8 Thread.Execute; 9 except 10 Thread.FFatalException := AcquireExceptionObject; 11 end; 12 finally 13 FreeThread := Thread.FFreeOnTerminate; 14 Result := Thread.FReturnValue; 15 Thread.DoTerminate; 16 Thread.FFinished := True; 17 SignalSyncEvent; 18 if FreeThread then Thread.Free; 19 EndThread(Result); 20 end; 21 end; 虽然也没有多少代码,但却是整个TThread中最重要的部分,因为这段代码是真正在线程中执行的代码.下面对代码作逐行说明: DoTerminate方法的代码如下:
procedure TThread.DoTerminate;
begin if Assigned(FOnTerminate) then Synchronize(CallOnTerminate); end; 很简单,就是通过Synchronize来调用CallOnTerminate方法,而CallOnTerminate方法的代码如下,就是简单地调用OnTerminate事件:
procedure TThread.CallOnTerminate;
begin if Assigned(FOnTerminate) then FOnTerminate(Self); end; 因为OnTerminate事件是在Synchronize中执行的,所以本质上它并不是线程代码,而是主线程代码(具体见后面对Synchronize的分析). 执行完OnTerminate后,将线程类的FFinished标志设置为True.接下来执行SignalSyncEvent过程,其代码如下:
procedure SignalSyncEvent;
begin SetEvent(SyncEvent); end; 也很简单,就是设置一下一个全局Event:SyncEvent,关于Event的使用,本文将在后文详述,而SyncEvent的用途将在WaitFor过程中说明. 说完构造函数,再来看析构函数:
1 destructor TThread.Destroy;
2 begin 3 if (FThreadID <> 0) and not FFinished then begin 4 Terminate; 5 if FCreateSuspended then 6 Resume; 7 WaitFor; 8 end; 9 if FHandle <> 0 then CloseHandle(FHandle); 10 inherited Destroy; 11 FFatalException.Free; 12 RemoveThread; 13 end; 在线程对象被释放前,首先要检查线程是否还在执行中,如果线程还在执行中(线程ID不为0,并且线程结束标志未设置),则调用Terminate过程结束线程.Terminate过程只是简单地设置线程类的Terminated标志,如下面的代码:
procedure TThread.Terminate;
begin FTerminated := True; end; 所以线程仍然必须继续执行到正常结束后才行,而不是立即终止线程,这一点要注意. 以前面那个InterlockedIncrement为例,我们用CriticalSection(Windows API)来实现它:
Var
InterlockedCrit : TRTLCriticalSection; Procedure InterlockedIncrement( var aValue : Integer ); Begin EnterCriticalSection( InterlockedCrit ); Inc( aValue ); LeaveCriticalSection( InterlockedCrit ); End; 现在再来看前面那个例子: 关于临界区的使用,有一点要注意:即数据访问时的异常情况处理.因为如果在数据操作时发生异常,将导致Leave操作没有被执行,结果将使本应被唤醒的线程未被唤醒,可能造成程序的没有响应.所以一般来说,如下面这样使用临界区才是正确的做法:
EnterCriticalSection
Try // 操作临界区数据 Finally LeaveCriticalSection End; 最后要说明的是,Event和CriticalSection都是操作系统资源,使用前都需要创建,使用完后也同样需要释放.如 我们知道,Synchronize是通过将部分代码放到主线程中执行来实现线程同步的,因为在一个进程中,只有一个主线程.先来看看Synchronize的实现:
1 class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord);
2 var 3 SyncProc: TSyncProc; 4 begin 5 if GetCurrentThreadID = MainThreadID then 6 ASyncRec.FMethod 7 //首先是判断当前线程是否是主线程,如果是,则简单地执行同步方法后返回. 8 else begin 9 SyncProc.Signal := CreateEvent(nil, True, False, nil); 10 {通过局部变量SyncProc记录线程交换数据(参数)和一个Event Handle,其记录结构如下: 11 TSyncProc = record 12 SyncRec: PSynchronizeRecord; 13 Signal: THandle; 14 end; } 15 try 16 EnterCriticalSection(ThreadLock); 17 { 18 接着进入临界区(通过全局变量ThreadLock进行,因为同时只能有一个线程进入Synchronize状态,所以可以用全局变量记录) 19 } 20 try 21 {然后就是把这个记录数据存入SyncList这个列表中(如果这个列表不存在的话,则创建它).} 22 if SyncList = nil then SyncList := TList.Create; 23 // 24 SyncProc.SyncRec := ASyncRec; 25 SyncList.Add(@SyncProc); 26 { 再接下就是调用SignalSyncEvent,其代码在前面介绍TThread的构造函数时已经介绍过了,它的功能就是简单地将SyncEvent作一个Set的操作.关于这个SyncEvent的用途,将在后面介绍WaitFor时再详述.} 27 SignalSyncEvent; 28 {接下来就是最主要的部分了:调用WakeMainThread事件进行同步操作.WakeMainThread是一个TNotifyEvent类型的全局事件.这里之所以要用事件进行处理,是因为Synchronize方法本质上是通过消息,将需要同步的过程放到主线程中执行,如果在一些没有消息循环的应用中(如Console或DLL)是无法使用的,所以要使用这个事件进行处理.} 29 if Assigned(WakeMainThread) then WakeMainThread(SyncProc.SyncRec.FThread); 30 LeaveCriticalSection(ThreadLock); 31 //在执行完WakeMainThread事件后,就退出临界区 32 try 33 WaitForSingleObject(SyncProc.Signal, INFINITE); 34 {然后调用WaitForSingleObject开始等待在进入临界区前创建的那个Event.这个Event的功能是等待这个同步方法的执行结束,关于这点,在后面分析CheckSynchronize时会再说明.} 35 finally 36 EnterCriticalSection(ThreadLock); 37 end; 38 {注意在WaitForSingleObject之后又重新进入临界区,但没有做任何事就退出了,似乎没有意义,但这是必须的! 39 因为临界区的Enter和Leave必须严格的一一对应.那么是否可以改成这样呢: 40 if Assigned(WakeMainThread) then 41 WakeMainThread(SyncProc.SyncRec.FThread); 42 WaitForSingleObject(SyncProc.Signal, INFINITE); 43 f inally 44 LeaveCriticalSection(ThreadLock); 45 end; 46 上面的代码和原来的代码最大的区别在于把WaitForSingleObject也纳入临界区的限制中了.看上去没什么影响,还使代码大大简化了,但真的可以吗?事实上是不行! 47 因为我们知道,在Enter临界区后,如果别的线程要再进入,则会被挂起.而WaitFor方法则会挂起当前线程,直到等待别的线程SetEvent后才会被唤醒.如果改成上面那样的代码的话,如果那个SetEvent的线程也需要进入临界区的话,死锁(Deadlock)就发生了(关于死锁的理论,请自行参考操作系统原理方面的资料).死锁是线程同步中最需要注意的方面之一! 48 } 49 finally 50 LeaveCriticalSection(ThreadLock); 51 end; 52 finally 53 CloseHandle(SyncProc.Signal); 54 end; 55 //最后释放开始时创建的Event,如果被同步的方法返回异常的话,还会在这里再次抛出异常. 56 if Assigned(ASyncRec.FSynchronizeException) then 57 raise ASyncRec.FSynchronizeException; 58 end; 59 end; 这段代码略多一些,不过也不算太复杂. 而响应这个事件的是Application对象,下面两个方法分别用于设置和清空WakeMainThread事件的响应(来自Forms单元):
procedure TApplication.HookSynchronizeWakeup;
begin Classes.WakeMainThread := WakeMainThread; end; procedure TApplication.UnhookSynchronizeWakeup; begin Classes.WakeMainThread := nil; end; 上面两个方法分别是在TApplication类的构造函数和析构函数中被调用. 这就是在Application对象中WakeMainThread事件响应的代码,消息就是在这里被发出的,它利用了一个空消息来实现:
procedure TApplication.WakeMainThread(Sender: TObject);
begin PostMessage(Handle, WM_NULL, 0, 0); end; 而这个消息的响应也是在Application对象中,见下面的代码(删除无关的部分):
procedure TApplication.WndProc(var Message: TMessage);
… begin try … with Message do case Msg of … WM_NULL: CheckSynchronize; … except HandleException(Self); end; end; 其中的CheckSynchronize也是定义在Classes单元中的,由于它比较复杂,暂时不详细说明,只要知道它是具体处理Synchronize功能的部分就好. 回到前面CheckSynchronize,见下面的代码:
1 function CheckSynchronize(Timeout: Integer = 0): Boolean;
2 var 3 SyncProc: PSyncProc; 4 LocalSyncList: TList; 5 begin 6 //首先,这个方法必须在主线程中被调用(如前面通过消息传递到主线程),否则就抛出异常. 7 if GetCurrentThreadID <> MainThreadID then 8 raise EThread.CreateResFmt(@SCheckSynchronizeError, [GetCurrentThreadID]); 9 {接下来调用ResetSyncEvent(它与前面SetSyncEvent对应的,之所以不考虑WaitForSyncEvent的情况,是因为只有在Linux版下才会调用带参数的CheckSynchronize,Windows版下都是调用默认参数0的CheckSynchronize).} 10 if Timeout > 0 then 11 WaitForSyncEvent(Timeout) 12 else 13 ResetSyncEvent; 14 {现在可以看出SyncList的用途了:它是用于记录所有未被执行的同步方法的.因为主线程只有一个,而子线程可能有很多个,当多个子线程同时调用同步方法时,主线程可能一时无法处理,所以需要一个列表来记录它们.} 15 LocalSyncList := nil; 16 EnterCriticalSection(ThreadLock); 17 try 18 Integer(LocalSyncList) := InterlockedExchange(Integer(SyncList), Integer(LocalSyncList)); 19 try 20 Result := (LocalSyncList <> nil) and (LocalSyncList.Count > 0); 21 if Result then begin 22 {在这里用一个局部变量LocalSyncList来交换SyncList,这里用的也是一个原语:InterlockedExchange.同样,这里也是用临界区将对SyncList的访问保护起来.只要LocalSyncList不为空,则通过一个循环来依次处理累积的所有同步方法调用.最后把处理完的LocalSyncList释放掉,退出临界区.} 23 while LocalSyncList.Count > 0 do begin 24 {再来看对同步方法的处理:首先是从列表中移出(取出并从列表中删除)第一个同步方法调用数据.然后退出临界区(原因当然也是为了防止死锁).接着就是真正的调用同步方法了.} 25 SyncProc := LocalSyncList[0]; 26 LocalSyncList.Delete(0); 27 LeaveCriticalSection(ThreadLock); 28 try 29 try 30 SyncProc.SyncRec.FMethod; 31 except //如果同步方法中出现异常,将被捕获后存入同步方法数据记录中. 32 SyncProc.SyncRec.FSynchronizeException := AcquireExceptionObject; 33 end; 34 finally 35 EnterCriticalSection(ThreadLock); 36 {重新进入界区后,调用SetEvent通知调用线程,同步方法执行完成了(详见前面Synchronize中的WaitForSingleObject调用).} 37 end; 38 SetEvent(SyncProc.signal); 39 end; 40 end; 41 finally 42 LocalSyncList.Free; //等list的序列全部执行完后,释放list的资源 43 end; 44 finally 45 LeaveCriticalSection(ThreadLock); 46 end; 47 end; 至此,整个Synchronize的实现介绍完成. 最后来说一下WaitFor,它的功能就是等待线程执行结束.其代码如下:
1 function TThread.WaitFor: LongWord; 2 var 3 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论