首页 > 其他分享 >adfadf

adfadf

时间:2024-12-19 18:56:20浏览次数:2  
标签:begin end 协程 Create 窗体 线程 adfadf

{

把窗体从 tabsBar 上摘除后,也要清除一下,frm 与 mainForm 的关系;原因有2点:
1. 窗体与主窗体的 parent 和 owner 关系还是存在,frm.destroy的时候,TForm 又会利用 VisualManager(就是这个 FormtabsBar的接口指针)来调用
   procedure TTaskFormTabsBarTabs.DeleteForm(AForm: TForm); 由于 form我们已经手工移除了,手工移除的原因是我们需要自己来管理 form 何时释放,
  就是说 form不释放也不在 tab上展示 所以 它再次 delete的时候,肯定找不到,这里我们修改成找不到就跳过就行了,下面的代码这里就会报错;
  procedure TTaskFormTabsBarTabs.DeleteForm(AForm: TForm);
  var
    LIndex: Integer;
  begin
    LIndex := IndexOf(AForm); //这里会得到 - 1,从而 Delete 报错,不清除关系的话,子窗体.destroy又会调用一次这个;
    Delete(LIndex);
    TabsBar.Invalidate;
  end;

2. 若不清除关系,main窗体释放的时候,会自动连带释放这些窗体,导致这些窗体的释放,失去了我们自己协程机制的控制,glTaskFormMap容器会存在2种
   类型的窗体;A. 我们自己管理,非 tab;B. tab主窗体连带释放。从而导致各种问题;

----

总结:根据正规的浏览器逻辑,这里就应该剔除 选项卡与 主窗体的关系;

}

{

把窗体从 tabsBar 上摘除后,也要清除一下,frm 与 mainForm 的关系;原因有2点:
1. 窗体与主窗体的 parent 和 owner 关系还是存在,frm.destroy的时候,TForm 又会利用 VisualManager(就是这个 FormtabsBar的接口指针)来调用
   procedure TTaskFormTabsBarTabs.DeleteForm(AForm: TForm); 由于 form我们已经手工移除了,手工移除的原因是我们需要自己来管理 form 何时释放,
  就是说 form不释放也不在 tab上展示 所以 它再次 delete的时候,肯定找不到,这里我们修改成找不到就跳过就行了,下面的代码这里就会报错;
  procedure TTaskFormTabsBarTabs.DeleteForm(AForm: TForm);
  var
    LIndex: Integer;
  begin
    LIndex := IndexOf(AForm); //这里会得到 - 1,从而 Delete 报错,不清除关系的话,子窗体.destroy又会调用一次这个;
    Delete(LIndex);
    TabsBar.Invalidate;
  end;

2. 若不清除关系,main窗体释放的时候,会自动连带释放这些窗体,导致这些窗体的释放,失去了我们自己协程机制的控制,glTaskFormMap容器会存在2种
   类型的窗体;A. 我们自己管理,非 tab;B. tab主窗体连带释放。从而导致各种问题;

----

总结:根据正规的浏览器逻辑,这里就应该剔除 选项卡与 主窗体的关系;

}

unit zx.core.Task;

interface

uses
System.SyncObjs, System.Generics.Collections, Vcl.Forms,
System.Rtti, System.Classes, System.Generics.Defaults, System.DateUtils,
Winapi.Windows, System.SysUtils, Vcl.Dialogs, vcl.FormTabsBar, System.UITypes,
Vcl.Controls;

type

///


/// 类的向前声明,避开类间交叉引用的问题。举例:TTskForm 与 TTask 相互交叉。
/// Delphi有时候难以做到一个类一个单元,举例:类A有成员依赖类B,类B有成员
/// 依赖类A,则会造成单元的循环引用,无法编译,解决方法通常用3个:
/// 1.把类A和类B放进同一个单元里(此时依然会有顺序问题),然后使用预声明,向前声明下就可。
/// 2.合并2个类为1个。
/// 3.使用接口。
/// 这3个方法都不咋的,都是增加复杂度的做法。但是没有办法,目前Delphi就是这样,一个单元你,
/// 可以理解为一个命名空间或包。这里我采用了方法1,最大化简化这个协程库的使用。
/// 另外留意:parent: TSelf 也需要向前声明,就是说 一个类 需要知道它父节点的指针,如TTask类的parent: TTask,
/// 也需要TTask向前声明;
///

TTask = class;

///


/// 全局过程,即全局过程,类的静态过程也是全局
///

TProcGlb = procedure(task: TTask);

///


/// 类的过程,即 TXxxx.DoSomeThing;
///

TProcObj = procedure(task: TTask) of object;

///


/// 匿名的过程,可以直接在代码里写过程,不用提前声明;
/// 不再支持,避免局部变量的生命周期复杂化,在有UI的任何语言中,闭包容易产生复杂问题,
/// 直接不支持降低出问题的概率,提升代码的可读性。
//TProcAnonymous = reference to procedure(context: TContext);

///


/// 处理全局异常的过程,只提供一个全局函数版本,可以联网提交错误到服务端,不去细分异常名
/// 异常的处理往往的步骤:
/// 1. 由于异常,通常需要在终端提示出来异常,让用户知道发生了异常;
/// 2. 异常需要提交到服务端分析;
/// 总结:这个全局异常函数,会直接在主线程里运行,因为毕竟发生了异常,弹窗提示又涉及到UI线程安全;
/// 只要是个窗体,无论是dialog还是普通的窗体都不是线程安全的,因为dialog也具有点击关闭等鼠标操作
/// 这些鼠标操作的消息也是进入主线程的消息队列中的,故线程不安全,全局异常只能在主线程里运行,
/// 这么做干脆利索,防止开发者写出在后台线程中 Application.ShowException(e)的代码;
/// 具体使用场景:
/// 1.(普通)后台wk线程遇到异常,---> TThread.Synchronize()同步的方式到主线程 ---> Application.ShowException(e)
/// 2.(eurekalog)后台wk线程遇到异常,---> TThread.Synchronize()同步的方式到主线程 ---> Application.ShowException(e)
/// ---> eurekalog 发现主线程里有这样的语句,就会自动弹出eurekalog的异常窗体而不是官方的默认的异常窗体;
///

TExceptionHandler = procedure(e: Exception);

///


/// 协程类型枚举;
/// 并发编程的3种过程,并发方法没有意义,因为要返回值有什么作用,若一个协程
/// 的执行,依赖于另一个协程的返回值,说明2个协程之间变成了串行化,需要把这2
/// 个协程合并成一个,要保证协程的最小粒度合理性;
///

EProKind = (PK_GLB, PK_OBJ); //PK_ANONYMOUS

///


/// 协程类别
///

ETskKind = (TK_ROOT, TK_BG, TK_UI);

///


/// 线程的4种状态, 1(运行中)、2(无限等待)、3(有限等待)、4(程序关闭,已经退出)
///

/// EThreadState = (ACTIVE, WAIT_INFINITE, WAIT_TIMEOUT, EXITED);

///


/// 携带协程上下文的Form,所有Form必须继承这个,
/// 1. 窗体的创建和释放必须在主线程里进行;因为创建的时候会创建很多UI有关的组件,如:画刷;
/// 2. 窗体的释放也是必须在主线程里进行,因为举例主线程A 对应的窗体消息队列里有很多此窗体的
/// 消息,后台线程B把此窗体释放掉了,那么会导致主线程A拿到消息后,找不到窗体;若是在主线程
/// 里释放窗体,主线程会取消消息队列里此窗体对应的所有消息,这是VCL内部的机制;
/// 3. 只能通过 Create(AOwner: TComponent) + parent := xxx; 来创建窗体,故而 这2个地方要处理下
/// aowner 必须为 nil、TTaskForm、控件(如panel,但要求 panel.owner 必须为 TTaskForm)
/// parent 必须为 nil、控件(如panel,但要求 panel.owner 必须为 TTaskForm); parent := TTaskForm 或 TForm 没有意义 禁止
///

TTaskForm = class(TForm)
private
///
/// 窗体GUID
///

id: string;

/// <summary>
/// 释放中,当开发人员调用 free后,由于有 task 节点,当前窗体会被标记为释放中
/// 等待主线程回收完对应的 task列表时,回收掉这个窗体,原子操作读取,避免多核问题
/// </summary>
isFreeIng: Boolean;

/// <summary>
/// 当前Form作用域下的协程;
/// 1.gc负责删除;
/// 2.其它线程新增,故枷锁;
/// </summary>
tasks: TDictionary<string, TTask>;
tasksLock: TCriticalSection;

public
///


/// TForm的创建,共计5个方法,参见:https://www.cnblogs.com/del88/p/18576411;https://www.cnblogs.com/findumars/p/4751820.html
/// 1. constructor Create(AOwner: TComponent); override;
/// 2. constructor CreateParented(ParentWindow: HWnd);
/// 3. class function CreateParentedControl(ParentWindow: HWND): TWinControl;
/// 4. constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0); virtual;
/// 5. constructor CreateScaledNew(AOwner: TComponent; ADPI: Integer = 0; Dummy: Integer = 0); virtual;
/// ---------------------------------------------------------------------------------------------------------
/// 1. 只允许使用 Create 这个基本方法来创建窗体 + TControl.SetParent 来设置父窗口;
/// 2. CreateParented、CreateParentedControl 这2个方法 都是 FParentWindow := ParentWindow;
/// 调用了 procedure SetParentWindow(Value: HWND); 这个方法的主要作用是将控件嵌入到一个不由 VCL 管理的窗口中,
/// 那么可能会使用 TWinControl.SetParentWindow。但是,在使用这个方法之前,你应该非常清楚你在做什么,因为它可
/// 能会引入一些复杂的问题。大多数情况下使用 TControl.SetParent:如果你正在处理 VCL 控件,并且需要改变控件的
/// 父控件,应该使用 TControl.SetParent。这个方法更安全,因为它保持了 VCL 控件层级的一致性和正确性; 所以这2个
/// 方法也是废弃,禁止使用;
/// 3. CreateNew、CreateScaledNew 这2个方法显示不正常,看博客,也是废弃,禁止用;
/// ----------------------------------------------------------------------------------------------------------
/// 当不是虚方法你想屏蔽的时候,需要满足以下条件:
/// 1. 此单元内,必须使用到这个方法;Free显然是满足的;只有这一个条件,说明:
/// TObject.Free; 不是虚方法;子类重新定义,并变更为 private;不会报任何错;
/// Free方法 就是调用父类的Free,然后父类会最终调用子类重写的Destroy;
/// 总结:只要父类不是虚方法的,这样搞下都没有问题,其他单元都无法调用,方便屏蔽一些方法;
/// ---------------------------------------------------------------------------------------------------------
/// 虚方法的要求就比较多了:
/// 1.子类若定义重名,则必须覆盖 使用 overide 关键字,否则报错,
/// 提示虚方法被隐藏;是warling级别的错误;
/// 2. 你加上overide后,他发现你变更了作用域为private也会报错,是hint级别的错误;
/// 【既然虚方法无法变更作用域,那就不能搞了】,故虚方法放弃屏蔽;
/// 3. 此单元必须使用到这个方法; FreeInstance、FreeOnRelease 等此单元根本用不到,所以
/// 放弃屏蔽这些方法;
/// ---------------------------------------------------------------------------------------------------------
/// constructor Create(AOwner: TComponent); override;
/// constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0); virtual;
/// constructor CreateScaledNew(AOwner: TComponent; ADPI: Integer = 0; Dummy: Integer = 0); virtual;
/// TCustomForm 的三个构造函数都是虚方法,所以无法屏蔽,这里留意下;
/// TObject.Create 和 Free 都不是虚方法,且都用到了,所以都可以屏蔽;
/// 但是TCustomForm官方已经屏蔽了Create只提供了三个带参的构造,所以TTaskForm没有必要去管Create了;
/// TTask可以屏蔽下Create因为是直接从TObject继承过来的;且也用到了Create;
///

constructor Create(AOwner: TComponent); override;
constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0); override; //禁止使用
constructor CreateScaledNew(AOwner: TComponent; ADPI: Integer = 0; Dummy: Integer = 0); override; //禁止使用
constructor CreateParented(ParentWindow: HWnd); //禁止使用
class function CreateParentedControl(ParentWindow: HWND): TWinControl; //禁止使用

/// <summary>
/// 只能通过 Create(AOwner: TComponent) + parent := xxx; 来创建窗体>,parent := xxx; 调用的就是这个虚方法
/// 我们出来下:AParent 必须为 A. nil、TWinControl(或其他控件,但 TWinControl.owner 必须为 TTaskForm)
/// </summary>
procedure SetParent(AParent: TWinControl); override;

/// <summary>
/// 重新定义下 Free,这个free是个假 free,开发者调用的是这个新的,这个重写的 free
/// 不去调用父类的 .free;最终不调用 destroy;
/// 1. 修改下当前窗体的状态 isFreeIng = true;
/// 2. 若有task则取消;
/// 3. 若有子窗体,则递归向下 做 步骤1 和 步骤2
/// 然后什么都不做,以后 gc回收完task时会通知 main,然后 main会调用 self.destroy这样的流程来释放;
/// </summary>
procedure Free;

