DRT中面向异步程序结构的自动引用工具库

发布于 2022年 05月 19日 10:26

面向异步程序结构的自动引用工具库

本文是Delphi新功能点托管记录技术的科普文,适合还没吃透托管记录的人阅读。

为了配合高性能服务端要求的的异步代码结构,DRT中附带了一套自动引用工具,帮助实现以引用计数的方式进行内存生命周期管理,该工具包括有自动计数对象引用、自动计数指针引用以及任务同步器三套工具,这些工具所在的单元为DRT.YWTypes。DRT项目组中的TestAutoRef.exe项目(发文前刚上传Gitee, 请到Gitee拉取项目更新 YWtheGod/DRT (gitee.com)),就是这些工具的Demo代码。

自动计数对象引用

  • 类型:R<T>与WR<T>,目的为为所有类实例添加interface式的自动释放能力,再加上interface不具备的弱引用功能。
    R<T>代表对于实例的强引用,只有强引用计数减到零时,对象才会被释放,WR<T>代表对实例的弱引用,不会改变实例的引用计数,但是当实例被释放时弱引用的值会被设定为nil。
  • 相关Demo代码讲解: 
var A,B : R<TClassA>;  
    C : WR<TClassA>; 

此处为变量定义形式,A,B为关于TClassA类型的强引用变量,C为弱引用变量 

  D : TClassA;  
begin  
  Put('=======Object Auto Ref Demo Started==========');  
 ShowValue([A,B,C]);

此处打印列出ABC三个变量的初始值,可以看到虽然是函数局部变量,这些引用类型是具有自动初始化的功能的。   

  PUT;  
  Put('A := TClassA.Create;');  
  A := TClassA.Create;  
  ShowValue([A,B,C]);

这里演示对引用变量赋值的方式,就如同普通类变量一样使用即可   

  PUT;  
  B :=TClassA.Create;  
  A :=B;  
  ShowValue([A,B]);  

这里可以在MEMO的打印记录中看到,对A赋新值,导致A的旧引用实例会被释放 

  PUT;  
  Put('D:=A');  
  D := A;  
  ShowValue([A,B,C,D]);  

这里演示从引用变量取得常规类实例的方法,引用类型与对象类型,可以在参数中直接取代常规对象使用。 

  PUT;  
  PUT('C := B;');  
  C  := B;  
  ShowValue([A,B,C]); 

这里演示弱引用类型的赋值方法  

  PUT;  
  PUT('C.O.SayHello;');  
  C.O.SayHello;  

这里演示通过引用变量调用被引用对象的成员函数 

  PUT;  
  begin  
    var E : R<TClassA>;  
    PUT('E := C;');  
    E := C;  
    ShowValue([A,B,C,D,E]); 

这里演示从弱引用变量赋值到强引用变量,变量E的作用域是被限制在begin end之间 

    PUT;  
    PUT('A:=nil; B:=nil');  
    A:=nil; B := nil;  
    ShowValue([A,B,C,D,E]); 

A和B的引用都被解除,变量E是实例最后的引用,在其作用域退出后,将会自动解除引用

    PUT;  
    PUT('E end of life');  
  end;  
  ShowValue([A,B,C,D]); 

这里可以看到类实例的释放记录打印,同时可以看到随着实例释放,变量C自动被设定为nil,而常规类变量D无法感知实例的释放情况 

  PUT;  
  PUT('D.Free.......');  
  try  
    D.Free;  
  except on E: Exception do  
    PUT(E.Message);  
  end;  
  Put('========Object Auto Ref Demo Ended===========');  

调用D.free,抛出异常证实常规类变量D的地址值是无效的。

  • 自动计数引用技术注意事项:
    • 一个实际对象仅可以赋值给一个引用变量一次!多次赋值将会导致多个引用变量重复执行Free操作引发异常!
    • 一个实际对象赋值给一个引用变量之后,引用变量最终一定会调用这个对象的Free方法,所以,自行主动调用该对象的Free方法一定会导致重复Free!
    • 引用变量和弱引用变量都不是线程安全的!要在并发环境中对同一个引用或弱引用变量赋值必须加锁!
    • 引用变量适用于对象释放时机不可控场合,如果你有需要立即释放一些资源,例如关闭句柄,断开链接,断开数据库等等,不要通过destructor来进行资源释放操作,调用一个专门释放资源非destructor方法,然后在使用代码中加入判断资源是否有效的检测
    • 持有一个实例的弱引用可以对实例保持跟踪而又不会阻止实例的释放,当你需要操作被弱引用的实例时,正确的方法是将弱引用变量赋值给一个局部强引用变量,然后判断局部变量是否为nil, 如果为nil说明实例在其他场合已经被释放,如果不为nil,则可以通过局部强引用变量放心操作实例,在局部强引用变量离开其作用域之前,它将确保被引用实例不会被释放,范例代码:
  1. procedure MainForm.OnEvent(Sender : TObject);  
  2. var R : R<TMyClass>;  
  3. begin  
  4.   R := WR // a variable of WR<TMyClass>  
  5.   if R<>nil then begin  
  6.       R.O.DoSomeThing;  
  7.   end;  
  8. end;  

 

自动计数指针引用

  • 类型:REFPTR和WREFPTR,分别代表着强引用指针和弱引用指针。指针引用可以保护一片内存块,当引用计数减到零时,自动调用Freemem释放内存。
  • 相关Demo代码讲解:
var A,B : REFPTR;  
    C : WREFPTR;  
    D : Pointer;  

变量定义形式,REFPTR为强引用指针,WREFPTR为弱引用指针

begin  
  Put('=======Pointer Auto Ref Demo Started==========');  
  ShowMem;  
  A.Alloc(MEMSIZE);  
  ShowMem;  

这里通过两次对比内存占用情况,演示REFPTR类型的Alloc方法

  GetMem(D,MEMSIZE); B := D;  
  C := B;  
  ShowValue([A,B,C]);  

这里演示普通指针对REFPTR的赋值,以及REFPTR对WREFPTR的赋值,它们在作为参数传递给ShowValue函数时,都能够自动兼容Pointer类型

  ShowMem;  
  B := A;  
  ShowMem;  

第一次显示的内存占用为A,B各指向不同内存块时的占用,第二次时,B指向新内存块,旧引用计数降到零触发内存释放,从内存占用数值可以看出内存B的旧内存已经被释放

  ShowValue([A,B,C]);  

B和C原本指向同一片内存,C是弱引用所以不能阻止B释放内存,但是C可以感知到B的内存释放行为,其值已经自动变为nil。

  begin  
    var E: REFPTR  := A;  

变量E为作用域局限在begin end之间的强引用变量

    A := nil; B := nil;  
    ShowMem;  

A,B变量解除引用,但由于E保持了引用,内存占用情况并没有变化

    ShowValue([A,B,C,D,E]);  
  end;  
  ShowMem;  

变量E的作用域结束后,从内存占用情况可以看到E引用的内存已经被释放

  ShowValue([A,B,C,D]);  

传统的指针变量D依然指向一个地址,但是该地址已经被释放,D现在是一个野指针

  Put('========Pointer Auto Ref Demo Ended===========');  
  Put;  
end;  

 

  • 注意事项同样是那几点:
    • 一片内存只能赋值给一个指针引用一次,否则多个指针引用将会重复释放同一片内存
    • 一片内存赋值给指针引用后,就不能再被手动释放
    • 指针引用变量非线程安全,多线程对其赋值要加锁
    • 指针引用变量没有记录内存块的大小,想要知道内存块大小,要么向内存管理器查询,要么自己用其他途径记录并自行传递尺寸。

 

任务同步器

DRT提供的任务同步工具类型为TaskSyncer,它是一个带方法的托管记录体,所以使用时无须创建,它自带引用计数,会自动释放资源,提供了4个方法帮助进行线程同步:它的功能很简单,就是对待完成任务数量进行计数,当计数到0时在当前线程立即执行善后代码,从而完全避免了Waitfor机制。

  • procedure AddTask(const i : integer); overload; inline;
    添加需要等待完成的任务数量,这些任务数量将会被异步任务减一直到0后触发善后回调。
  • procedure OnAllDone(F : TProc); inline;
    设定所有任务完成后的回调操作,传入一个匿名函数,当所有任务计数为0时执行。
  • function Cancel(F: TProc):TTaskResult; inline;
    取消任务组,当任务计数器未达到0而且之前未被取消过,传入的匿名函数将会被执行,同时设定任务组为已取消状态。多次Cancel只有第一次调用传入的匿名函数会被执行。注意:取消任务组并不会中止任何异步操作的执行,每个异步操作都将按原来的计划执行,只是当它们完成后,将不再会触发OnAllDone的任务完成回调而已,要真正中止任何进行中的异步操作,请手动实现中止机制。
  • function DoneTask : TTaskResult; inline;
    标记完成了一个任务,任务计数器-1,当任务组未被取消且计数器减到0时,触发OnAllDone回调执行,该函数会返回任务组任务执行状态。

 

Demo代码如下:

var T : TaskSyncer;  
begin  
  Put('=======Task Syncer Demo Started==========');  
  T.OnAllDone(procedure begin  
    UIPut('All Task Done!');  
    UIPut('========Task Syncer Ref Demo Ended===========');  
    UIPut;  
  end); 

设定任务完成回调匿名函数 

  T.AddTask(20);  

预设待完成任务为20个

  for var i := 1 to 20 do begin  
    var F := procedure(i : integer) begin 

在匿名函数一文中提到过的将循环变量传递进内层匿名函数的小手段 

      TTask.Run(procedure begin  
        UIPut('Task '+i.ToString+' Start.....');  
        var a := random(3000)+200;

用随机数生成sleep的时间模拟不确定的任务执行时间  

        if a and 3 = 1 then begin  
          T.AddTask;

模拟在意外情况下临时新增任务的场景  

          TTask.Run(procedure begin  
            UIPut('Additional Task From Task '+i.tostring+' Running...');  
            Sleep(2000);  
            UIPut('Additional Task From Task '+i.tostring+' Done!');  
            T.DoneTask; 

新增任务也是并发执行,新增任务也要显式用DoneTask声明自己的任务已完成 

          end);  
        end;  
        sleep(a);  
        UIPut('Task '+i.ToString+' Done!');  
        T.DoneTask;  

用Sleep模拟任务执行过程,用DoneTask声明任务完成

      end);  
    end;  
    F(i);  
  end;  
end; 

函数直到结束也没有Waitfor任何信号,善后工作由最后一个完成任务的线程执行。 

推荐文章