Delphi中线程类TThread实现多线程编程2---事件、临界区、Synchronize、WaitFor……

   2023-02-09 学习力0
核心提示:  接着上文介绍TThread。  现在开始说明 Synchronize和WaitFor  但是在介绍这两个函数之前,需要先介绍另外两个线程同步技术:事件和临界区事件(Event)  事件(Event)与Delphi中的事件有所不同。从本质上讲,Event其实就相当于一个全局的布尔变量

  接着上文介绍TThread。

  现在开始说明 Synchronize和WaitFor

  但是在介绍这两个函数之前,需要先介绍另外两个线程同步技术:事件和临界区

事件(Event)

  事件(Event)与Delphi中的事件有所不同。从本质上讲,Event其实就相当于一个全局的布尔变量。它有两个赋值操作:Set和ReSet,相当于把它设置为 True或False。而检查它的值是通过WaitFor操作进行。对应在Windows平台上,是三个API函数:SetEvent、ResetEvent、WaitForSignalObject(实现WaitFor功能的API还有几个,这是最简单的一个)。

  这三个都是原语,所以Event可以实现一般布尔变量不能实现的在多线程中的应用。Set和Reset的功能前面已经说过了,现在来说一下WaitFor的功能:

  WaitFor的功能是检查Event的状态是不是为Set状态(相当于True),如果是则立即返回,如果不是,则等待它变为Set状态,在等待期间,调用WaitFor的线程处于挂起状态。另外WaitFor有一个参数用于超时设置,如果此参数为0,则不等待,立即返回Event的状态,如果是INFINITE则无线等待这,直到Set状态发生,若是一个有限的数值,则等待相应的毫秒数之后返回Event的状态

  当Event从Reset状态向Set状态转换时,唤醒其他由于WaitFor这个Event而挂起的线程,这就是他为什么叫Event的原因。所谓“事件”就是指“状态的转换”。通过Event可以在线程间传递这种“状态转换”信息。

  当然用一个受保护(见下面的临界区介绍——)的布尔变量也能实现类似的功能,只要用一个循环检查此布尔值的代码来代替WaitFor即可,从功能上说完全没有问题,但是实际使用中就会发现,这样会占用大量的CPU资源,降低系统性能,影响到别的线程的执行速度,所以是不经济的,有时候甚至可能出问题,所以不建议这么做

临界区(Critical Section)

  临界区则是一项共享数据访问保护的技术。它其实也是相当于一个全局的布尔变量。但对它的操作有所不同,它只有两个操作:Enter和Leave,同样可以把它的两个状态当做True和False,分别表示现在是否处于临界区中。这两个操作也是原语,所以它可以用在多线程应用中保护共享数据,防止访问冲突

  用临界区保护共享数据的方法很简单:在每次要共享数据之前调用Enter设置进入临界区标识,然后再操作数据,最后调用Leave离开临界区。它的保护原理是这样的:当一个线程进入临界区之后,如果此时另一个线程也要访问这个数据,则它会在调用Enter时,发现已经有线程进入临界区,然后此线程就会被挂起,等待当前在临界区的线程调用Leave离开临界区,当另一个线程完成操作时,调用Leave离开后,次线程就会被唤醒,并设置临界区标志,开始操作数据,这样就防止了访问冲突

  以前面那个InterlockedIncrement为例,我们用CriticalSection(Windows API)来实现它

var
    InterlockedCrit: TRTLCriticalSection;
procedure InterlockedIncrement(var aValue: Integer);
begin
    EnterCriticalSection(InterlockedCrit);
    Inc(aValue);
    LeaveCriticalSection(InterlockedCrit);
end;

  注意使用临界区的语法格式:先Enter临界区,再对某个数据进行操作,然后Leave临界区

  现在再来看前面的那个例子(在上篇博客里)

1)线程A进入临界区(假设数据为3)

2)线程B进入临界区,因为此时A已经在临界区中,所以B被挂起

3)线程A对数据加一(现在是4)

4)线程A离开临界区,唤醒线程B(现在内存中的数据是4)

5)线程B被唤醒,对数据加一(现在就是5)

6)线程B离开临界区,现在的数据就是正确的了

  临界区就是这样保护共享数据的访问

  关于临界区的使用,有一点要注意:即数据访问时的异常情况处理。因为如果在数据操作时发生异常,将导致Leave操作没有被执行,结果将使本应该被唤醒的线程未被唤醒,可能造成程序的没有响应。所以一般来说,如下面这样访问临界区才是正确的做法