/// <summary>
/// 析构,释放此作用域下的协程
/// </summary>
destructor Destroy; override;

end;

{
//----- 1.0 代码我已经备份到自己的博客,主要是 新建 zx.core.TaskFormTabsBar 单元,然后... ------------

1. 通过 delphi ide 全选 -- 批量修改,把类名 前都加上 Task前缀 与 官方的区分开,方便以后有问题好排查;
2. procedure TCustomTaskFormTabsBar.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
   修改这个方法
3. TTaskFormTabsBarTabs.DeleteForm(AForm: TForm); 修改这个方法

//----

2.0 更加方便维护,直接继承,然后覆盖一个用户点击关闭按钮的方法,省得官方升级 TFormTabsBar 时,还得重新看下
zx.core.TaskFormTabsBar 的问题;

}
TTaskFormTabsBar = class(TFormTabsBar)
public
///


/// 点击关闭按钮时,会跳到这个虚方法上,覆盖这个虚方法;
/// 官方的这个方法,仅仅是做了一个 close动作,close 就是 触发 用户自定义的 onclose + hide当前窗体
///

procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
end;

///


/// 协程是一个杂合体
///

TTask = class
private
///
/// 协程ID,全局唯一,不能简单的使用integer(相对于父节点唯一),
/// 因为还有作用域这个维度,若相对于父节点唯一,那一个作用域对应多个协程
/// 就不好搞了,所以要全局唯一;
/// 创建此协程的时候自动生成,子节点运行完毕后,要根据这个ID,
/// 在父节点的child里删除自己的指针,还要在作用域里容器里删除指针;
///

id: string;

/// <summary>
/// 父节点指针
/// </summary>
parent: TTask;

/// <summary>
/// 协程类型,只读,创建的时候指定;
/// </summary>
tkind: ETskKind;

/// <summary>
/// 过程类型,只读,创建的时候指定;
/// </summary>
pkind: EProKind;

/// <summary>
/// 协程过程体
/// </summary>
procGlb: TProcGlb;
procObj: TProcObj;

/// <summary>
/// 协程状态,这里会多线程短暂访问,原子操作读取,避免多核问题
/// 1. new - canceling - canceled; 做前被其它线程释放,接活的线程修改为 canceled
/// 2. new - doing - canceling - canceld; 做中退出函数,做完判断发现是canceling,修改为canceled
/// 3. new - doing - completed 正常走完
/// 由于这个流程 是一致朝向终态来进行走的,不会逆转 倒退,所以不用担心 什么问题,不加锁;
/// -------------------------
/// 1.协程处理的过程中,需要读取这个值,来判断当前协程是否已取消,从而及时退出自己。
/// 2.父协程取消时,需要修改子协程的这个值为canceling;
/// 3.作用域销毁时,需要修改对应协程的这个值为canceling;
/// </summary>
state: Integer;

/// <summary>
/// 携带的数据,变量名(String) --> 数据类型(任何类型,自己在过程内部转换),只读;
/// 不要在协程中修改变量的值,一个协程独享一个data,而不是所有的协程共享一个data.
/// data不涉及到多线程写操作,仅仅是多线程读,且支持向上回溯,子协程中不存在这个
/// 变量名时,去向上查找父节点携带的数据是否包含这个变量名,那么回溯就是一个单链表的形式,
/// 你会认为查找的效率低,不如直接全部协程共享一个data,且若全部协程共享一个data,
/// 那么会导致这个data必须加锁,最终会导致整个【协程树】的执行 全部变成了串行化,
/// 效率会更低。故一个协程独享一个data。
/// 【注意,禁止对data里的数据进行修改,否则会有多线程的问题,我这边也会尽最大努力,
///  想办法从语法上,杜绝对data的修改】
///  data 并不加锁,data是创建后,多线程只读的,应避免在协程内部对data的修改操作。
///  delphi里 字段名与方法名不可重名,故命名为FData,来避开方法名使用data的问题;
/// </summary>
FData: TDictionary<string, TValue>;

/// <summary>
/// 子节点指针; 常见如下:
/// 1.当前协程,运行的过程中可能不停的生产子协程,需要做增加操作;
///
/// 2.生产出来的子协程,立即被其它线程拿到执行,执行完毕后,
///   垃圾回收器需要在父节点的childs里根据ID消除自己指针,做删除操作。
///
/// 3.由于父节点被取消了,那么需要读这个变量来获取子节点指针。
///  ===================
/// 总结:这里通常都是多线程的写操作,这里用自旋锁
/// </summary>
childs: TDictionary<string, TTask>;
childsLock: TCriticalSection;

/// <summary>
/// 协程作用域,一个协程对应可以对应多个作用域;不可使用owner,因为要使用
/// owner要求TTask必须继承TCompnent,且不能做到完全可控,没有必要放弃,因为
/// Form里的UI可视组件都已经是可视,禁止动态创建,而是要采用拖拽的方式;
/// 1. 协程创建的时候,统一指定好的;此时只有一个线程访问;
/// 2. 协程释放的时候,是GC遍历这个来找formx,只有GC一个线程访问,并释放;
/// 故这个不存在多线程的问题,不需要加锁;
/// </summary>
FScopes: TList<TTaskForm>;

/// <summary>
/// 重复协程的标记,重复协程衍生出来的协程(bg/ui)全部具有这个重复标记,
/// 这样子协程在创建的时候可以判断下父协程这个标记,避免子协程里再次衍生
/// 出重复协程,就是说重复协程禁止嵌套,这个标记的作用仅仅是避免重复协程
/// 嵌套,协程树上只有立即执行的协程,rpt协程是一个单独的队列;
/// </summary>
isRpt: Boolean;

/// <summary>
/// 重复协程的固定时间间隔,上一次协程【运行成功后】 --- 间隔N毫秒 --- 再次执行
/// 这里是上一次协程运行成功后,间隔多久再次执行,是智能的;
/// 而不是死板的间隔(上次的还未运行完,又开始新的)
/// </summary>
rptInterval: Int64;

/// <summary>
/// 上一次运行完毕后,通过 + Interval 计算得出的下一次运行时间,精确到毫秒
/// </summary>
nextTime: Int64;

/// <summary>
/// 成功回调相关
/// </summary>
isExistOnComplete: Boolean; //是否存在成功回调
onCompleteTKind: ETskKind; //回调也是一个协程,需要这个协程的运行类型 bg/ui
onCompletePKind: EProKind; //协程方法类型
onCompleteGlb: TProcGlb;
onCompleteObj: TProcObj;

/// <summary>
/// 取消回调相关
/// </summary>
isExistOnCancel: Boolean; //是否有取消回调
onCancelTKind: ETskKind; //回调也是一个协程,需要这个协程的运行类型 bg/ui
onCancelPKind: EProKind; //协程方法类型
onCancelGlb: TProcGlb;
onCancelObj: TProcObj;

/// <summary>
/// 切记这里不需要异常回调相关
/// ,因为每个节点分支出来的成功回调和取消回调都可能不同,
/// 但每个节点分支出来的异常节点都相同,我们采用全局异常
/// 处理的方式,简单明了,且异常直接只放到主线程来执行,
/// 即避开了异常弹出框UI的问题,也方便了eurekalog的使用,
/// 因为eurekalog默认就是主线程的异常,后台线程抛出异常是被
/// 忽略的,异常需要展示在主线程中;
/// </summary>
// isExistOnException: Boolean;......

/// <summary>
/// 若是bg协程,当前协程所在的bg线程索引,方便 .start 衍生子协程时,写入自己的本地队列
/// 默认这个是 -1 ;之所以默认是负数是为了避免逻辑漏洞;让其故意为负数; ui协程的时候是用不到这个的
/// -----------------------------
/// 1. main、df 线程,在 gotoBgWaitQueue 那里,写入这个值;
/// 2. wk线程从全局队列获取的时候,写入这个值;
/// 3. wk线程从同事那里偷的时候,改变这个值成自己的线程索引;
/// 4. wk线程 自己衍生子任务时 bg.bg.start那里,写入这个值,如自己的本地队列;
/// </summary>
wkThreadIndex: Integer;

/// <summary>
/// 新建协程节点,start前的临时指针,方便构建协程,最后start
/// </summary>
forkTemp: TTask;

/// <summary>
/// 输出一个无参的过程,方便在主线程里执行,最终还是调用了有参的方法
/// </summary>
procedure outNoArgProc;

/// <summary>
/// 当不是虚方法你想屏蔽的时候,需要满足以下条件:
/// 1. 此单元内,必须使用到这个方法;Free显然是满足的;
/// 只有这一个条件,说明:
/// TObject.Free; 不是虚方法;
/// 子类重新定义,并变更为 private;不会报任何错;
/// Free方法 就是调用父类的Free,然后父类会最终调用子类重写的Destroy;
/// 总结:只要父类不是虚方法的,这样搞下都没有问题,其他单元都无法调用,方便屏蔽一些方法;
/// ===============================
/// 虚方法的要求就比较多了:
/// 1.子类若定义重名,则必须覆盖 使用 overide 关键字,否则报错,
///    提示虚方法被隐藏;是warling级别的错误;
/// 2. 你加上overide后,他发现你变更了作用域为private也会报错,是hint级别的错误;
///    【既然虚方法无法变更作用域,那就不能搞了】,故虚方法放弃屏蔽;
/// 3. 此单元必须使用到这个方法; FreeInstance、FreeOnRelease 等此单元根本用不到,所以
///    放弃屏蔽这些方法;
/// ================================
/// constructor Create(AOwner: TComponent); override;
/// constructor CreateNew(AOwner: TComponent; Dummy: Integer  = 0); virtual;
/// constructor CreateScaledNew(AOwner: TComponent; ADPI: Integer = 0; Dummy: Integer = 0); virtual;
/// TCustomForm 的三个构造函数都是虚方法,所以无法屏蔽,这里留意下;
/// TObject.Create 和 Free 都不是虚方法,且都用到了,所以都可以屏蔽;
/// 但是TCustomForm官方已经屏蔽了Create只提供了三个带参的构造,所以TFormX没有必要去管Create了;
/// TTask可以屏蔽下Create因为是直接从TObject继承过来的;且也用到了Create;
/// </summary>
procedure Free;
constructor Create; //这里仅提供一个简单的构造,只包含 bg/ui协程 共有的必须参

public
///


/// 析构函数 - 放到public区域,否则编译器报错
///

destructor Destroy; override;

/// <summary>
/// 设置全局异常处理函数,只需要在一个地方设置一次就可以了,这个是全局的,并不是一个
/// 协程一个异常处理函数,而是全部协程有异常 都是这个处理函数,所以只需在一个地方设置一次
/// </summary>
procedure setGlobalExceptionHandler(handler: TExceptionHandler);

/// <summary>
/// 协程是否取消,针对耗时较长的协程,开发者应该在协程内部代码里
/// 多插入一些判断当前协程取消状态的代码,方便父协程取消时、或作用域关闭时能及时
/// 快速响应退出此协程函数。这个方法主要是供开发者在自己写的方法中判断当前协程的
/// 取消状态是否被外部力量干涉,如父协程取消、作用域销毁。
/// </summary>
function isCancel: Boolean;

/// <summary>
/// 协程过程内部获取携带数据的方法
/// </summary>
/// <param name="key">变量名</param>
function getString(key: string): string;
function getInteger(key: string): Integer;
function getBoolean(key: string): Boolean;
function getInt64(key: string): Int64;
function getUInt64(key: string): UInt64;
function getCurrency(key: string): Currency;
function getExtended(key: string): Extended;

/// <summary>
/// 创建一个立即运行的子协程;
/// </summary>
/// <returns>返回创建后的子协程</returns>
function bg(proc: TProcGlb): TTask; overload;
function bg(proc: TProcObj): TTask; overload;
function ui(proc: TProcGlb): TTask; overload;
function ui(proc: TProcObj): TTask; overload;

/// <summary>
/// 设置协程的携带数据,只支持基本类型
/// </summary>
function data(data: TArray<TValue>): TTask;

/// <summary>
/// 设置协程的作用域,一个协程可以多个作用域
/// </summary>
function scopes(scopes: TArray<TTaskForm>): TTask;

/// <summary>
/// 设置重复间隔的毫秒数,重复协程,是运行成功后间隔多久再次执行;
/// 重复协程只能从根部分支
/// </summary>
function interval(interval: Int64): TTask;

/// <summary>
/// 运行协程,返回协程ID,方便外部通过协程ID来取消 协程链条
/// </summary>
function start: string;

end;

//==================== 公开的变量 =============================
var
///


/// 协程上下文 - 根部
///

task: TTask;

procedure Register; // 注册TTaskFormTabsBar

implementation

//====================== 不需要公开的类 ======================

type

///


/// 后台线程基类
/// 程序只有主线程和后台线程,后台线程根据业务逻辑,
/// 分为各种做不同事的线程类型
///

TBaseThread = class(TThread)
private
///
/// 给线程起个名字方便调试的时候 OutputDebugString 查看
///

name: string;

/// <summary>
/// 后台线程事件,尝试过使用 TSemaphore 但是它类似停车场闸机处的保安模式,里面有些内部
/// 自动化设计,会难以控制最终结果,因为一切都在变化,而这些变化依赖于一个外部的协程队列,
/// 而不是内部的线程主动请求授权。不如Event来的简单粗暴,易控制排查问题,Event类似Java
/// NotifyAll
/// </summary>
//event: TEvent; 废弃,而是采用 更实用性的 自我睡眠 自我醒来,因为这样省去了 多线程之间的event呼叫,
//更节省资源,虽然实时性下降,但是真正干活的时候,没有把CPU资源花费在不停的notif_event同步上,更节省资源,
//能干出更多的活,更具有实用性;

/// <summary>
/// 当前线程的状态, 1(运行中)、2(无限等待)、3(有限等待)、4(已经退出)
/// </summary>
//state: EThreadState; 废弃,线程自我睡眠,自我醒来,外界不需要知道线程的状态

public
///


/// 构造与析构
///

constructor Create(name: string);
end;

///


/// defer推迟线程,专一处理未来会发生的事,通常是在睡觉状态,根据链表的头元素,决定睡觉的时间;
/// 特点:
/// 1. 没有任务的时候,死等;
/// 2. 有任务的时候,根据头任务来决定睡多久醒来;
///

TRptThread = class(TBaseThread)
private
///
/// 未来线程通常会很闲,若一个项目没有重复任务,则没有必要频繁醒来了,
/// 故这里使用同步事件,有任务进来的时候,通知一下他醒来,工人线程是
/// 若使用Tevent,频繁通知的话会浪费更多的资源,而未来线程是若不用事件
/// 频繁醒来会浪费更多的资源,未来线程不使用sleep而是使用waitfor(头元素时间)
///

event: TEvent;
protected
constructor Create(name: string);
destructor Destroy; override;
procedure Execute; override;
end;

///


/// worker后台工人线程
///

TWkThread = class(TBaseThread)
private
///
/// 当前线程在 wkThreads里的索引,方便从同事那里偷协程的
/// 时候,跳过这个索引,不能偷自己的
///

index: Integer;

/// <summary>
/// 本地等待队列
/// </summary>
lcWaitQueue: TQueue<TTask>;
lcWaitQueueLock: TCriticalSection;

/// <summary>
/// 本地完成队列,用map 是因为需要从中间删除;
/// </summary>
lcDoneMap: TDictionary<string, TTask>;
lcDoneMapLock: TCriticalSection;

protected
///


/// 定义新构造,index - 队列容器索引
///

constructor Create(name: string; index: Integer);
destructor Destroy; override;
procedure Execute; override;
end;

///


/// 垃圾回收线程,为了【避免多个线程同时去遍历、递归释放一颗协程树】,导致不必要的锁竞争。
/// 后台线程专一去做事,而不用管做完后,内存释放的问题,内存释放的问题统一交给一个单独
/// 的线程来处理,避开freeLock锁竞争,提升效率。这里不要在犯错,切记,否则功亏一篑。
/// 这里犯了2次错误,导致花费一周的时间,思路才又回到原点,【不要指望】后台线程 + GC线程
/// 双方配合来释放协程,他俩配合的结果 本身就是矛盾的,会陷入思维地狱,锁竞争地狱。应该
/// 各司其职,BG线程只干事,干完就交给GC,GC线程负责根据协程的父子关系,释放协程。【释放
/// 协程这件事,千万不要再让BG线程进来,无论是直接进来还是间接进来都不可,切记!!!!】
///

TGcThread = class(TBaseThread)
protected
procedure Execute; override;
public
///
/// 向主线程投递的方法,让主线程遍历 gc TTaskForm,因为TThread.Queue 需要 对象过程才可,所以挂载到这里
///

procedure letMainGcTaskForm;
end;

//================================ 非公开的变量 ====================================

var

///


/// 协程机制是否在正常运行,要求APP 在运行,协程机制就在运行,不要分为单独2个状态
///

TASK_IS_RUNING: Boolean;

///


/// 定义一个标记是否开启调试模式,与官方的无关,只用于我自己的协程库的调试
///

CONFIG_IS_DEBUG: Boolean = True;

///


/// 全局异常处理函数
///

CONFIG_EXCEPTION_HANDLER: TExceptionHandler;

///


/// 总 bg 任务数,方便本地没有时,问全局要 或 偷别人的时,先看下当前有没有任务,
/// 不要盲目的去全局取 或 盲目的偷,有了这个 也方便调试,新增 bg + 1,wk 做完就 - 1;
/// 1. gotoBgWaitQueue 那里 + 1;main,df、gc线程新增协程;
/// 2. wk线程 自己衍生时候 + 1;bg.bg.start那里
/// 3. wk.excute 那里,本地队列出队执行时候 - 1;
/// 入队时是 先入队再 + 1,出队时是 先 - 1,再出队,这样能避免多线程下信息不对称的问题;
///

WAIT_BG_COUNT: Integer;

///


/// wk 线程总数,方便开发,很多地方用到,省得去用 wkThreads.count
///

WK_COUNT: Integer;

///


/// GC线程隔一段时间,向主线程发送一个启动回收 TTaskForm的任务,原因是 main线程使用了tryEnter,若
/// 拿不到锁就会跳过,有可能导致 TaskForm 忘记回收,这样做是为了提升main的效率,毕竟main是主线程,
/// 要做的事比较多,不可能等锁,也可以避免主线程gc时发生潜在的死锁问题,故而 gc 定期给main一个任务
/// 定期唤醒它一下,由于gc正常情况下 已经向 main 发送回收 form的任务了,这个仅仅是为了 避免 main
/// tryEnter 跳过某一个窗体的问题,故而 这个周期 就设置成 100 个 gc醒来周期;gc 醒来一次 就减1,
/// 减到0的时候,就发送任务,修改再修改为100
///

GC_MAIN_CYCLE: Integer = 100;

//------------------ 以下是容器和线程 --------------------------

///


/// 全局容器
///

glTaskFormMap: TDictionary<string, TTaskForm>; //方便从中间删除,故而使用 Map
glWaitQueue: TQueue;
glWaitQueueLock: TCriticalSection;
glRptList: TList; //可排序
glRptListLock: TCriticalSection;
rptDoneMap: TDictionary<string, TTask>; //方便从中间删除,故而使用 Map
rptDoneMapLock: TCriticalSection;
mainDoneMap: TDictionary<string, TTask>; //方便从中间删除,故而使用 Map
mainDoneMapLock: TCriticalSection;

///


/// 重复协程的线程,需要event通知,故定义这个变量
///

rptThread: TRptThread;

///


/// task回收线程
///

gcThread: TGcThread;

///


/// 工人线程
///

wkThreads: TList;

//================================ 非公开的常量 ====================================

const

//-------------- 一些默认配置 -----------------

///


/// 线程在没有任务的时候,自我休眠多久,单位为毫秒
///

SLEEP_GC_MS: Integer = 500;
SLEEP_WK_MS: Integer = 500;

///


/// 默认本地 wait 队列,元素的个数
///

FIXED_WAIT_SIZE = 256;

//---------------- 协程状态 -----------------

///


/// 新建,等待执行
///

ST_NEW = 1;

///


/// 当前协程正在执行
///

ST_DOING = 2;

///


/// 1.当前协程已经执行完毕;
/// 2.切记这里并非,“当前节点以及子节点全部执行完成”
///

ST_COMPLETING = 3;

///


/// 处理成功回调中
///

ST_ON_COMPLETING = 4;

///


/// 最终一切成功,包括回调也成功,此时协程是终态
///

ST_COMPLETED = 5;

///


/// 当前节点执行的过程中(DOING状态),被外部节点执行了取消,状态变为取消中
/// 等待持有当前节点的线程自我发现(协程中的代码若存在耗时,要善于多处判断当
/// 前协程的状态)自我发现后,并接力向下递归取消;
///

ST_CANCELING = 6;

///


/// 处理取消回调中
///

ST_ON_CANCELING = 7;

///


/// 已成功取消
///

ST_CANCELED = 8;

///


/// 当是调试模式的时候,输出调试日志
///

procedure log(msg: string);
begin
if CONFIG_IS_DEBUG then
begin
if TThread.Current.ThreadID = MainThreadID then
begin
OutputDebugString(PChar('main --- ' + msg));
end else begin
OutputDebugString(PChar(TBaseThread(TThread.Current).name + ' --- ' + msg));
end;
end;
end;

///


/// 协程核心异常,通常不会发生,直接弹框报错,不考虑UI线程安全问题。
/// 1. 协程核心错误,我的错误,比如新增协程状态等,这类错误基本遇不到,第一时间解决;
/// 2. 开发人员书写错误,这个属于开发人员使用协程书写的基本问题,也是低级错误,第一时间解决;
/// 废弃,这2种错误都是不会发生的,且都是开发人员的低级问题,若
/// 直接Raise EDoroutineCoreException,后台线程又不会显示异常,
/// 一旦程序release发布给用户了,这类低级错误不易发现,这里针对
/// 这种低级错误,采用直接弹框报错的方式,不管线程是否安全的问题了
/// 毕竟仅仅是报错框,应该没有什么问题;直接showMessage('错误')
///

procedure ShowCoreErrorMsg(msg: string);
begin
if TThread.Current.ThreadID = MainThreadID then
begin
ShowMessage(PChar('协程核心 --- main --- ' + msg));
end else begin
ShowMessage(PChar('协程核心 --- ' + TBaseThread(TThread.Current).name + ' --- ' + msg));
end;
end;

///


/// 当前线程是否是主线程
///

function isMainThread: Boolean;
begin
Result := TThread.CurrentThread.ThreadID = MainThreadID;
end;

///


/// 运行协程,是一个不开放的方法,开发者不需要 手工 调用 TTaskConfig.start 或 TTask.run,而是若开发者使用协程时
/// TTask.start 分支协程时候,自动启动相应的线程,容器是默认 initialization 创建的,只是没有启动线程,避免开发时
/// 加载了 bpl,也在运行着线程;而是做到运行时,开发者真正用到 分支协程的时候,才会启用协程机制,启动所有线程
///

procedure enableTaskThreads;
begin
TInterlocked.Exchange(TASK_IS_RUNING, True); //原子操作

//============= 初始化容器和锁,创建是【先容器后线程】释放时反之 ============
glTaskFormMap := TDictionary<string, TTaskForm>.Create;

glWaitQueue := TQueue.Create;
glWaitQueueLock := TCriticalSection.Create;

mainDoneMap := TDictionary<string, TTask>.Create;
mainDoneMapLock := TCriticalSection.Create;
rptDoneMap := TDictionary<string, TTask>.Create;
rptDoneMapLock := TCriticalSection.Create;

glRptList := TList.Create(TComparer.Construct(
function(const Left, Right: TTask): Integer
begin
//由于 list 是用动态数组实现的,为了方便删除时,不移动数组元素,从后面删除,
//要求最先执行的拍在最右边,最后边;
//优先按 state排序,state为 canceling的,排在后面
if Left.state = ST_CANCELING then
begin
Result := 1;
end else if Right.state = ST_CANCELING then begin
Result := - 1;
end else begin
Result := Right.nextTime - Left.nextTime; //倒序
end;
end)); //Comparer是一个接口,内存自动管理,不用担心释放的问题
glRptListLock := TCriticalSection.Create;

//============== 启动线程,先创建工人线程,因为工人线程不依赖其它线程 ===================
{
默认开启的工人线程数 = CPU逻辑处理器 - 主线程 - GC线程 - RPT线程(忽略不计) = 起码 cpu - 2;

1. 若是终端软件,主线程是比较忙碌的,通常理论值是 cpu - 2;
2. 若是用作服务端,主线程是比较闲的,通常理论值是 cpu - 1(gc);

但是线程的 cpu 都有超线程技术,且我们没有处理 wk线程 执行协程时,协程阻塞的问题,若一旦某一个 wk线程阻塞了,
将会导致这个CPU核心处于等待期,造成浪费;我们不能做到 golang那样,阻塞时 可以动态 切换 m 与 p的绑定;为了
避免某一个线程 因为协程阻塞导致资源浪费的问题,我们需要假设条件:

A. 假设当前是终端软件(我们自己就是用来做终端的);
B. 假设最大有3个wk,在做协程的时候发生了阻塞;

根据这2个假设,wk的数量 最好为:cpu - 主线程 - gc线程 + 3 = cpu + 1;

GetEnvironmentVariable('NUMBER_OF_PROCESSORS') = CPUCount;

}
WK_COUNT := CPUCount + 1;
wkThreads := TList.Create;
wkThreads.Capacity := WK_COUNT;
for var I := 1 to WK_COUNT do
begin
var wk := TWkThread.Create('wk' + I.ToString, I - 1);
wk.Start;
wkThreads.Add(wk);
end;
rptThread := TRptThread.Create('rpt');
rptThread.Start;
gcThread := TGcThread.Create('gc');
gcThread.Start;