EnterCriticalSection( InterlockedCrit );
try
    //操作临界区数据
finally
    LeaveCriticalSection( InterlockedCrit );
end;

  最后要说的是,Event和CriticalSection都是操作系统资源,使用前都需要创建,使用完成之后也同样需要释放。如

  TThread类用到的一个全局Event:SyncEvent 和全局CriticalSection: ThreadLock,都是在InitThreadSynchronization和 DoneThreadSynchronization中进行创建和释放的,而它们则是在Classes单元的Initialization 和 Finalization 中被调用的。

  由于在TThread中都是用API来操作Event和CriticalSection的,所以前面都是以API为例,其实Delphi已经提供它们的封装,在 SyncObjs单元中,分别是 TEvent类 和 TCriticalSection类。用法也和前面用API的方法相差无几。因为 TEvent的构造函数参数过多,为了简单起见,Delphi还提供了一个用默认参数初始化的 Event类: TSimpleEvent

  顺便再介绍一下另一个用于线程同步的类:TMultiReadExclusiveWriteSynchronizer, 它是在 SysUtils 单元里面定义的。据我所知,这是Delphi RTL中定义的最长的一个类名,还好它有一个短的别名: TMREWSync。至于他的用处,光看名字就可以知道了

 

 



 

有了前面对 Event和 CriticalSection的准备知识,可以正式开始讨论Synchronize和 WaitFor了。

  我们知道,Synchronize是通过将部分代码放到主线程中执行来实现线程同步的,因为在一个进程中,只有一个主线程,先来看看Synchronize的实现

procedure TThread.Synchronize(Method: TTheadMethod);
begin
    FSynchronize.FThread:= Self;
    FSynchronize.FSynchronize.Exception:= nil;
    FSynchronize..FMethod:= Method;
    Synchronize(@FSynchronize);
end;

  其中FSynchronize是一个记录类型:

PSynchronizeRecord= ^TSynchronizeRecord;
TSynchronizeRecord = record
    FThread: TObject;
    FMethod: TThreadMethod;
    FSynchronizeException: TObject;
end;

  用于进行线程和主线程之间进行数据交换,包括传入线程类对象,同步方法及发生的异常。

  在Synchronize中调用它的一个重载版本,而且这个重载版本比较特别,它是一个“类方法”,所谓类方法,是一种特殊的类成员方法,它的调用并不需要创建类实例,而是想构造函数那样,通过类名调用。之所以会用类方法来实现它,是因为为了可以在线程对象还没有被创建的时候也能调用它。不过实际中用它的是另外一个重载版本(也是类方法)和另一个类方法StaticSynchronize。

  下面是这个Synchronize的代码

class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord);
var
  SyncProc: TSyncProc;