//主线程命名
if CONFIG_IS_DEBUG then
begin
TThread.NameThreadForDebugging('main', MainThreadID);
end;
end;

///


/// 当前unix时间戳,单独出来方便下面使用,不使用 TZxDateUtil 是为了避免引入自己的单元,
/// 全部只有官方的单元,别人使用时也只需要 task单元,没有其他单元
///

function currentTimeMillis: Int64;
var
LDate: TDateTime;
begin
LDate := TTimeZone.local.ToUniversalTime(Now);
//这里SecondsBetween改成MilliSecondsBetween,由秒修改成毫秒
Result := MilliSecondsBetween(UnixDateDelta, LDate);
if LDate < UnixDateDelta then
Result := -Result;
end;

///


/// 向下递归取消,先判断状态,再childsLock.tryEnter
///

procedure cancelTask(tsk: TTask);
begin
//若递归取消的过程中,程序退出了,则应该层层结束递归
if not TASK_IS_RUNING then
begin
Exit;
end;

//判断协程的状态,8个状态
case tsk.state of

ST_NEW: begin
  //说明还在等待队列里,直接标记为CANCELING,还没有分支故退出递归
  AtomicExchange(tsk.state, ST_CANCELING);
  Exit;
end;

//最终取消要靠持有该协程的线程内部代码自身,并进行取消接力棒递归;
ST_DOING: begin
  AtomicExchange(tsk.state, ST_CANCELING);
  //遇到doing的,就退出继续向下的递归,让持有该协程的线程来进行接力取消就可;
  Exit;
end;

ST_CANCELING: begin  // 已经被其它线程置于取消中了,持有该协程的线程会接力,停止继续向下递归;
  Exit;
end;

ST_COMPLETING: begin
  //自己完成了,分支不清楚,也是修改为 取消中,继续向下递归取消
  AtomicExchange(tsk.state, ST_CANCELING);
end;

ST_ON_COMPLETING: begin
  //1.已经是最底层了,分支都成功了,在进行成功回调,无法再向下走了,退出递归
  Exit;
end;

ST_ON_CANCELING: begin
  //1.已经是最底层了,分支都取消了,在进行取消回调,无法再向下走了,退出递归
  Exit;
end;

ST_COMPLETED: begin
  //1.已经是最底层了,分支都完成了,终态了,退出递归
  Exit;
end;

ST_CANCELED: begin
  //1.已经是最底层了,分支都完成了,终态了,退出递归
  Exit;
end;

end;

{
取消操作是尝试性的取消,最大化的去取消,到底能取消多少任务未知,为了避免死锁,确保整个协程的稳定性,取消
的模式采用 tryEnter,拿不到分支的锁就放弃向下递归;因为 有的正在做的线程 doing,state获取存在并行指令的问题
我们也不知道CPU会先运行那个线程的指令;故而为了最大化避免死锁,导致全线崩溃的问题,我们采用最大化的取消模式;
不确保所有子节点都能得到取消;
}
if tsk.childsLock.TryEnter then
begin
for var c in tsk.childs.Values do
begin
cancelTask(c);
end;
tsk.childsLock.Leave;
end;
end;

///


/// 1. df写入 正常待执行的bg任务;
/// 2. main ui 衍生 bg
/// 3. gc 写入 bg类型的成功或取消回调;
/// ------------
/// 随机优先本地队列,这样可以避开全局队列的锁竞争,随机3次都放不进去的话,
/// 就放全局队列, goto 是转到的意思
///

procedure gotoBgWaitQueue(tsk: TTask);
begin
var times := WK_COUNT; //尝试总数,有几个 wk 就尝试几次
while times > 0 do
begin
Randomize; //每次都要重新设置随机因子,避免每次随机出来的都是一样的
var wkThreadIndex := Random(WK_COUNT - 1);
var wkth := wkThreads.Items[wkThreadIndex];
if wkth.lcWaitQueueLock.TryEnter then //用的是 tryEnter 避开锁竞争
begin
if wkth.lcWaitQueue.Count < wkth.lcWaitQueue.Capacity then
begin
//不满的时候写入
tsk.wkThreadIndex := wkThreadIndex; //写入协程所在的wk线程索引
wkth.lcWaitQueue.Enqueue(tsk);
wkth.lcWaitQueueLock.Leave;

    AtomicIncrement(WAIT_BG_COUNT); // 先入队,再 + 1
    Exit; //可以退出了
  end else begin
    wkth.lcWaitQueueLock.Leave; //只退锁,并不退出循环,继续下一次随机
  end;
end;
Dec(times);

end;

//走到这里说明随机尝试了n次写入某一个线程的本地队列都失败了,开始写入全局
glWaitQueueLock.Enter;
glWaitQueue.Enqueue(tsk);
glWaitQueueLock.Leave;

AtomicIncrement(WAIT_BG_COUNT); // 先入队,再 + 1
end;

///


/// 向上递归回溯查找上下文值,是链表的性能
///

function findValue(tsk: TTask; key: string): TValue;
begin
//当是根节点的时候结束递归,说明不存在,是开发人员写错
if tsk.tkind = TK_ROOT then
begin
ShowCoreErrorMsg('开发人员错误,Key在上下文中不存在!');
end;

//判断当前节点是否存在这个key
if tsk.FData.ContainsKey(key) then
begin
Exit(tsk.FData.Items[key]);
end;

//此节点不存在的时候,就递归向上回溯,性能如单链表的查找
Exit(findValue(tsk.parent, key));
end;

///


/// 根据 parent 和 owner 递归 Free form;
/// 解除 TTaskForm 之间的互相 parent 和 owner的绑定关系;
///

procedure freeTaskForm(frm: TTaskForm);
begin
if TASK_IS_RUNING then //若当前程序正在运行才会进行这2步;否则若程序在关闭时只进行第三步解除关系
begin
//1.修改状态,先修改状态,其它wk线程 不要再 scopes它了
TInterlocked.Exchange(frm.isFreeIng, True); //AtomicExchange 没有 Boolean 重载 用这个

//2.若有task则取消
frm.tasksLock.Enter;
if (frm.tasks <> nil) and (not frm.tasks.IsEmpty) then
begin
  //说明还有协程任务在挂载,取消下
  for var v in frm.tasks.Values do
  begin
    cancelTask(v);
  end;
  frm.tasksLock.Leave;
end;

end;

//3.看下是否存在子窗体,若有子窗体,则向下递归 重复 步骤1 和 步骤2
// 解除 TTaskForm 之间的互相 parent 和 owner的绑定关系;
var delControls := TList.Create;
for var i := 0 to frm.ControlCount - 1 do
begin
var wokao := frm.Controls[i].ClassName;
//窗体在创建时,若指定了 parent会有这个,比如 parent := panel1;
if frm.Controls[i] is TTaskForm then
begin
delControls.Add(frm.Controls[i]);
end;
end;
if not delControls.IsEmpty then
begin
for var c in delControls do
begin
freeTaskForm(TTaskForm(c)); //递归
frm.RemoveControl(c); //解除 parent 关系
end;
end;
delControls.Free;

//开始解除 owner 关系
var delComponents := TList.Create;
for var i := 0 to frm.ComponentCount - 1 do
begin
//窗体在创建时,若指定了 owner 会有这个,比如 create(frm3)
if frm.Components[i] is TTaskForm then
begin
delComponents.Add(frm.Components[i]);
end;
end;
if not delComponents.IsEmpty then
begin
for var c in delComponents do
begin
freeTaskForm(TTaskForm(c)); //递归
frm.RemoveComponent(c); //解除 owner 关系
end;
end;
delComponents.Free;
end;

constructor TBaseThread.Create(name: string);
begin
//创建后会立即执行,那么会导致 Self.name := name; 及以下代码都失效,看不到效果,故最好创建后不要立即执行;
inherited Create(True);
//我们自己掌控何时释放,它默认就是False,这里写上做下提示
//Self.FreeOnTerminate := False;
if CONFIG_IS_DEBUG then
begin
Self.name := name; //方便输出日志 OutputDebugString,在控制台看到名字日志
NameThreadForDebugging(name, Self.ThreadID); //方便调试时,在IDE调试器里,看到名字
end;
end;

constructor TRptThread.Create(name: string);
begin
inherited Create(name);
//唤醒一次后,立即进入等待模式
Self.event := TEvent.Create(nil, False, False, '');
end;

destructor TRptThread.Destroy;
begin
Self.event.Free;
inherited;
end;

procedure TRptThread.Execute;
begin
while TASK_IS_RUNING do
begin
glRptListLock.Enter; //这里不tryEnter跳过,而是等待
if not glRptList.IsEmpty then
begin
{
排序放到这里的理由:
1. 插入列表的时候就不需要排序了,插入10个重复任务,醒来后只需要排序一次,而不是插入时排序10次;
插入操作主要是主线程;
2. list 是基于 动态数组实现的,若 glRptList.Delete(0); 会导致 后面的元素全部向前移动,故而采用 尾部是最先执行的策略;倒序排序;
}
glRptList.Sort;
var tsk := glRptList[glRptList.Count - 1];
//先看下状态,因为若是被取消的话,取消线程只是改变了状态,此时状态会是 canceling
if tsk.state = ST_CANCELING then
begin
//删除尾部元素,并退出锁
glRptList.Delete(glRptList.Count - 1);
glRptListLock.Leave;

    //转到做完队列里去
    rptDoneMapLock.Enter;
    rptDoneMap.Add(tsk.id, tsk);
    rptDoneMapLock.Leave;
  end else begin
    //检查第一个元素是否到执行时间了
    var waitTimeout := (tsk.nextTime - CurrentTimeMillis);
    if waitTimeout <= 0 then
    begin
      //删除元素,并退出锁
      glRptList.Delete(glRptList.Count - 1);
      glRptListLock.Leave;

      //根据类型,加入执行队列
      case tsk.tkind of
        TK_BG: gotoBgWaitQueue(tsk);

        TK_UI: TThread.Queue(nil, tsk.outNoArgProc); //这里Wl线程是后台线程直接用queue就可;
      end;
    end else begin
      //说明还未到时间,退出锁,继续等待
      glRptListLock.Leave;

      //进入有限等待模式
      log('再睡 ' + waitTimeout.ToString + ' 毫秒');

      //放到最下面,程序退出时候,唤醒此线程,可立即退出,因为由于在最下面,一旦醒来第一时间就是 TASK_STOP 的判断
      case event.WaitFor(waitTimeout) of
        wrSignaled: log('有限等待后,被唤醒了');

        wrTimeout: log('睡到时间了,自我醒了');

        //wrAbandoned, wrError, wrIOCompletion
        else ShowCoreErrorMsg('df核心错误,有限等待的过程中发生了异常!');
      end;
    end;
  end;
end else begin
  //先释放锁
  glRptListLock.Leave;

  //进入死等模式
  log('进入死等模式');

  //放到最下面,程序退出时候,唤醒此线程,可立即退出,因为由于在最下面,一旦醒来第一时间就是 TASK_STOP 的判断
  case event.WaitFor(INFINITE) of

    wrSignaled: log('死等后,被唤醒了');

    //wrTimeout,wrAbandoned, wrError, wrIOCompletion
    else ShowCoreErrorMsg('df核心错误,死等的过程中发生了异常!');
  end;
end;

end;
//关闭程序的时候才会走到这里,否则不会走到这里
log('线程终止');
end;

constructor TWkThread.Create(name: string; index: Integer);
begin
inherited Create(name);
Self.index := index;
//先创建锁,再队列
lcWaitQueueLock := TCriticalSection.Create;
lcWaitQueue := TQueue.Create;
lcWaitQueue.Capacity := FIXED_WAIT_SIZE; //固定大小
lcDoneMapLock := TCriticalSection.Create;
lcDoneMap := TDictionary<string, TTask>.Create; //动态大小
end;

destructor TWkThread.Destroy;
begin
//先释放锁,再队列
lcWaitQueueLock.Free;
lcWaitQueue.Free;
lcDoneMapLock.Free;
lcDoneMap.Free;
inherited;
end;

procedure TWkThread.Execute;
begin
while TASK_IS_RUNING do
begin
//醒来后,先看下指示牌,是否有任务,若没有则说明 本地队列、全局队列、同事队列 统统都没有,就没有必要进行下面的步骤了
if WAIT_BG_COUNT > 0 then
begin
//1.优先看下 本地队列里有没有
lcWaitQueueLock.Enter; //这里使用 enter,先搞本地队列
if not lcWaitQueue.IsEmpty then
begin
AtomicIncrement(WAIT_BG_COUNT); //先 - 1,再出队,避免自己要出队前,别人还来偷;

    var tsk := lcWaitQueue.Dequeue;
    lcWaitQueueLock.Leave;

    {
      首先要明确,在等待执行的队列 bg/uiQueue中 只可能存在 2种状态,即:NEW, CANCELING,
      因为能在这个队列中存在,说明还没有开始执行。
      1.当前协程在队列中处于NEW状态,此时可演变成DOING
      2.当前协程在队列中处于CANCELING状态,就是说由异常、作用域关闭、主动取消父节点,
        而引起的取消中,这类的直接跳过,其它的情况不存在,就报错。
    }
    case tsk.state of
      ST_NEW: begin
        //只有NEW可以演变成DOING,取消的线程发现是doing了,就会结束向下递归,而是把取消的权利转让给当前线程
        AtomicExchange(tsk.state, ST_DOING);

        {
          留意点在执行时要锁住childs,这样一旦被外部修改成canceling,子节点又被其它线程完成了,gc开始回收子节点,
          去父节点的 childs那里 删除指针,不能让其删除,由于 gc.childsLock.tryEnter 拿不到锁就会走开,这样让自己执
          行完毕后再判断状态,进入如果架构图所示流程,避免父节点生成着,GC 回收着子节点,可能会出现意外的问题,要求
          必须父节点完成了,gc才有可能回收子节点;
        }
        tsk.childsLock.Enter;

        //接下来开始执行代码
        try
          case tsk.pkind of
            PK_GLB: tsk.procGlb(tsk);
            PK_OBJ: tsk.procObj(tsk);
          end;

          {
           此时说明没有发生异常,走到这里state的情况为:
           1. doing
           2. canceling 被上层修改为了取消中
           只有以上2种可能否则就是逻辑有问题
          }
          case tsk.state of
            ST_DOING: AtomicExchange(tsk.state, ST_COMPLETING);

            ST_CANCELING: begin
              //若有分支就 接力棒式 递归向下取消
              if (tsk.childs <> nil) and (not tsk.childs.IsEmpty) then
              begin
                for var c in tsk.childs.Values do
                begin
                  cancelTask(c);
                end;
              end;
            end;

            else ShowCoreErrorMsg('核心错误,状态只可能是 doing 或 canceling,请联系开发者!');
          end;

          tsk.childsLock.Leave; //执行完毕后才松开子节点锁
        except
          //这里若有代码则编译不通过,必须放到 on 内部
          on e: Exception do
          begin
            //有异常的时候,标记为canceling,会被GC最终执行全局异常函数,异常并不会影响协程的继续
            AtomicExchange(tsk.state, ST_CANCELING);
            //若有分支就 接力棒式 递归向下取消
            if (tsk.childs <> nil) and (not tsk.childs.IsEmpty) then
            begin
              for var c in tsk.childs.Values do
              begin
                cancelTask(c);
              end;
            end;
            tsk.childsLock.Leave; //在这里松开子节点的锁

            //判断是否分配了全局异常处理函数
            if Assigned(CONFIG_EXCEPTION_HANDLER) then
            begin
              {
                注意这里使用的是:TThread.Synchronize(),而不是TThread.ForceQueue(),区别:

                TThread.Synchronize() 是阻止当前后台线程的继续运行,可以卡住当前后台线程;
                此时的e还在后台线程栈里,没有被自动回收掉,主线程运行完处理异常函数后,
                此后台线程才可能继续执行,正因为如此,才导致e在主线程里并没有被释放,可以
                在主线程里获取到e的线程栈等信息;

                TThread.ForceQueue(),是将任务塞进主线的任务队列里,主线程会在空闲的时候执行,
                然后继续本后台线程的执行,这样会导致此后台线程运行完此任务后,Exception的实例
                被释放,主线程再去Application.ShowException(e)时候,e是个空指针;没有起到卡住
                当前后台线程的作用,主线程拿不到异常栈信息,故禁用;
              }
              TThread.Synchronize(nil, procedure begin
                CONFIG_EXCEPTION_HANDLER(e);
              end);
            end else begin
              //没有分配的时候,默认在主线程里弹出官方的默认窗体报错,Application.ShowException 只
              //能在主线程里运行,无论是普通的窗体还是Dialog,因为有窗体消息循环都只能在主线程里运行;
              TThread.Synchronize(nil, procedure begin
                Application.ShowException(e);
              end);
            end;
          end;
        end;
      end;

      ST_CANCELING: begin
        //什么都不做,让其进入 完毕队列
      end;

      else ShowCoreErrorMsg('核心错误,状态只可能是 new 或 canceling,请联系开发者!');
    end;

    //写入本地完成队列
    lcDoneMapLock.Enter;
    lcDoneMap.Add(tsk.id, tsk);
    lcDoneMapLock.Leave;
  end else begin
    //2.本地没有,从全局获取,从全局获取时,依然是锁住自己的本地队列
    glWaitQueueLock.Enter;
    if not glWaitQueue.IsEmpty then
    begin
      {
        每次从全局拿取的个数,采用 golang 的公式:min(len(GRQ)/GOMAXPROCS + 1, len(GRQ)/2);
        这个公式的好处是,若有人拿这个在服务端运行,购买一个 1 -2 核的CPU,默认是2个工作线程;
        若全局任务有 10个; len(GRQ)/GOMAXPROCS 就会等于  10/2 + 1 = 6,一次拿了一半多,不那么
        公平了,其实golang是想让更公平些,拿的总数一次不要总个数的超过一半,我们也采用这个公式;
        -----------------------
        由于我们最小线程数 为 3,因为golang不考虑 阻塞线程的问题,它会自动销毁切换阻塞线程,
        不存在这个问题,而我们没有解决阻塞线程的问题,故而我们采用最小为3个线程,来解决避免有的
        线程阻塞,影响进度;不存在这个问题,所以不使用下面这句:
        var takeCount := Min((glWaitQueue.Count div WK_COUNT) + 1, glWaitQueue.Count div 2);
      }
      var takeCount := (glWaitQueue.Count div WK_COUNT) + 1;
      if takeCount > FIXED_WAIT_SIZE then
      begin
        takeCount := FIXED_WAIT_SIZE; //不要超过本地队列的总容量
      end;
      while takeCount > 0 do
      begin
        var newIn := glWaitQueue.Dequeue;
        newIn.wkThreadIndex := Self.index; //进入前,写入线程所在的索引
        lcWaitQueue.Enqueue(newIn);
        Dec(takeCount);
      end;
      glWaitQueueLock.Leave; //退出全局锁
      lcWaitQueueLock.Leave; //退出本地锁,让其重新走流程,临界不是可重入锁
    end else begin
      //先退出全局锁,此时的本地队列锁依然是锁着的
      glWaitQueueLock.Leave;

      {
        3.去同事那里偷,是尝试 用的是 tryEnter,避免与同事的队列进行锁竞争,golang这里采用的是 随机偷
        随机偷的缺陷是,若一个队列里有,随机数始终随机不到这个队列,则会导致浪费,且随机几次退出,也难以判断
        若随机3次退出,其中A列有,但始终没随机到,岂不白搭。若每次 都从 0 - count -1,来进行遍历,又会
        导致,第一个队列最糟糕,所有的人都先偷他,根本不公平,增加第一列的锁竞争。若排序谁的多偷谁的,又会
        导致排序计算性能下降;这里我采用了 一个最先进的办法,比golang的还要先进,就是每个人都优先偷自己下一
        个,然后偷一圈,这样即保证了简单,又循环了一圈;非常完美,避开了 golang 随机的问题,和 从 0开始遍历
        第一列不公平,锁竞争的问题;
      }
      var stealIndexs := TList<Integer>.Create;
      if Self.index = 0 then //说明是头元素
      begin
        //1 - 最后一个
        for var i := 1 to (WK_COUNT - 1) do
        begin
          stealIndexs.Add(i);
        end;
      end else if Self.index = (WK_COUNT - 1) then begin //说明是尾元素
        //0 - 倒数第二
        for var i := 0 to (WK_COUNT - 2) do
        begin
          stealIndexs.Add(i);
        end;
      end else begin //说明是中间元素
        //自己的后一个 - 最后一个
        for var i := (Self.index + 1) to (WK_COUNT - 1) do
        begin
          stealIndexs.Add(i);
        end;
        //自己前面的
        for var i := 0 to (Self.index - 1) do
        begin
          stealIndexs.Add(i);
        end;
      end;

      //开始去偷
      var stealSuccess := False;
      for var stealIndex in stealIndexs do
      begin
        var stealTh := wkThreads.Items[stealIndex];
        if stealTh.lcWaitQueueLock.TryEnter then //TryEnter 尝试去偷,避免死锁
        begin
          if not stealTh.lcWaitQueue.IsEmpty then
          begin
            //一次偷一半
            var takeCount := (stealTh.lcWaitQueue.Count div 2) + 1;
            while takeCount > 0 do
            begin
              var newIn := stealTh.lcWaitQueue.Dequeue;
              newIn.wkThreadIndex := Self.index; //把协程的锁在的线程索引,修改成当前线程的索引
              Self.lcWaitQueue.Enqueue(newIn);
              Dec(takeCount);
            end;

            stealSuccess := True;
            stealTh.lcWaitQueueLock.Leave; //释放锁
            Break; //偷完跳出循环,只偷一个人的就可以了,注意这里是 直接跳出循环,所以上面这句 stealTh.lcWaitQueueLock.Leave; 必须写在上面
          end else begin
            stealTh.lcWaitQueueLock.Leave; //只释放锁
          end;

          //无论是否偷成功,退出被偷队列;思路 bug 留意,这段代码保留,作为严重提示,不能写在这里,因为上面若偷成功是 break,直接跳出了循环,根本不会执行到这里;
          //stealTh.lcWaitQueueLock.Leave;
        end;
      end;
      stealIndexs.Free;

      Self.lcWaitQueueLock.Leave; //无论是否偷成功,退出本地锁,让其重新走流程,临界不是可重入锁

      //若没有偷到,则说明很大概率没有任务了,休息一会,再重来吧
      if not stealSuccess then
      begin
        Sleep(SLEEP_WK_MS);
      end;
    end;
  end;
end else begin
  Sleep(SLEEP_WK_MS);
end;

end;
//关闭程序的时候才会走到这里,否则不会走到这里
log('线程终止');
end;

procedure TGcThread.letMainGcTaskForm;
begin
{
为了避免主线程等待锁,卡死,这里使用 tryEnter拿不到锁就走,等待下次;
但是因为若用 tryEnter主线程拿不到锁就走了,会导致窗体永远得不到释放,
不像普通的协程那样 有gc线程定时醒来,解决方法就是让gc线程 间隔一定的
时间向主线程发送这个任务,这样主线程也可以定期检测这个全局容器了,遇到
v.tasksLock,遇到这个锁,拿不到的时候,就可以先退出,下次再遍历执行了;
也可以避免潜在的死锁问题;
}
if not glTaskFormMap.IsEmpty then
begin
var freeIds := TList.Create; //避免边遍历,边删除的问题,先记录要删除的ID列表
try
for var kv in glTaskFormMap do
begin
var frm := kv.Value;
if frm.isFreeIng then
begin
//v.tasksLock,遇到这个锁,拿不到的时候,就可以先退出,下次再遍历执行了;也可以避免潜在的死锁问题;
if frm.tasksLock.TryEnter then
begin
if (frm.tasks = nil) or (frm.tasks.IsEmpty) then
begin
freeIds.Add(frm.id);
end;
frm.tasksLock.Leave;
end;
end;
end;

  //开始删除
  if not freeIds.IsEmpty then
  begin
    for var fid in freeIds do
    begin
      //自己的free是个假free;要使用自己的 Destroy 来释放
      glTaskFormMap.Items[fid].Destroy;
      glTaskFormMap.Remove(fid); //再删除
    end;
  end;
finally
  freeIds.Free;
end;

end;
end;

///


/// 单独出来一个容器,一个容器的清洗,主要是 TGcThread.Execute 调用
///