begin
  if GetCurrentThreadID = MainThreadID then
    ASyncRec.FMethod
    // 首先是判断当前线程是否是主线程,如果是,则简单地执行同步方法后返回。
  else
  begin
    SyncProc.Signal := CreateEvent(nil, True, False, nil);
    { 通过局部变量SyncProc记录线程交换数据(参数)和一个Event Handle,其记录结构如下:
      TSyncProc = record
      SyncRec: PSynchronizeRecord;
      Signal: THandle;
      end; }
    try
      EnterCriticalSection(ThreadLock);
      {
        接着进入临界区(通过全局变量ThreadLock进行,因为同时只能有一个线程进入Synchronize状态,所以可以用全局变量记录)
      }
      try
        { 然后就是把这个记录数据存入SyncList这个列表中(如果这个列表不存在的话,则创建它)。 }
        if SyncList = nil then
          SyncList := TList.Create;
        //
        SyncProc.SyncRec := ASyncRec;
        SyncList.Add(@SyncProc);
        { 再接下就是调用SignalSyncEvent,其代码在前面介绍TThread的构造函数时已经介绍过了,它的功能就是简单地将SyncEvent作一个Set的操作。关于这个SyncEvent的用途,将在后面介绍WaitFor时再详述。 }
        SignalSyncEvent;
        { 接下来就是最主要的部分了:调用WakeMainThread事件进行同步操作。WakeMainThread是一个TNotifyEvent类型的全局事件。这里之所以要用事件进行处理,是因为Synchronize方法本质上是通过消息,将需要同步的过程放到主线程中执行,如果在一些没有消息循环的应用中(如Console或DLL)是无法使用的,所以要使用这个事件进行处理。 }
        if Assigned(WakeMainThread) then
          WakeMainThread(SyncProc.SyncRec.FThread);
        LeaveCriticalSection(ThreadLock);
        // 在执行完WakeMainThread事件后,就退出临界区
        try
          WaitForSingleObject(SyncProc.Signal, INFINITE);
          { 然后调用WaitForSingleObject开始等待在进入临界区前创建的那个Event。这个Event的功能是等待这个同步方法的执行结束,关于这点,在后面分析CheckSynchronize时会再说明。 }
        finally
          EnterCriticalSection(ThreadLock);
        end;
        { 注意在WaitForSingleObject之后又重新进入临界区,但没有做任何事就退出了,似乎没有意义,但这是必须的!
          因为临界区的Enter和Leave必须严格的一一对应。那么是否可以改成这样呢:
          if Assigned(WakeMainThread) then
          WakeMainThread(SyncProc.SyncRec.FThread);
          WaitForSingleObject(SyncProc.Signal, INFINITE);
          finally
          LeaveCriticalSection(ThreadLock);
          end;
          上面的代码和原来的代码最大的区别在于把WaitForSingleObject也纳入临界区的限制中了。看上去没什么影响,还使代码大大简化了,但真的可以吗?事实上是不行!
          因为我们知道,在Enter临界区后,如果别的线程要再进入,则会被挂起。而WaitFor方法则会挂起当前线程,直到等待别的线程SetEvent后才会被唤醒。如果改成上面那样的代码的话,如果那个SetEvent的线程也需要进入临界区的话,死锁(Deadlock)就发生了(关于死锁的理论,请自行参考操作系统原理方面的资料)。死锁是线程同步中最需要注意的方面之一!
        }
      finally
        LeaveCriticalSection(ThreadLock);
      end;
    finally
      CloseHandle(SyncProc.Signal);
    end;
    // 最后释放开始时创建的Event,如果被同步的方法返回异常的话,还会在这里再次抛出异常。
    if Assigned(ASyncRec.FSynchronizeException) then
      raise ASyncRec.FSynchronizeException;
  end;
end;

  这段代码略多一些,不过也不算复杂。

  可见ThreadLock这个临界区就是为了保护对SyncList的访问,这一点在后面介绍CheckSynchronize时会再次看到。

  而响应这个事件的是Application对象,下面两个方法分别用于设置和清空 WakeMainThread事件的响应(来自Forms单元)

procedure TApplication.HookSynchronizeWakeup;
begin
    Classes.WakeMianThread:= 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,见下面的代码

function CheckSynchronize(Timeout: Integer = 0): Boolean;
var
  SyncProc: PSyncProc;
  LocalSyncList: TList;
begin
  // 首先,这个方法必须在主线程中被调用(如前面通过消息传递到主线程),否则就抛出异常。
  if GetCurrentThreadID <> MainThreadID then
    raise EThread.CreateResFmt(@SCheckSynchronizeError, [GetCurrentThreadID]);
  { 接下来调用ResetSyncEvent(它与前面SetSyncEvent对应的,之所以不考虑WaitForSyncEvent的情况,是因为只有在Linux版下才会调用带参数的CheckSynchronize,Windows版下都是调用默认参数0的CheckSynchronize)。 }
  if Timeout > 0 then
    WaitForSyncEvent(Timeout)
  else
    ResetSyncEvent;
  { 现在可以看出SyncList的用途了:它是用于记录所有未被执行的同步方法的。因为主线程只有一个,而子线程可能有很多个,当多个子线程同时调用同步方法时,主线程可能一时无法处理,所以需要一个列表来记录它们。 }
  LocalSyncList := nil;
  EnterCriticalSection(ThreadLock);
  try
    Integer(LocalSyncList) := InterlockedExchange(Integer(SyncList),
      Integer(LocalSyncList));
    try
      Result := (LocalSyncList <> nil) and (LocalSyncList.Count > 0);
      if Result then
      begin
        { 在这里用一个局部变量LocalSyncList来交换SyncList,这里用的也是一个原语:InterlockedExchange。同样,这里也是用临界区将对SyncList的访问保护起来。只要LocalSyncList不为空,则通过一个循环来依次处理累积的所有同步方法调用。最后把处理完的LocalSyncList释放掉,退出临界区。 }
        while LocalSyncList.Count > 0 do
        begin
          { 再来看对同步方法的处理:首先是从列表中移出(取出并从列表中删除)第一个同步方法调用数据。然后退出临界区(原因当然也是为了防止死锁)。接着就是真正的调用同步方法了。 }
          SyncProc := LocalSyncList[0];
          LocalSyncList.Delete(0);
          LeaveCriticalSection(ThreadLock);

          try
            try
              SyncProc.SyncRec.FMethod;
            except // 如果同步方法中出现异常,将被捕获后存入同步方法数据记录中。
              SyncProc.SyncRec.FSynchronizeException := AcquireExceptionObject;
            end;

          finally
            EnterCriticalSection(ThreadLock);
            { 重新进入临界区后,调用SetEvent通知调用线程,同步方法执行完成了(详见前面Synchronize中的WaitForSingleObject调用)。 }
          end;
          SetEvent(SyncProc.signal);
        end;
      end;
    finally
      LocalSyncList.Free; // 等list的序列全部执行完后,释放list的资源
    end;
  finally
    LeaveCriticalSection(ThreadLock);
  end;