procedure gcTask(map: TDictionary<string, TTask>; mapLock: TCriticalSection; var needGcForm: Boolean);
begin
//先看下容器是否为空
if mapLock.TryEnter then // TryEnter; 没进来就先跳过,等待下次循环,保证GC的效率,不要等待任何一个
begin
if not map.IsEmpty then
begin
var freeIds := TList.Create; //这里不会出错,没有必要加 try ... finally
for var v in map.Values do
begin
//做完的协程等待区 completing、oncompleting、canceling、oncanceling、completed、canceled
case v.state of
ST_COMPLETING: begin
//看下是否有分支
if v.childsLock.TryEnter then //没进来就先跳过,等待下次循环,保证GC的效率,不要等待任何一个
begin
//还存在分支就跳过什么都不做,让其分支继续完成
if (v.childs = nil) or (v.childs.IsEmpty) then
begin
//不存在分支的话,就看下是否有成功回调
if v.isExistOnComplete then
begin
//有回调,就衍生回调
AtomicExchange(v.state, ST_ON_COMPLETING);
case v.onCompleteTKind of
TK_BG: begin
case v.onCompletePKind of
PK_GLB: v.bg(v.onCompleteGlb).start; //新衍生的协程,不携带数据和作用域,查找的话也是查找父节点的数据和依赖父节点作用域;

                    PK_OBJ: v.bg(v.onCompleteObj).start;
                  end;
                end;

                TK_UI: begin
                  case v.onCompletePKind of
                    PK_GLB: v.ui(v.onCompleteGlb).start; //新衍生的协程,不携带数据和作用域,查找的话也是查找父节点的数据和依赖父节点作用域;

                    PK_OBJ: v.ui(v.onCompleteObj).start;
                  end;
                end;
              end;
            end else begin
              //没有回调就修改状态为 completed,等待下次循环回收
              AtomicExchange(v.state, ST_COMPLETED);
            end;
          end;
          v.childsLock.Leave;
        end;
      end;

      ST_CANCELING: begin
        //看下是否有分支
        if v.childsLock.TryEnter then //没进来就先跳过,等待下次循环
        begin
          //还存在分支就跳过什么都不做,让其分支继续完成
          if (v.childs = nil) or (v.childs.IsEmpty) then
          begin
            //不存在分支的话,就看下是否有成功回调
            if v.isExistOnCancel then
            begin
              //有回调,就衍生回调
              AtomicExchange(v.state, ST_ON_CANCELING);
              case v.onCancelTKind of
                TK_BG: begin
                  case v.onCancelPKind of
                    PK_GLB: v.bg(v.onCancelGlb).start; //新衍生的协程,不携带数据和作用域,查找的话也是查找父节点的数据和依赖父节点作用域;

                    PK_OBJ: v.bg(v.onCancelObj).start;
                  end;
                end;

                TK_UI: begin
                  case v.onCancelPKind of
                    PK_GLB: v.ui(v.onCancelGlb).start; //新衍生的协程,不携带数据和作用域,查找的话也是查找父节点的数据和依赖父节点作用域;

                    PK_OBJ: v.ui(v.onCancelObj).start;
                  end;
                end;
              end;
            end else begin
              //没有回调就修改状态为 终态,等待下次循环回收
              AtomicExchange(v.state, ST_CANCELED);
            end;
          end;
          v.childsLock.Leave;
        end;
      end;

      ST_ON_COMPLETING: begin
        //看下是否还有分支,有分支就说明回调子任务还没有执行完就跳过,无分支就修改为终态,让GC下次循环回收
        if v.childsLock.TryEnter then
        begin
          if (v.childs = nil) or (v.childs.IsEmpty) then
          begin
            AtomicExchange(v.state, ST_COMPLETED);
          end;
          v.childsLock.Leave;
        end;
      end;

      ST_ON_CANCELING: begin
        //看下是否还有分支,有分支就说明回调子任务还没有执行完就跳过,无分支就修改为终态,让GC下次循环回收
        if v.childsLock.TryEnter then
        begin
          if (v.childs = nil) or (v.childs.IsEmpty) then
          begin
            AtomicExchange(v.state, ST_CANCELED);
          end;
          v.childsLock.Leave;
        end;
      end;


      ST_COMPLETED, ST_CANCELED: begin //终态的话加入free队列
        freeIds.Add(v.id);
      end;
    end;
  end;


  //开始删除
  if not freeIds.IsEmpty then
  begin
    for var v in freeIds do
    begin
      var tsk := map.Items[v];

      //无论是普通的协程还是重复的协程,完全关系解除成功后,这里都是先删除
      map.Remove(v);

      //判断是否是重复协程,重复的协程不需要解除关系
      if not tsk.isRpt then
      begin
        var isAllSuccess := True; //全部关系解除成功
        //1. 解除 与 TTaskForm 的关系映射
        if (tsk.FScopes <> nil) and (not tsk.FScopes.IsEmpty) then
        begin
          for var f in tsk.FScopes do
          begin
            if f.tasksLock.TryEnter then //提升gc的效率,不等待,不行就跳过下次再说
            begin
              if (f.tasks = nil) or (not f.tasks.ContainsKey(v)) then
              begin
                ShowCoreErrorMsg('协程核心错误,ID在scopes中找不到!');
              end;

              f.tasks.Remove(v);
              if (f.tasks.IsEmpty) and (f.isFreeIng) then
              begin
                needGcForm := True;
              end;
              f.tasksLock.Leave;
            end else begin
              isAllSuccess := False; //未能解除成功,等待下次循环再尝试
            end;
          end;
        end;

        //2.解除与父节点的关系; root 是不会在全局容器中的,不用考虑 root的父节点的问题
        var parentTsk := tsk.parent;
        if parentTsk.childsLock.TryEnter then
        begin
          parentTsk.childs.Remove(tsk.id);
          parentTsk.childsLock.Leave;
        end else begin
          isAllSuccess := False; //未能解除成功,等待下次循环再尝试
        end;

        //3.最后若以上都成功了,则释放
        if isAllSuccess then
        begin
          tsk.Free; //先释放
          map.Remove(v); //从 done 容器移除
        end;
      end else begin
        {
          说明是重复的协程的步骤,切记:先从done容器中移除,然后再加入 rpt 容器,
          避免先 rpt容器,rpt若是正好醒来,这个协程又会快速执行,又进入done容器,
          导致 key 键重复;重复的协程不释放;
        }
        //1. 先从done容器中移除
        map.Remove(v);

        //2.重新计算下次运行时间
        tsk.nextTime := currentTimeMillis + tsk.rptInterval;

        //3.改变 state 为new
        AtomicExchange(tsk.state, ST_NEW);

        //4.再次加入容器,所有地方统一在 df 线程排序;
        glRptListLock.Enter; //这里不能使用 tryEnter了,这里无法跳过,必须等待
        glRptList.Add(tsk);
        glRptListLock.Leave;

        //5.通知延迟线程醒来看下
        rptThread.event.SetEvent;
      end;
    end;
  end;

  freeIds.Free; //释放
end;
mapLock.Leave; //释放锁

end;
end;

///


/// 留意点要用 xx.TryEnter; 没进来就先跳过,等待下次循环,保证GC的效率,不要等待任何一个
///

procedure TGcThread.Execute;
begin
while TASK_IS_RUNING do
begin
var needGcForm := False; //看下本轮遍历后,最终是否需要回收 form,方便最后通知主线程去遍历form

//1. main 完毕容器
gcTask(mainDoneMap, mainDoneMapLock, needGcForm);

//2. rpt 完毕容器
gcTask(rptDoneMap, rptDoneMapLock, needGcForm);

//3. wk 完毕容器
for var th in wkThreads do
begin
 gcTask(th.lcDoneMap, th.lcDoneMapLock, needGcForm);
end;


//本轮完毕后,若最后发现需要主线程回收窗体,则向主线程投递遍历回收窗体的方法
if needGcForm then
begin
  log('gc - main 发送 gcForm');
  TThread.Queue(nil, self.letMainGcTaskForm);
end;


//最后都做完了后,睡眠一会自我醒来
Sleep(SLEEP_GC_MS);
//醒来后就递减次数,只有gc访问这个,不存在多线程的问题
if GC_MAIN_CYCLE = 0 then
begin
  //补偿发送
  TThread.Queue(nil, self.letMainGcTaskForm);
  log('gc - main 补偿发送');
  GC_MAIN_CYCLE := 100;
end else begin
  Dec(GC_MAIN_CYCLE);
end;

end;
//关闭程序的时候才会走到这里,否则不会走到这里
log('线程终止');
end;

///


/// 程序关闭时,主线程从ROOT节点向下递归释放整个协程树,只所以不从队列入手来释放,
/// 是因为一个协程的指针,可能存在多个队列容器里,导致从队列入手的话,还得
/// 判断AssignEx,效率低,故直接从ROOT节点向下递归
///

procedure terminateTask(tsk: TTask);
begin
//只有主线程操作不用加锁了,一定不是ROOT
if (tsk.childs <> nil) and (tsk.childs.Count > 0) then
begin
for var v in tsk.childs.Values do
begin
terminateTask(v);
end;
end;
tsk.Free;
end;

constructor TTask.Create;
begin
inherited Create;
id := TGUID.NewGuid.ToString;
childsLock := TCriticalSection.Create; //锁必须初始化
isRpt := False; //默认不是重复协程
state := ST_NEW; //新创建的协程默认是NEW状态
wkThreadIndex := - 1; //故意默认为 -1 ,这样有逻辑问题时,方便报错
end;

destructor TTask.Destroy;
begin
childsLock.Free;
//分支也是动态创建的
if childs <> nil then
begin
childs.Free;
end;
//FData是动态创建的,所以判断下
if FData <> nil then
begin
FData.Free;
end;
//作用域是动态创建的,所以判断下
if FScopes <> nil then
begin
//协程 与 XForm的关系为:协程不在了,XForm可能还在;协程在XForm一定存在,所以释放协程时,只需释放字段就行,不要释放formx
//gc那里已经处理了,作用域formx的问题,这里只是简单的释放就可
FScopes.Free;
end;
log(Self.id + ' ----- 被释放了');
inherited;
end;

procedure TTask.setGlobalExceptionHandler(handler: TExceptionHandler);
begin
if not Assigned(CONFIG_EXCEPTION_HANDLER) then
begin
CONFIG_EXCEPTION_HANDLER := handler;
end else ShowCoreErrorMsg('开发人员错误,全局异常函数,只可配置一次!');
end;

procedure TTask.outNoArgProc;
begin
{
首先要明确,在等待执行的队列 bg/uiQueue中 只可能存在 2种状态,即:NEW, CANCELING,
因为能在这个队列中存在,说明还没有开始执行。
1.当前协程在队列中处于NEW状态,此时可演变成DOING
2.当前协程在队列中处于CANCELING状态,就是说由异常、作用域关闭、主动取消父节点,
而引起的取消中,这类的直接跳过,其它的情况不存在,就报错。
}
case Self.state of
ST_NEW: begin
//只有NEW可以演变成DOING,取消的线程发现是doing了,就会结束向下递归,而是把取消的权利转让给当前线程
AtomicExchange(Self.state, ST_DOING);

  {
    留意点在执行时要锁住childs,这样一旦被外部修改成canceling,子节点又被其它线程完成了,gc开始回收子节点,
    去父节点的 childs那里 删除指针,不能让其删除,由于 gc.childsLock.tryEnter 拿不到锁就会走开,这样让自己执
    行完毕后再判断状态,进入如果架构图所示流程,避免父节点生成着,GC 回收着子节点,可能会出现意外的问题,要求
    必须父节点完成了,gc才有可能回收子节点;
  }
  Self.childsLock.Enter;

  //接下来开始执行代码
  try
    case Self.pkind of
      PK_GLB: Self.procGlb(Self);
      PK_OBJ: Self.procObj(Self);
    end;

    {
     此时说明没有发生异常,走到这里state的情况为:
     1. doing
     2. canceling 被上层修改为了取消中
     只有以上2种可能否则就是逻辑有问题
    }
    case Self.state of
      ST_DOING: AtomicExchange(Self.state, ST_COMPLETING);

      ST_CANCELING: begin
        //若有分支就 接力棒式 递归向下取消
        if (Self.childs <> nil) and (not Self.childs.IsEmpty) then
        begin
          for var c in Self.childs.Values do
          begin
            cancelTask(c);
          end;
        end;
      end;

      else ShowCoreErrorMsg('核心错误,状态只可能是 doing 或 canceling,请联系开发者!');
    end;

    Self.childsLock.Leave; //执行完毕后才松开子节点锁
  except
    //这里若有代码则编译不通过,必须放到 on 内部
    on e: Exception do
    begin
      //有异常的时候,标记为canceling,会被GC最终执行全局异常函数,异常并不会影响协程的继续
      AtomicExchange(Self.state, ST_CANCELING);
      //若有分支就 接力棒式 递归向下取消
      if (Self.childs <> nil) and (not Self.childs.IsEmpty) then
      begin
        for var c in Self.childs.Values do
        begin
          cancelTask(c);
        end;
      end;
      Self.childsLock.Leave; //在这里松开子节点的锁

      //判断是否分配了全局异常处理函数
      if Assigned(CONFIG_EXCEPTION_HANDLER) then
      begin
        //这里就是主线程了,所以直接执行 没有必要 TThread.Synchronize
        CONFIG_EXCEPTION_HANDLER(e);
      end else begin
        //没有分配的时候,默认在主线程里弹出官方的默认窗体报错,Application.ShowException 只
        //能在主线程里运行,无论是普通的窗体还是Dialog,因为有窗体消息循环都只能在主线程里运行;
        Application.ShowException(e);
      end;
    end;
  end;

  //进入完毕队列
  mainDoneMapLock.Enter;
  mainDoneMap.Add(Self.id, Self);
  mainDoneMapLock.Leave;
end;

ST_CANCELING: begin
  //转入完毕队列
  mainDoneMapLock.Enter;
  mainDoneMap.Add(Self.id, Self);
  mainDoneMapLock.Leave;
end;

else ShowCoreErrorMsg('核心错误,状态只可能是 new 或 canceling,请联系开发者!');

end;
end;

function TTask.isCancel: Boolean;
begin
{
当程序退出时,BG/UI线程若在执行耗时较久的协程,那么在协程内部应该判断isCancel,
来最快的响应退出,程序员不需要关注 APP_EXIT这个变量,仅仅通过一个isCancel来决
定是否退出协程
}
if not TASK_IS_RUNING then
begin
Exit(True);
end;

case Self.state of
ST_CANCELING, ST_ON_CANCELING, ST_CANCELED: Exit(True);
else Exit(False);
end;
end;

function TTask.getString(key: string): string;
begin
Result := findValue(Self, key).AsString;
end;

function TTask.getInteger(key: string): Integer;
begin
Result := findValue(Self, key).AsInteger;
end;

function TTask.getBoolean(key: string): Boolean;
begin
Result := findValue(Self, key).AsBoolean;
end;

function TTask.getInt64(key: string): Int64;
begin
Result := findValue(Self, key).AsInt64;
end;

function TTask.getUInt64(key: string): UInt64;
begin
Result := findValue(Self, key).AsUInt64;
end;

function TTask.getCurrency(key: string): Currency;
begin
Result := findValue(Self, key).AsCurrency;
end;

function TTask.getExtended(key: string): Extended;
begin
Result := findValue(Self, key).AsExtended;
end;

function TTask.bg(proc: TProcGlb): TTask;
begin
if forkTemp <> nil then
begin
ShowCoreErrorMsg('开发人员书写错误,不可以重复使用bg');
end;

forkTemp := TTask.Create;
forkTemp.tkind := TK_BG;
forkTemp.pkind := PK_GLB;
forkTemp.procGlb := proc;

Result := Self;
end;

function TTask.bg(proc: TProcObj): TTask;
begin
if forkTemp <> nil then
begin
ShowCoreErrorMsg('开发人员书写错误,不可以重复使用bg');
end;

forkTemp := TTask.Create;
forkTemp.tkind := TK_BG;
forkTemp.pkind := PK_OBJ;
forkTemp.procObj := proc;

Result := Self;
end;

function TTask.ui(proc: TProcGlb): TTask;
begin
if forkTemp <> nil then
begin
ShowCoreErrorMsg('开发人员书写错误,不可以重复使用ui');
end;

forkTemp := TTask.Create;
forkTemp.tkind := TK_UI;
forkTemp.pkind := PK_GLB;
forkTemp.procGlb := proc;

Result := Self;
end;

function TTask.ui(proc: TProcObj): TTask;
begin
if forkTemp <> nil then
begin
ShowCoreErrorMsg('开发人员书写错误,不可以重复使用ui');
end;

forkTemp := TTask.Create;
forkTemp.tkind := TK_UI;
forkTemp.pkind := PK_OBJ;
forkTemp.procObj := proc;

Result := Self;
end;

function TTask.data(data: TArray): TTask;
var
key: string;
len, i: Integer;
begin
if forkTemp = nil then
begin
ShowCoreErrorMsg('开发人员书写错误,应该先调用bg/ui才能继续!');
end;

//禁止task.data().data().data()这种写法一次性写入提高效率task.data(多行写入更加直观,效率也高)
if forkTemp.FData <> nil then
begin
ShowCoreErrorMsg('开发人员书写错误,禁止data().data()这种不直观语法,请核查!');
end;

len := Length(data);
if len = 0 then
begin
//这里不报错,直接退出,有时候为了方便调式会注释掉data里的参数
Exit(Self);
end;

//判断能否被2整除
if (len mod 2) <> 0 then
begin
ShowCoreErrorMsg('开发人员书写错误,数据参数必须成对输入,请核查!');
end;

//开始创建容器,循环一次性写入,当不使用data的时候,容器是为nil的,提升效率
forkTemp.FData := TDictionary<string, TValue>.Create;
for i := 0 to (len - 1) do
begin
if (i mod 2) = 0 then
begin
key := data[i].AsString;
end
else
begin
//判断value是否是支持的类型
case data[i].Kind of
//string会自动匹配这个
tkUString, tkInteger,
//Boolean会是这个
tkEnumeration,
//经过测试Int64和UInt64都会匹配到这个,可以参考我博客上的文章,结果是正常的
tkInt64,
//带小数的会是这个
tkFloat:
forkTemp.FData.Add(key, data[i]);
else
ShowCoreErrorMsg('开发人员书写错误,数据参数仅支持String、Integer、Boolean、Int64、UInt64、Currency、Extended,请核查!');
end;
end;
end;

//完全成功时,返回自己
Result := Self;
end;

function TTask.scopes(scopes: TArray): TTask;
begin
if forkTemp = nil then
begin
ShowCoreErrorMsg('开发人员书写错误,应该先调用bg/ui才能继续!');
end;

if forkTemp.FScopes <> nil then
begin
ShowCoreErrorMsg('开发人员书写错误,禁止scopes().scopes()这种不直观语法,请核查!');
end;

if Length(scopes) = 0 then
begin
//这里不报错,直接退出,有时候为了方便调式会注释掉scopes里的参数
Exit(Self);
end;

//这里先把窗体的指针存入协程,并不验证窗体的指针是否正常,验证的操作交给start,
//因为start需要窗体也挂载上来协程的指针,就好比进入正式环境一样,协程对窗体的
//指针挂载只是临时,且只有start那里能终止执行;
forkTemp.FScopes := TList.Create;
for var f in scopes do
begin
if f = Application.MainForm then
begin
raise Exception.Create('开发人员错误,scopes(主窗体)是没有意义的,主窗体关闭就是程序关闭,这样写是多余的,禁止这样写,请去除主窗体!');
end;
forkTemp.FScopes.Add(f);//这里不需要加锁来添加了,因为还没有开始进入执行阶段
end;

//返回自己
Result := Self;
end;

function TTask.interval(interval: Int64): TTask;
begin
if forkTemp = nil then
begin
ShowCoreErrorMsg('开发人员书写错误,应该先调用bg/ui才能继续!');
end;

if Self.tkind <> TK_ROOT then
begin
ShowCoreErrorMsg('开发人员书写错误,重复执行的协程,只能从根节点衍生!');
end;

//这里不用写了,统一在 .start;root 衍生那里写,这里只是提示下,另外 rpt协程首次也是直接入执行队列的,并不先入 rptList 容器;
// if not isMainThread then
// begin
// ShowCoreErrorMsg('开发人员书写错误,重复执行的协程,只能在主线程里衍生!');
// end;

forkTemp.isRpt := True;
forkTemp.rptInterval := interval;

Result := Self;
end;

function TTask.start: string;
begin
{
边取消,边新增的问题;举例:上层协程在取消,而此协程在生成,你可能会误解的认为,
此时取消和衍生这2个线程再抢这个childsLock,然后又分为谁先拿到锁2种情况,其实不是这样的,
我们的取消是接力棒,若当前协程正在执行,另一个取消线程发现是DOING时,就已经退出了,
取消的操作交给了本线程;所以不存在取消线程与本线程 抢childsLock的问题;且取消是 tryEnter
拿不到锁就退出了,是最大化取消,不是绝对全部取消,不会出现抢锁的问题;
这里要判断下当前协程是否被取消,若被取消了就不要再新增分支了,由于当前协程正在做,若外部
取消只可能是 doing -> canceling 一个协程,已经被其它线程该为 canceling了;此时 协程内部若
需要衍生出大量的子协程的话,应该使用 isCancel方法,及时跳出循环,不再衍生;若开发者没有使用isCancel,
则只在协程的start这里进行拦截,此协程不入执行队列;生成还是照样生成,就是不入队列,生成了 -> 又会
被立即释放,因为A:取消不是常规操作,又加上 B: 此协程需要衍生大量子协程; 又加上 C 开发
者又没有使用isCancel;A + B + C 三个因素情况少见,没有在协程的创建 --> start 全链路都加
if state = canceling 的判断,代码冗余,不利于维护;
}
if state = ST_CANCELING then
begin
//已经创建了的,这里free下,然后再退出,避免有内存泄漏
if forkTemp <> nil then
begin
FreeAndNil(forkTemp);
end;
Exit;
end;

if forkTemp = nil then
begin
ShowCoreErrorMsg('开发人员书写错误,应该先调用bg/ui才能继续!');
end;

//校验作用域,并不检查窗体悬空指针的问题,悬空指针是开发人员的问题,后期观察下 什么情况下会出现悬空指针,从开发人员上杜绝;
if forkTemp.FScopes <> nil then
begin
for var frm in forkTemp.FScopes do
begin
//判断窗体是否处于关闭中,只有主线程可以写窗体的状态,因为关闭操作是windows消息嘛,故不存在多线程的问题;
if frm.isFreeIng then
begin
{
coreErrorShow('开发人员逻辑错误,窗体实例已在关闭中!');
这里并不报错,举例:一个窗体上 有一个 开始按钮,衍生100个协程,然后窗体关闭,应该停止衍生新协程;而不是报错;
}
FreeAndNil(forkTemp); //有一个作用域退出,就不执行,由于是for循环里,这里是要退出,就退出了;
Exit;
end else begin
//动态创建提高效率,并不是所有协程都有作用域
frm.tasksLock.Enter;
if frm.tasks = nil then
begin
frm.tasks := TDictionary<string, TTask>.Create;
end;
//把协程的指针也挂载到form上
frm.tasks.Add(forkTemp.id, forkTemp);
frm.tasksLock.Leave;
end;
end;
end;

{
2.边新增边回收的问题,根本就不会存在这个问题,因为GC线程的回收一定是先判断当前协程的状态
若发现当前协程是Doing是不会去回收的;下游的协程完毕后,GC线程会从本协程的childs里删除
指针关系,故会与当前线程来同时操作childs,所以这里要加锁
}
//动态创建,提高效率,并不是每一个协程都有分叉
childsLock.Enter; //由于gc也在运行着,即使节点还没有slb,操作本节点childs的话要加锁
if childs = nil then
begin
childs := TDictionary<string, TTask>.Create;
end;
childs.Add(forkTemp.id, forkTemp);
childsLock.Leave;

//建立父子关系,这里由于还没有开始执行此协程,故这里不存在多线程有关的任何问题
forkTemp.parent := Self;

{
衍生协程,根据当前协程类型 + 衍生出来的协程类型 分为4种情况:
1. bg -- bg
2. bg -- ui
3. ui -- bg
4. ui -- ui
}
//根据当前协程类型 + 衍生出的新的协程类型,这样一个关系做出处理
case task.tkind of
TK_ROOT: begin

  //通常是开发者的问题,避免 新建一个后台线程或匿名线程,然后在里面使用 task.bg/ui就会导致这个问题;
  if not IsMainThread then
  begin
    ShowCoreErrorMsg('内部核心错误,请联系开发者,ROOT在非主线程中!');
  end;

  {
    通常需要先有窗体,task.scopes 才能进行,这个支持协程的窗体就是为了协程而服务的,所以创建协程窗体也要启动协程;
    1. 创建协程窗体时,启动协程;
    2. 不创建窗体,直接 task.bg.start;start这里分支第一个协程时 也要启动协程;
  }
  if not TASK_IS_RUNING then
  begin
    log('协程启动中...');
    enableTaskThreads;
  end;

  //新建的重复协程,首次也是立即入队列执行的,并不是先入 rptList容器,而是直接入执行队列,所以不用考虑是否是重复协程的问题;
  case forkTemp.tkind of
    TK_BG: gotoBgWaitQueue(forkTemp);

    TK_UI: begin
      //写入主线程容器,这个写入操作,点进入源码你会看到他用了TMonitor锁,我们不用在加锁了
      //这里之所以使用 ForceQueue 是因为,若UI协程A,被主线程运行时,又需要衍生N个子协程,
      //需要新衍生的这些子协程,有的是UI,有的又是BG,若同步执行UI,那么会导致BG会晚衍生,
      //从而浪费多核并发的优势,举例:需要UI_A,运行的过程中,需要衍生出UI_A1, UI_A2,BG_A3,
      //若同步执行,则BG_A3,需要前2个都执行完毕后,才会衍生出,用ForceQueue的话,A3可能会优于
      //A1,A2执行完毕;显然我们需要异步;
      TThread.ForceQueue(nil, forkTemp.outNoArgProc);
    end;
  end;
end;

TK_BG: begin
  case forkTemp.tkind of
    TK_BG: begin
      //优先进入自己的本地队列,本地满写全局
      var wkth := wkThreads.Items[Self.wkThreadIndex];
      wkth.lcWaitQueueLock.Enter;
      if wkth.lcWaitQueue.Count < wkth.lcWaitQueue.Capacity then
      begin
        forkTemp.wkThreadIndex := Self.wkThreadIndex;
        wkth.lcWaitQueue.Enqueue(forkTemp);
        wkth.lcWaitQueueLock.Leave;
      end else begin
        wkth.lcWaitQueueLock.Leave; //先退出本地锁

        //开始写入全局队列
        glWaitQueueLock.Enter;
        glWaitQueue.Enqueue(forkTemp);
        glWaitQueueLock.Leave;
      end;

      AtomicIncrement(WAIT_BG_COUNT); //先入队再 + 1,无论是写入本地还是全局这里都新增 1
    end;

    //这里只需加入队列就可,因为当前一定是bg线程了,不用ForceQueue
    TK_UI: TThread.Queue(nil, forkTemp.outNoArgProc);
  end;
end;