end;

  至此,整个Synchronize的实现介绍完成。 

  最后来说一下WaitFor,它的功能就是等待线程执行结束。其代码如下:
function TThread.WaitFor: LongWord;
var
  H: array [0 .. 1] of THandle;
  WaitResult: Cardinal;
  Msg: TMsg;
begin
  H[0] := FHandle;
  if GetCurrentThreadID = MainThreadID then
  begin
    WaitResult := 0;
    H[1] := SyncEvent;
    repeat
      { This prevents a potential deadlock if the background thread does a SendMessage to the foreground thread }
      if WaitResult = WAIT_OBJECT_0 + 2 then
        PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE);
      WaitResult := MsgWaitForMultipleObjects(2, H, False, 1000,
        QS_SENDMESSAGE);
      CheckThreadError(WaitResult <> WAIT_FAILED);
      if WaitResult = WAIT_OBJECT_0 + 1 then
        CheckSynchronize;
    until WaitResult = WAIT_OBJECT_0;
  end
  else
    WaitForSingleObject(H[0], INFINITE);
  CheckThreadError(GetExitCodeThread(H[0], Result));
end;

  如果不是在主线程中执行WaitFor的话,很简单,只要调用 WaitForSignalObject 等待此线程的Handle为Signaled状态即可

  如果是在主线程中执行WaitFor则比较麻烦。首先要在Handle数组中增加一个SyncEvent,然后循环等待,直到线程结束(即MsgWaitForMultipleObjects返回WAIT_OBJECT_0,详见MSDN中关于此API的说明)。

  在循环等待中作如下处理:如果有消息发生,则通过PeekMessage取出此消息(但并不把它从消息循环中移除),然后调用MsgWaitForMultipleObjects来等待线程Handle或SyncEvent出现Signaled状态,同时监听消息(QS_SENDMESSAGE参数,详见MSDN中关于此API的说明)。可以把此API当作一个可以同时等待多个Handle的WaitForSingleObject。如果是SyncEvent被SetEvent(返回WAIT_OBJECT_0 + 1),则调用CheckSynchronize处理同步方法。
  
  为什么在主线程中调用WaitFor必须用MsgWaitForMultipleObjects,而不能用WaitForSingleObject等待线程结束呢?因为防止死锁。由于在线程函数Execute中可能调用Synchronize处理同步方法,而同步方法是在主线程中执行的,如果用WaitForSingleObject等待的话,则主线程在这里被挂起,同步方法无法执行,导致线程也被挂起,于是发生死锁。
 
  而改用WaitForMultipleObjects则没有这个问题。首先,它的第三个参数为False,表示只要线程Handle或SyncEvent中只要有一个Signaled即可使主线程被唤醒,至于加上QS_SENDMESSAGE是因为ynchronize是通过消息传到主线程来的,所以还要防止消息被阻塞。这样,当线程中调用Synchronize时,主线程就会被唤醒并处理同步调用,在调用完成后继续进入挂起等待状态,直到线程结束。
  
  至此,对线程类TThread的分析可以告一个段落了,对前面的分析作一个总结:
1)线程类的线程必须按正常的方式结束,即Execute执行结束,所以在其中的代码中必须在适当的地方加入足够多的对Terminated标志的判断,并及时退出。如果必须要“立即”退出,则不能使用线程类,而要改用API或RTL函数。
2)对可视VCL的访问要放在Synchronize中,通过消息传递到主线程中,由主线程处理。
3)线程共享数据的访问应该用临界区进行保护(当然用Synchronize也行)。
4)线程通信可以采用Event进行(当然也可以用Suspend/Resume)。
5)当在多线程应用中使用多种线程同步
 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • Delphi中的消息处理机制 delphi 方法
    每一个VCL都有一内在的消息处理机制,其基本点就是构件类接收到某些消息并把它们发送给适当的处理方法,如果没有特定的处理方法,则调用缺省的消息处理句柄。    其中mainwndproc是定义在Twincontrol类中的一个静态方法,不能被重载(Override)。它不直接处
    02-09
  • Delphi XE6 通过JavaScript API调用百度地图
    Delphi XE6 通过JavaScript API调用百度地图
    参考昨天的内容,有朋友还是问如何调用百度地图,也是,谁让咱都在国内呢,没办法,你懂的。 首先去申请个Key,然后看一下百度JavaScript的第一个例子:http://developer.baidu.com/map/jsdemo.htm下一步,就是把例子中的代码,移动TWebBrower中。 unit Unit
    02-09
  • Delphi编译/链接过程 delphi编程案例
    Delphi编译/链接过程 delphi编程案例
    下面展示了Delphi是怎样编译源文件,并且把它们链接起来,最终形成可执行文件。当Delphi编译项目(Project)时,将编译项目源文件、窗体单元和其他相关单元,在这个过程中将会发生好几件事情:首先,Object Pascal编译器把项目单元编译为二进制对象文件,然后
    02-09
  • Delphi CompilerVersion Constant / Compiler C
    http://delphi.wikia.com/wiki/CompilerVersion_Constant The CompilerVersion constant identifies the internal version number of the Delphi compiler.It is defined in the System unit and may be referenced either in code just as any other consta
    02-09
  • Delphi revelations #1 – kbmMW Smart client
    Delphi 启示 #1 – kbmMW Smart client on NextGen (Android) – 作用域问题以更高级的方式使用kbmMW smart client,在Android设备上,我遇到了问题。通过继承TInvokeableVariantType,kbmMW smart client可以使用Delphi支持的特殊类型的自定义Variant,从而可
    02-09
  • Delphi 调用DLL外部函数时的指针参数
    某项目需要调用设备厂家提供的DLL的函数,厂家给了一个VB的例子,有个参数是ByRef pBuffer As Single。于是在Delphi中用buffer:array of single代替:function func(buffer:array of single;count:integer):integer;far;stdcall;external 'func.dll';调用后bu
    02-09
  • 《zw版·Halcon-delphi系列原创教程》 Halcon分
    《zw版·Halcon-delphi系列原创教程》 Halcon分类函数012,polygon,多边形为方便阅读,在不影响说明的前提下,笔者对函数进行了简化::: 用符号“**”,替换:“procedure”:: 用大写字母“X”,替换:“IHUntypedObjectX”:: 省略了字符:“const”、“OleVa
    02-09
  • 最简单的delphi启动画面(转)
    首先做一窗体,然后将BorderStyle的属性设为bsnone,放image控件,align设为alclient 然后将主程序的修改为 uses Windows, Forms, Unit1 in 'Unit1.pas' {Form1}, Unit2 in 'Unit2.pas' {Form2}; {$ R *.res} begin Application.Initialize; Form2:=TForm2.Cre
    02-09
  • Delphi备忘三:TCollection的使用,用Stream保
     代码unit ufrmGetFunctionDefine;interfaceuses  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,TypInfo,  Dialogs,ufrmStockBaseCalc, StdCtrls, ComCtrls,uQEFuncManager,uWnDataSet,uDataService;type  T
    02-09
  • Delphi Dcp 和BPL的解释
    dcp = delphi compiled package,是 package 编译时跟 bpl 一起产生出来的,记录着 package 中公开的 class、procedure、function、variable、const.... 等等的名称和相对位址。package英文翻译过来就是“包”。如果 某个控件包 A 引用了 控件包 B,当 控件包
    02-09
点击排行