TK_UI: begin
  case forkTemp.tkind of
    TK_BG: gotoBgWaitQueue(forkTemp);

    //当前是主线程用 ForceQueue
    TK_UI: TThread.ForceQueue(nil, forkTemp.outNoArgProc);
  end;
end;

end;

//将当前指针置为nil,上面的指针是入栈,已经存入相应的容器内部,这里指针重置为nil,方便继续衍生;
forkTemp := nil;
end;

procedure TTask.Free;
begin
inherited;
end;

constructor TTaskForm.CreateParented(ParentWindow: HWnd);
begin
raise Exception.Create('开发人员错误,禁止使用 TTaskForm.CreateParented 来创建窗体!');
end;

class function TTaskForm.CreateParentedControl(ParentWindow: HWND): TWinControl;
begin
raise Exception.Create('开发人员错误,禁止使用 TTaskForm.CreateParentedControl 来创建窗体!');
end;

constructor TTaskForm.CreateNew(AOwner: TComponent; Dummy: Integer = 0);
begin
raise Exception.Create('开发人员错误,禁止使用 TTaskForm.CreateNew 来创建窗体!');
inherited;
end;

constructor TTaskForm.CreateScaledNew(AOwner: TComponent; ADPI: Integer = 0; Dummy: Integer = 0);
begin
raise Exception.Create('开发人员错误,禁止使用 TTaskForm.CreateScaledNew 来创建窗体!');
inherited;
end;

constructor TTaskForm.Create(AOwner: TComponent);
begin
if TThread.Current.ThreadID <> MainThreadID then
begin
raise Exception.Create('开发人员错误,创建窗体必须在主线程里进行!');
end;

var isMainForm := False;
//主窗体的时候 Application.MainForm = nil的,不进行校验
if Application.MainForm <> nil then
begin
// aowner 必须为 nil、TTaskForm、控件(如panel,但要求 panel.owner 必须为 TTaskForm)
if (AOwner <> nil) and (AOwner <> Application.MainForm) then
begin
if (not (AOwner is TTaskForm)) and (not (AOwner.Owner is TTaskForm)) then
begin
raise Exception.Create('开发人员错误,AOwner 的类型范围必须为:nil、TTaskForm、AOwner.Owner = TTaskForm 这三种情况,请核查!');
end;
end;
end else begin
isMainForm := True;
{
通常需要先有窗体,task.scopes 才能进行,这个支持协程的窗体就是为了协程而服务的,所以创建协程窗体也要启动协程;
1. 主窗体启动时候,启动协程,有的时候,有登录窗体,登录窗体是在主窗体之前的,登录窗体可能已经把协程启动了,
所以这里还是要判断下
2. 创建协程窗体时,启动协程;
3. 不创建窗体,直接 task.bg.start;start这里分支第一个协程时 也要启动协程;
}
if not TASK_IS_RUNING then
begin
log('协程启动中...');
enableTaskThreads;
end;
end;

inherited Create(AOwner);

//生成一个GUID
Self.id := TGUID.NewGuid.ToString;
//为了使用上不报错,要求锁必须创建时初始化
tasksLock := TCriticalSection.Create;
//只要是TaskForm,就统一入容器,因为 用 taskForm就意味着要用协程机制,所以要加入协程的容器
if not isMainForm then //主窗体不加入这个容器
begin
glTaskFormMap.Add(Self.id, Self);
end;
end;

procedure TTaskForm.SetParent(AParent: TWinControl);
begin
//parent 必须为 nil、控件(如panel,但要求 panel.owner 必须为 TTaskForm); parent := TTaskForm 或 TForm 没有意义 禁止
if AParent <> nil then
begin
if (AParent is TTaskForm) or (AParent is TForm) then
begin
raise Exception.Create('开发人员错误,Parent := TTaskForm 或 TForm 是没有意义的。起码应该把窗体放进一个容器里,而不是直接窗体;故禁止操作!');
end;

if not (AParent.Owner is TTaskForm) then
begin
  raise Exception.Create('开发人员错误,Parent 必须为 TTaskForm 里的容器,而不是普通的 TForm,请将 Parent对应的容器的 form 继承 TTaskForm!');
end;

end;
inherited;
end;

destructor TTaskForm.Destroy;
begin
//看下是否有挂载

tasksLock.Free;
//taskMap是动态的,默认是nil,当调用.scope的时候才会创建,所以这里判断下
if tasks <> nil then
begin
tasks.Free;
end;

//记录窗体真被释放了,方便调试
log(Self.Caption + ' ----- 真被释放了!');

inherited;
end;

procedure TTaskForm.Free;
begin
//要让开发人员养成良好的编程习惯,故这里不删除
if not isMainThread then
begin
ShowCoreErrorMsg('开发人员错误,销毁窗体必须在主线程里进行!');
end;

{
关于窗体的释放,一共有 parent 和 owner 两个元素 影响释放,分为4种情况:
1. parent(no) + owner(no),比如 弹出窗体场景:

    with TTaskForm.Create(nil) do
    begin
      ShowModel;
      Free; //走到 free时,用户肯定点击了 frm.close; 所以窗体是已经隐藏了的
    end;

2. parent(yes) + owner(no),这个少见,与 parent(yes) + owner(yes) 一样,比如:创建一个窗体嵌入另一个窗体的panel上

    with TTaskForm.Create(nil) do
    begin
      parent := panel1;
      Show;
      //Free; 这里写不写 Free 都没有问题,因为指定了 parent 不写 free,最终也会调用 destroy,随 parent;
    end;

3. parent(yes) + owner(yes);与2 效果一样,比如:创建一个窗体嵌入另一个窗体的panel上,这个比较常用,main 选项卡就是用的这个;

    with TTaskForm.Create(taskform123) do
    begin
      parent := panel1;
      Show;
      //Free; 这里写不写 Free 都没有问题,因为指定了 parent 不写 free,最终也会调用 destroy,随 parent;
    end;

4. parent(no) + owner(yes)  这个更加少见,创建一个窗体 随另一个窗体释放,但却不再另一个窗体的容器里展示;奇怪 比较少见;

----------------------------------

2、3、4 这3种情况,都是把自己挂载到另一个 TTaskForm上,那么就意味着 最顶层的 TTaskForm.free 一定先执行,就是说 这4种 情况,都是

TTaskForm.Free 在先,然后 gc 回收 task 然后 通知 main 再回收 form.destroy,可见 free 一定是在 destroy 前的,我们把主窗体 也设计成 TTaskForm,
因为主窗体也要挂载其它 TTaskForm,所以 最顶层的一定是 TTaskForm.free 在先;主窗体那里 我们看下 如何做到 主窗体 关闭的时候,先调用 mainForm.free;

}

//开始递归,parent 或 owner 只要指定一个,就会向下递归
freeTaskForm(Self);
end;

procedure TTaskFormTabsBar.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
//逐步向下条件,直到满足所有条件,总之 inherited 一定会被执行一次;让父类做一些其它的重置操作;

//1.不是左键就调用父类的方法
if Button <> mbLeft then
begin
inherited;
Exit;
end;

//2.若根本都没有展示关闭按钮,则也是调用父类的然后退出
if not Self.TabOptions.ShowCloseButton then
begin
inherited;
Exit;
end;

//3.获取鼠标点击的 tab,获取不到 说明不是在tab点击的,而是在 tabbar空白区域点击的
var tab := Self.TabFromPoint(TPoint.Create(x, y));
if tab = nil then
begin
inherited;
Exit;
end;
var frm := tab.Form;

//4.看下点击的是否是 tab右上方的关闭按钮
if tab.CloseButtonMouseIn then
begin
//去除 form 的 onclose事件,不允许开发人员 干涉选项卡的关闭,就好比浏览器的选项卡,禁止程序员让其不关闭一样;
frm.OnClose := nil;
frm.OnCloseQuery := nil;

{
  先调用官方的,进行一些后置操作,其实官方的 就是 form.close; close 就是 执行用户自定义的 onclose事件代码 + hide 当前窗体
  onclose事件我们已经去除了,其实官方的就是 FMouseCaptureTab.FCloseButtonMouseDown := False; FMouseCaptureTab := nil;等一些
  重置操作;
}
inherited;

//由于 hide ,还是存在,会导致 tab上 还是有这个窗体,我们直接从 tab上摘除这个窗体
Self.DeleteForm(frm);

//然后调用窗体的free方法;1. 修改状态 isFreeIng = true; 2. 解除 parent 和 owner关系; 3. 取消协程; 然后什么都不做,并不真正释放,而是让主线程来定时释放,这是一个假 free 动作;
TTaskForm(frm).Free;

end else begin
//不是关闭窗体,就只交给父类就行了
inherited;
end;
end;

///


/// 尽量放到 implementation 末尾
///

procedure Register;
begin
RegisterComponents('ZhongXia', [TTaskFormTabsBar]);
end;

initialization

{
创建根节点:
1.有了根节点,开发者才能使用 task
3.根节点不加入队列,则不会执行,无需proc kind state 等。
3.根节点的作用域为 nil,不创建。
4.根节点可以写入一些根部数据,方便子节点回溯时,查到一些基础数据,这里暂时没有想到要写入哪些数据。
}
task := TTask.Create;
task.tkind := TK_ROOT;

finalization

{
走到这里说明程序关闭了,而程序的关闭是Form右上方的关闭按钮或其他,总之Windows需要
发送关闭进程的消息给程序,关闭程序进程操作系统会回收进程资源,所有我们不用担心内存
泄漏什么的,但是假如我们的程序有泄漏,而我们自己又不知道,在程序不关闭的情况下可能
耗完内存,所以我们通常会 ReportMemoryLeaksOnShutdown := True; 让程序关闭的时候提示
一下内存泄漏,方便我们调试; 我们应该在程序关闭时,清空所有我们应该清空的内存,不要
让其关闭时,报内存泄漏错误;

另外留意:主线程 是有2个队列的,一个是windows消息队列,一个是我们的协程队列,一旦
windows消息队列执行到关闭程序这个消息了,其实协程队列是不会执行的了,因为主线程优先
处理Windows消息队列,空闲的时候才会处理协程队列,而消息队列是个关闭程序的消息,代码
会走到这里,根本不会去执行协程队列;所以可以在这里终结所有后台线程,一切交给主线程
来断后,做一些清理工作,遍历整个协程树全部释放;
}

//=================== 反过来,先释放线程,再释放容器 ====================

//若当前协程机制正在运行,才有线程可以释放,默认协程机制是关闭的
if TASK_IS_RUNING then
begin
//这里很多线程还都在运行着,要原子操作写
TInterlocked.Exchange(TASK_IS_RUNING, False);

//WaitFor若线程已经结束会立即返回,没有结束会阻塞在这里,看博客;
//先停止 GC 和 rpt 线程
gcThread.WaitFor;
rptThread.event.SetEvent;
rptThread.WaitFor;
for var wkth in wkThreads do
begin
  wkth.WaitFor;
end;

//线程都停下来后,再统一释放,避免 wk线程 互相偷同事的问题,还都没停下来就释放,
//导致A去偷B的,结果B的容器释放了的问题
gcThread.Free;
rptThread.Free;
for var wkth in wkThreads do
begin
  wkth.Free;
end;
wkThreads.Free;

//开始释放容器,这里就没有必要加锁了
glWaitQueue.Free;
glWaitQueueLock.Free;

glRptList.Free;
glRptListLock.Free;

mainDoneMap.Free;
mainDoneMapLock.Free;
rptDoneMap.Free;
rptDoneMapLock.Free;

{
  glTaskFormMap 的释放,流程是:主窗体Close ---> 主窗体Destroy ---> owner或parent在自己上的子窗体 destroy ---> 走到本单元的finalization;
  由于走到本单元的 finalization 时,凡是主窗体与选项卡还没有解绑的,实际上已经被main窗体连带释放,所以 glTaskFormMap的
  释放会有问题,若关闭主程序则先解绑 主窗体与选项卡的绑定,然后再走下面的路程来释放 所有 TTaskForm;
  就是说 glTaskFormMap 容器里 不能存在 2种生命周期的窗体,走到这里时必须统一管理;否则这里代码是没法写的;
  我们已经在 enableTaskThreads 协程启动时,给主窗体绑定事件,关闭主窗体时,先解除与所有TTaskForm的绑定,避免关闭主窗体
  这些选项卡一并随主窗体释放了,再去遍历 glTaskFormMap 释放元素时会有异常;Application.MainForm.OnDestroy := task.mainDestroy;
  这里可以统一直接释放了;
}
for var frm in glTaskFormMap.Values do
begin
  frm.Free; //先全部 free 下,解除互相之间的关系,避免连带释放;
end;
for var frm in glTaskFormMap.Values do
begin
  frm.Destroy; //调用的是 destroy ,因为 free 是假free;
end;
glTaskFormMap.Free;

end;

//最后释放整颗协程树,从root开始向下递归,包含 root,这句是无论协程是否运行都得有这句,因为肯定有 root 存在
terminateTask(task);

end.

标签:begin,end,协程,Create,窗体,线程,adfadf
From: https://www.cnblogs.com/del88/p/18617765

相关文章