mormot.core.threads.pas unit

mormot.core.threads.pas unit

Purpose: Framework Core Multi-Threading Support
- this unit is a part of the Open Source Synopse mORMot framework 2, licensed under a MPL/GPL/LGPL three license - see LICENSE.md

目的:框架核心多线程支持

  • 本单元是开源Synopse mORMot框架2的一部分,根据MPL/GPL/LGPL三重许可进行许可-参见LICENSE.md

1.1. Units used in the mormot.core.threads unit (在mormot.core.threads单元中使用的单元)

Unit Name Description
mormot.core.base Framework Core Shared Types and RTL-like Functions 框架核心共享类型和类似RTL的函数
mormot.core.buffers Framework Core Low-Level Memory Buffer Process 框架核心低级内存缓冲区处理
mormot.core.data Framework Core Low-Level Data Processing Functions 框架核心低级数据处理函数
mormot.core.json Framework Core Low-Level JSON Processing 框架核心低级JSON处理
mormot.core.log Framework Core Logging 框架核心日志记录
mormot.core.os Framework Core Low-Level Wrappers to the Operating-System API 框架核心对操作系统API的低级包装器
mormot.core.perf Framework Core Performance and Monitoring Classes 框架核心性能和监视类
mormot.core.rtti Framework Core Low-Level Cross-Compiler RTTI Definitions 框架核心低级跨编译器RTTI定义
mormot.core.text Framework Core Low-Level Text Processing 框架核心低级文本处理
mormot.core.unicode Framework Core Low-Level Unicode UTF-8 UTF-16 Ansi Conversion 框架核心低级Unicode UTF-8 UTF-16 Ansi转换
mormot.core.variants Framework Core Low-Level Variants / TDocVariant process 框架核心低级变量/TDocVariant处理

1.2. mormot.core.threads class hierarchy

TSynThread
TSynThreadPoolWorkThread
TNotifiedThreadTLoggedThread
TLoggedWorkThreadTThreadAbstract
TSynBackgroundThreadAbstract
TSynBackgroundThreadProcess
TSynBackgroundTimer
TSynBackgroundThreadMethodAbstract
TSynParallelProcessThread
TSynBackgroundThreadProcedure
TSynBackgroundThreadMethod
TSynBackgroundThreadEventTThread
TSynPersistentStoreTSynQueue
TSynPersistentLockTSynParallelProcess
TSynPersistent
TBlockingProcessPool
TBlockingProcess
TBlockingProcessPoolItem
TSynEventTObject
TSynThreadPool
TPendingTaskList
TInterfacedObjectWithCustomCreate
TLockedDocVariantIInterface
ILockedDocVariant
ESynException
ESynThread

mormot.core.threads class hierarchy

1.3. Objects implemented in the mormot.core.threads unit (mormot.core.threads 单元中实现的对象)

Objects Description
ESynThread Exception class raised by this unit 由本单元引发的异常类
ILockedDocVariant Ref-counted interface for thread-safe access to a TDocVariant document 用于线程安全访问TDocVariant文档的引用计数接口
TBlockingProcess A semaphore used to wait for some process to be finished 一个计数信号量,用于等待某个进程完成
TBlockingProcessPool Manage a pool of TBlockingProcessPoolItem instances 管理一组TBlockingProcessPoolItem实例
TBlockingProcessPoolItem A semaphore used in the TBlockingProcessPool 在TBlockingProcessPool中使用的计数信号量
TLockedDocVariant Allows thread-safe access to a TDocVariant document 允许线程安全地访问TDocVariant文档
TLoggedThread Abstract class to implement a thread with logging notifications 实现带有日志通知的线程的抽象类
TLoggedWorkThread A class able to run some process in a background thread 能够在后台线程中运行某些进程的类
TNotifiedThread Abstract class to implement a thread with start/stop notifications 实现带有启动/停止通知的线程的抽象类
TPendingTaskList Thread-safe list of tasks, stored as RawByteString, with a timestamp 线程安全的任务列表,以RawByteString形式存储,带有时间戳
TPendingTaskListItem Internal item definition, used by TPendingTaskList storage TPendingTaskList存储的内部项目定义
TSynBackgroundThreadAbstract Abstract TThread with its own execution content 带有自己的执行内容的TThreadAbstract
TSynBackgroundThreadEvent Allow background thread process of a method callback 允许在后台线程中处理方法回调
TSynBackgroundThreadMethod Allow background thread process of a variable TThreadMethod callback 允许在后台线程中处理可变TThreadMethod回调
TSynBackgroundThreadMethodAbstract Abstract TThread able to run a method in its own execution content 能够在自己的执行内容中运行方法的抽象TThread
TSynBackgroundThreadProcedure Allow background thread process of a procedure callback 允许在后台线程中处理过程回调
TSynBackgroundThreadProcess TThread able to run a method at a given periodic pace 能够在给定周期速率下运行方法的TThread
TSynBackgroundTimer TThread able to run one or several tasks at a periodic pace in a background thread 能够在后台线程中以周期速率运行一个或多个任务的TThread
TSynBackgroundTimerTask Used by TSynBackgroundTimer internal registration list 用于TSynBackgroundTimer内部注册列表
TSynParallelProcess Allow parallel execution of an index-based process in a thread pool 允许在线程池中并行执行基于索引的进程
TSynParallelProcessThread Thread executing process for TSynParallelProcess 为TSynParallelProcess执行进程的线程
TSynQueue Thread-safe FIFO (First-In-First-Out) in-order queue of records 线程安全的FIFO(先入先出)有序记录队列
TSynThread A simple TThread with a "Terminate" event run in the thread context 一个简单的TThread,带有在线程上下文中运行的"Terminate"事件
TSynThreadPool A simple Thread Pool, used e.g. for fast handling HTTP/1.0 requests 一个简单的线程池,例如用于快速处理HTTP/1.0请求
TSynThreadPoolWorkThread Defines the work threads used by TSynThreadPool 定义TSynThreadPool使用的工作线程
TThreadAbstract Abstract parent of all TThread inherited classes 所有TThread继承类的抽象父类

1.3.1. ESynThread

ESynThread = class(ESynException)

本单元引发的异常类


1.3.2. TSynQueue

TSynQueue = class(TSynPersistentStore)

*线程安全的FIFO(先入先出)记录有序队列

  • 内部使用TDynArray存储,具有滑动算法,比FPC或Delphi的TQueue或简单的TDynArray.Add/Delete更高效
  • 如果需要,支持TSynPersistentStore二进制持久化
  • 该结构在设计上也是线程安全的*

constructor Create(aTypeInfo: PRttiInfo; const aName: RawUtf8 = ''); reintroduce; virtual;

*初始化队列存储

  • aTypeInfo应是一个动态数组的TypeInfo() RTTI指针,它将在这个TSynQueue实例中存储值
  • 可以为这个实例可选地分配一个名称*

destructor Destroy; override;

*销毁存储


function Capacity: integer;

*返回当前在内存中预留的槽位数

  • 队列具有优化的自动调整大小算法,可以使用此方法返回其当前capacity
  • 此方法不是线程安全的,因此返回的值仅具有指示性*

function Count: integer;

*返回当前存储在此队列中的项数

  • 此方法不是线程安全的,因此返回的值要么具有指示性,要么应使用显式的安全锁定/解锁
  • 如果要检查队列是否为空,请调用Pending*

function Peek(out aValue): boolean;

*从队列中按FIFO(先入先出)方式查找一个项

  • 如果aValue已被填充了一个pending项,而不从队列中删除它(如Pop方法所做的),则返回true
  • 如果队列为空,则返回false
  • 此方法是线程安全的,因为它将锁定实例*

function Pending: boolean;

*如果队列中有一些当前pending的项,则返回true

  • 比检查Count=0更快,并且比PopPeek快得多
  • 此方法不是线程安全的,因此返回的值仅具有指示性*

function Pop(out aValue): boolean;

*从队列中按FIFO(先入先出)方式提取一个项

  • 如果aValue已被填充了一个pending项,并且该项已从队列中删除(如果不想删除它,请使用Peek),则返回true
  • 如果队列为空,则返回false
  • 此方法是线程安全的,因为它将锁定实例*

function PopEquals(aAnother: pointer; aCompare: TDynArraySortCompare; out aValue): boolean;

*从队列中按FIFO(先入先出)方式提取一个匹配的项

  • 当前pending项与aAnother值进行比较*

function WaitPeekLocked(aTimeoutMS: integer; const aWhenIdle: TThreadMethod): pointer;

*等待从队列中按FIFO(先入先出)方式查找一个项

  • 在aTimeoutMS时间内返回一个指向pending项的指针
  • 保持Safe.ReadWriteLock,因此调用者可以检查其内容,然后如果它是预期的项,则调用Pop(),并最终调用Safe.ReadWriteUnlock
  • 如果在时间内没有将任何内容推入队列,则返回nil
  • 此方法是线程安全的,但仅在需要时锁定实例*

function WaitPop(aTimeoutMS: integer; const aWhenIdle: TThreadMethod; out aValue; aCompared: pointer = nil; aCompare: TDynArraySortCompare = nil): boolean;

*等待并从队列中按FIFO(先入先出)方式提取一个项

  • 如果在指定的aTimeoutMS时间内aValue已被填充了一个pending项,则返回true
  • 如果在时间内没有将任何内容推入队列,或者已调用WaitPopFinalize,则返回false
  • aWhenIdle可以被分配,例如给VCL/LCL Application.ProcessMessages
  • 可以在返回之前可选地比较pending项(例如,当多个线程将项放入队列时可以使用)
  • 此方法是线程安全的,但仅在需要时锁定实例*

procedure Clear;

*删除当前存储在此队列中的所有项,并清空其capacity

  • 此方法是线程安全的,因为它将锁定实例*

procedure Push(const aValue);

*将一个项存储到队列中

  • 此方法是线程安全的,因为它将锁定实例*

procedure Save(out aDynArrayValues; aDynArray: PDynArray = nil); overload;

*使用存储的队列项初始化一个动态数组

  • aDynArrayValues应是一个在Create中定义的aTypeInfo变量
  • 可以检索一个可选的TDynArray包装器,例如用于二进制或JSON持久化
  • 此方法是线程安全的,并将复制队列数据*

procedure WaitPopFinalize(aTimeoutMS: integer = 100);

*确保任何pending或未来的WaitPop()方法立即返回false

  • 总是由Destroy析构函数调用
  • 也可以从UI的OnClose事件中调用,以避免任何锁定
  • 此方法是线程安全的,但仅在需要时锁定实例*

1.3.3. TPendingTaskListItem

TPendingTaskListItem = packed record

内部项目定义,由TPendingTaskList存储使用


Task: RawByteString;

相关联的task,以原始二进制形式存储


Timestamp: Int64;

当TPendingTaskList.GetTimestamp达到此值时,应执行task


1.3.4. TPendingTaskList

TPendingTaskList = class(TObject)

*线程安全的任务列表,以RawByteString形式存储,并带有时间戳

  • 你可以向内部列表添加任务,以在给定延迟后执行,使用类似于post/peek的算法
  • 执行延迟预计不准确,但根据每次NextPendingTask调用和GetTimestamp解析度进行最佳猜测*

constructor Create; reintroduce;

默认返回GetTickCount64来初始化列表内存和资源


function NextPendingTask: RawByteString; virtual;

*检索下一个待处理task

  • 如果当前没有计划中的task,则返回''
  • 返回与指定延迟相对应的下一个堆栈*

procedure AddTask(aMilliSecondsDelayFromNow: integer; const aTask: RawByteString); virtual;

从当前时间开始,指定毫秒延迟后Append一个task


procedure AddTasks(const aMilliSecondsDelays: array of integer; const aTasks: array of RawByteString);

*指定任务之间的毫秒延迟后Append多个任务

  • 第一个提供的延迟将从当前时间开始计算,然后它将指定等待下一个提供task需要多长时间——也就是说,aMilliSecondsDelays不是绝对延迟*

procedure Clear; virtual;

清除所有待处理任务


property Count: integer read GetCount;

当前定义了多少个待处理任务


property Task: TPendingTaskListItemDynArray read fTask;

*直接低级访问内部task列表

  • 警告:此动态数组的长度是列表容量:请使用Count属性来检索存储的确切项数
  • 使用try ... finally Safe.Unlock块中的Safe.Lock/TryLock进行线程安全访问此数组
  • 项目按递增的Timestamp存储,即第一个项目是NextPendingTask方法将返回的下一个项目*

property Timestamp: Int64 read GetTimestamp;

*访问内部TPendingTaskListItem.Timestamp存储的值

  • 对应当前时间
  • 默认实现是返回GetTickCount64,在Windows下典型分辨率为16毫秒*

1.3.5. ILockedDocVariant

ILockedDocVariant = interface(IInterface)

*用于线程安全访问TDocVariant文档的引用计数接口

  • 例如,由TLockedDocVariant实现,用于IoC/DI解析
  • 快速且安全地存储任何类似JSON的对象,作为属性/值对,或类似JSON的数组,作为值*

function AddExistingProp(const Name: RawUtf8; var Obj: variant): boolean;

*将现有属性value添加到给定的TDocVariant文档对象

  • 如果Name存在,则返回TRUE并将Name/Value对添加到Obj
  • 如果存储的文档中不存在Name,则返回FALSE
  • 此方法在查找Name期间会使用lock,但无论是否返回FALSE,总是会释放lock(参见AddExistingPropOrLock)*

function AddExistingPropOrLock(const Name: RawUtf8; var Obj: variant): boolean;

*将现有属性value添加到给定的TDocVariant文档对象

  • 如果Name存在,则返回TRUE并将Name/Value对添加到Obj,使用内部lock确保线程安全
  • 如果存储的文档中不存在Name,则返回FALSE并锁定内部存储:调用者最终应通过AddNewPropAndUnlock()释放lock
  • 可以这样使用,以实现线程安全的缓存:
if not cache.AddExistingPropOrLock('Articles',Scope) then
  cache.AddNewPropAndUnlock('Articles',GetArticlesFromDB,Scope);

这里GetArticlesFromDB会在主要lock内部执行


function Copy: variant;

对内部TDocVariant文档对象或数组进行线程安全的复制


function Exists(const Name: RawUtf8; out Value: Variant): boolean;

*按名称检查和返回一个给定的属性

  • 如果找到Name,则返回TRUE并将与提供的Name相关联的value填充到Value中,使用内部lock确保线程安全
  • 如果未找到Name,则返回FALSE并释放内部lock:如果你想要添加缺失的value,请使用ExistsOrLock()*

function ExistsOrLock(const Name: RawUtf8; out Value: Variant): boolean;

*按名称检查和返回一个给定的属性

  • 如果找到Name,则返回TRUE并将与提供的Name相关联的value填充到Value中,使用内部lock确保线程安全
  • 如果Name不存在,则返回FALSE并设置内部lock:调用者随后应通过ReplaceAndUnlock()释放lock*

function Lock: TAutoLocker;

对关联的线程安全互斥锁进行低级访问


function ToJson(HumanReadable: boolean = false): RawUtf8;

将存储的值保存为UTF-8编码的JSON对象


procedure AddItem(const Value: variant);

*将value添加到内部TDocVariant文档数组


procedure AddNewProp(const Name: RawUtf8; const Value: variant; var Obj: variant);

*将属性value添加到给定的TDocVariant文档对象

  • 此方法在调用时不会期望资源被锁定,与AddNewPropAndUnlock不同
  • 将使用内部lock确保线程安全
  • 如果Name已存在,将更新/更改现有的value
  • 可以这样使用,以实现线程安全的缓存:
if not cache.AddExistingProp('Articles',Scope) then
  cache.AddNewProp('Articles',GetArticlesFromDB,Scope);

这里GetArticlesFromDB会在主lock之外执行


procedure AddNewPropAndUnlock(const Name: RawUtf8; const Value: variant; var Obj: variant);

*将属性value添加到给定的TDocVariant文档对象以及内部存储的文档,然后释放之前的lock

  • 调用此方法之前应由AddExistingPropOrLock()返回false,即在一个已锁定的实例上执行*

procedure Clear;

删除所有存储的属性


procedure ReplaceAndUnlock(const Name: RawUtf8; const Value: Variant; out LocalValue: Variant);

*通过属性名称设置value,并设置本地副本

  • 可以这样使用,以实现线程安全的缓存:
if not cache.ExistsOrLock('prop',local) then
  cache.ReplaceAndUnlock('prop',newValue,local);
  • 调用此方法之前应由ExistsOrLock()返回false,即在一个已锁定的实例上执行

property Value[const Name: RawUtf8]: Variant read GetValue write SetValue;

*通过此属性安全地访问文档字段

  • 这是此存储的主要入口点
  • 如果在读取时Name不存在,将引发EDocVariant异常
  • 实现类将对variant value进行线程安全的副本*

1.3.6. TLockedDocVariant

TLockedDocVariant = class(TInterfacedObjectWithCustomCreate)

*允许线程安全地访问TDocVariant文档


constructor Create(options: TDocVariantOptions); reintroduce; overload;

使用相应的选项初始化线程安全的文档存储


constructor Create(options: TDocVariantModel); reintroduce; overload;

从给定的模板初始化线程安全的文档存储


constructor Create; overload; override;

*使用快速的TDocVariant初始化线程安全的文档


destructor Destroy; override;

销毁存储


function AddExistingProp(const Name: RawUtf8; var Obj: variant): boolean;

*向给定的TDocVariant文档对象添加一个已存在的属性value

  • 如果Name存在,则返回TRUE并将Name/Value对添加到Obj
  • 如果存储的文档中不存在Name,则返回FALSE
  • 此方法在查找Name期间会使用lock,但无论是否返回FALSE,总是会释放lock(参见AddExistingPropOrLock)*

function AddExistingPropOrLock(const Name: RawUtf8; var Obj: variant): boolean;

*向给定的TDocVariant文档对象添加一个已存在的属性value

  • 如果Name存在,则返回TRUE并将Name/Value对添加到Obj
  • 如果存储的文档中不存在Name,则返回FALSE,并期望最终调用Lock.Leave或AddNewPropAndUnlock()

function Copy: variant;

对内部TDocVariant文档对象或数组进行线程安全的复制


function Exists(const Name: RawUtf8; out Value: Variant): boolean;

按名称检查和返回一个给定的属性


function ExistsOrLock(const Name: RawUtf8; out Value: Variant): boolean;

*按名称检查和返回一个给定的属性

  • 如果找到,则返回TRUE并返回现有Name的value
  • 如果未找到,则返回FALSE,并期望最终调用Lock.Leave或ReplaceAndUnlock()

function Lock: TAutoLocker;

对关联的线程安全互斥锁进行低级访问


function ToJson(HumanReadable: boolean = false): RawUtf8;

*将存储的value保存为UTF-8编码的JSON对象


procedure AddItem(const Value: variant);

value添加到内部TDocVariant文档数组


procedure AddNewProp(const Name: RawUtf8; const Value: variant; var Obj: variant);

*向给定的TDocVariant文档对象添加一个属性value

  • 此方法在调用时不会期望资源被锁定,与AddNewPropAndUnlock不同
  • 将使用内部lock确保线程安全
  • 如果Name已存在,将更新/更改现有的value*

procedure AddNewPropAndUnlock(const Name: RawUtf8; const Value: variant; var Obj: variant);

向给定的TDocVariant文档对象和内部存储的文档添加一个属性value


procedure Clear;

删除所有存储的属性


procedure ReplaceAndUnlock(const Name: RawUtf8; const Value: Variant; out LocalValue: Variant);

通过属性名称设置value,并设置本地副本


property Value[const Name: RawUtf8]: variant read GetValue write SetValue;

*将通过此属性安全地访问文档字段

  • 如果Name不存在,将引发EDocVariant异常
  • 返回的variant结果是副本,不是varByRef,因为副本将更具线程安全性*

1.3.7. TThreadAbstract

TThreadAbstract = class(TThread)

*所有TThread继承类的抽象父类

  • 利用跨编译器和跨版本RTL的差异
  • 拥有预期的Start和TerminateSet方法,以及Terminated属性*

procedure Start;

*调用此方法以start线程

  • Resume在最新的RTL中已被弃用,因为一些操作系统(例如Linux)没有实现此暂停/恢复功能;我们在此为Delphi的旧版本定义此方法*

procedure Terminate; reintroduce;

重新引入以调用TerminatedSet


procedure TerminatedSet; virtual;

*正确terminate线程


property Terminated;

定义为public,因为可能用于terminate处理方法


1.3.8. TSynBackgroundThreadAbstract

TSynBackgroundThreadAbstract = class(TThreadAbstract)

*具有自身执行内容的抽象TThread


constructor Create(const aThreadName: RawUtf8; const OnBeforeExecute: TOnNotifyThread = nil; const OnAfterExecute: TOnNotifyThread = nil; CreateSuspended: boolean = false); reintroduce;

*初始化线程

  • 可以定义一些回调来嵌套线程执行,例如分配给TRestServer.BeginCurrentThread/EndCurrentThread,或者至少将OnAfterExecute设置为TSynLogFamily.OnThreadEnded*

destructor Destroy; override;

*释放使用的资源


function SleepOrTerminated(MS: cardinal): boolean;

*Sleep()的安全版本,不会中断线程进程

  • 如果线程已终止,则返回TRUE
  • 如果成功等待了MS毫秒,则返回FALSE*

procedure TerminatedSet; override;

*正确终止线程

  • 由重新引入的Terminate调用*

procedure WaitForNotExecuting(maxMS: integer = 500);

*等待Execute/ExecuteLoop结束(即fExecute<>exRun)

  • 在循环中调用Sleep(),直到达到超时
  • 例如在Destroy中使用,以避免任何GPF并确保干净地终结*

property Pause: boolean read fExecuteLoopPause write SetExecuteLoopPause;

*临时停止ExecuteLoop的执行,直到设置为false


property ProcessEvent: TSynEvent read fProcessEvent;

*访问与低级别相关的事件,用于通知后台线程任务执行

  • 可以调用ProcessEvent.SetEvent来触发内部处理循环*


1.3.9. TSynBackgroundThreadMethodAbstract

TSynBackgroundThreadMethodAbstract = class(TSynBackgroundThreadAbstract)

*抽象TThread,能够在其自己的执行内容中运行方法

  • 典型用途是用于处理数据或远程访问的后台线程,同时UI将保持响应,通过在循环中运行OnIdle事件:例如,查看mormot.rest.client.pas单元中TRestClientUri.OnIdle如何处理这种情况
  • 不应直接使用此类,而应从中继承并重写Process方法,或使用TSynBackgroundThreadEvent / TSynBackgroundThreadMethod并提供更方便的回调*

constructor Create(const aOnIdle: TOnIdleSynBackgroundThread; const aThreadName: RawUtf8; const OnBeforeExecute: TOnNotifyThread = nil; const OnAfterExecute: TOnNotifyThread = nil); reintroduce;

*初始化线程

  • 如果aOnIdle未设置(即等于nil),它将简单地等待后台进程完成,直到RunAndWait()返回
  • 可以定义一些回调来嵌套线程执行,例如分配给TRestServer.BeginCurrentThread/EndCurrentThread*

destructor Destroy; override;

终结线程


function RunAndWait(OpaqueParam: pointer): boolean;

*在后台线程中异步启动Process抽象方法

  • 等待进程完成,同时调用OnIdle()回调
  • 在后台线程中引发的任何异常都将在调用者线程中转换
  • 如果self未设置,或者从当前正在处理的同一线程中调用(以避免从OnIdle()回调引发的竞态条件),则返回false
  • 当后台进程完成时返回true
  • OpaqueParam将用于指定后台进程的线程安全内容
  • 此方法是线程安全的,即它将等待由另一个线程启动的任何进程:可以从任何线程调用此方法,即使其主要目的是从主UI线程调用*

property OnAfterProcess: TOnNotifyThread read fOnAfterProcess write fOnAfterProcess;

在每个Process之后在Execute中触发的可选回调事件


property OnBeforeProcess: TOnNotifyThread read fOnBeforeProcess write fOnBeforeProcess;

在每个Process之前在Execute中触发的可选回调事件


property OnIdle: TOnIdleSynBackgroundThread read fOnIdle write fOnIdle;

*在远程阻塞进程期间循环执行的回调事件,例如,在请求时间较长时刷新UI

  • 可以为此属性分配一个回调,例如调用Application.ProcessMessages,在后台线程中执行远程请求,但让UI保持响应:mORMotUILogin.pas中的TLoginForm.OnIdleProcess和OnIdleProcessForm方法将满足此属性的预期
  • 如果OnIdle未设置(即等于nil),它将简单地等待后台进程完成,直到RunAndWait()返回*

property OnIdleBackgroundThreadActive: boolean read GetOnIdleBackgroundThreadActive;

*如果后台线程处于活动状态,并且在处理过程中调用了OnIdle事件,则为TRUE

  • 例如,用于确保用户界面消息不会重新进入*

1.3.10. TSynBackgroundThreadEvent

TSynBackgroundThreadEvent = class(TSynBackgroundThreadMethodAbstract)

允许后台线程处理方法回调


constructor Create(const aOnProcess: TOnProcessSynBackgroundThread; const aOnIdle: TOnIdleSynBackgroundThread; const aThreadName: RawUtf8); reintroduce;

*初始化线程

  • 如果aOnIdle未设置(即等于nil),它将简单地等待后台进程完成,直到RunAndWait()返回*

property OnProcess: TOnProcessSynBackgroundThread read fOnProcess write fOnProcess;

*提供一个在后台线程中执行的方法处理程序

  • 由RunAndWait()方法触发 - 该方法将等待直到完成
  • 在RunAndWait()中指定的OpaqueParam将在此处提供*

1.3.11. TSynBackgroundThreadMethod

TSynBackgroundThreadMethod = class(TSynBackgroundThreadMethodAbstract)

允许后台线程处理可变的TThreadMethod回调


procedure RunAndWait(Method: TThreadMethod); reintroduce;

*运行一次提供的TThreadMethod回调

  • 使用此方法,而不是继承的RunAndWait()*

1.3.12. TSynBackgroundThreadProcedure

TSynBackgroundThreadProcedure = class(TSynBackgroundThreadMethodAbstract)

允许后台线程处理过程回调


constructor Create(aOnProcess: TOnProcessSynBackgroundThreadProc; const aOnIdle: TOnIdleSynBackgroundThread; const aThreadName: RawUtf8); reintroduce;

*初始化线程

  • 如果aOnIdle未设置(即等于nil),它将简单地等待后台进程完成,直到RunAndWait()返回*

property OnProcess: TOnProcessSynBackgroundThreadProc read fOnProcess write fOnProcess;

*提供一个在后台线程中执行的过程处理程序

  • 由RunAndWait()方法触发 - 该方法将等待直到完成
  • 在RunAndWait()中指定的OpaqueParam将在此处提供*

1.3.13. TSynBackgroundThreadProcess

TSynBackgroundThreadProcess = class(TSynBackgroundThreadAbstract)

能够在给定周期速率下运行方法的TThread


constructor Create(const aThreadName: RawUtf8; const aOnProcess: TOnSynBackgroundThreadProcess; aOnProcessMS: cardinal; const aOnBeforeExecute: TOnNotifyThread = nil; const aOnAfterExecute: TOnNotifyThread = nil; aStats: TSynMonitorClass = nil; CreateSuspended: boolean = false); reintroduce; virtual;

*初始化线程以进行周期性任务处理

  • 当调用ProcessEvent.SetEvent或自上次处理以来已经过去了aOnProcessMS毫秒周期时,将调用aOnProcess
  • 如果aOnProcessMS为0,将等待直到调用ProcessEvent.SetEvent
  • 可以定义一些回调来嵌套线程执行,例如分配给TRestServer.BeginCurrentThread/EndCurrentThread*

destructor Destroy; override;

终结线程并等待其结束


property OnException: TNotifyEvent read fOnException write fOnException;

*当OnProcess引发异常时执行的事件回调

  • 提供的Sender参数是引发的异常实例*

property OnProcess: TOnSynBackgroundThreadProcess read fOnProcess;

访问周期性任务的实现事件


property OnProcessMS: cardinal read fOnProcessMS write fOnProcessMS;

访问周期性任务处理的延迟(以毫秒为单位)


property Stats: TSynMonitor read fStats;

*处理统计信息

  • 如果在类构造函数中aStats为nil,则可能为nil*

1.3.14. TSynBackgroundTimerTask

TSynBackgroundTimerTask = record

TSynBackgroundTimer内部注册列表使用


1.3.15. TSynBackgroundTimer

TSynBackgroundTimer = class(TSynBackgroundThreadProcess)

*能够在后台线程中以周期速率运行一个或多个任务的TThread


constructor Create(const aThreadName: RawUtf8; const aOnBeforeExecute: TOnNotifyThread = nil; aOnAfterExecute: TOnNotifyThread = nil; aStats: TSynMonitorClass = nil; aLogClass: TSynLogClass = nil); reintroduce; virtual;

*初始化线程以进行周期性taskprocessing

  • 你可以定义一些回调来嵌套线程执行,例如分配给TRestServer.BeginCurrentThread/EndCurrentThread,如TRestBackgroundTimer.Create所做的那样*

destructor Destroy; override;

终结线程


function DeQueue(const aOnProcess: TOnSynBackgroundTimerProcess; const aMsg: RawUtf8): boolean;

*从processing列表中移除一条消息

  • 提供的消息将在与aOnProcess相关联的内部FIFO列表中搜索,如果找到则从列表中移除
  • aOnProcess应通过之前的调用到Enable()方法进行注册
  • 如果成功则返回true,如果提供的消息未注册则返回false*

function Disable(const aOnProcess: TOnSynBackgroundTimerProcess): boolean;

*取消定义一个在周期秒数上运行的task

  • aOnProcess应通过之前的调用到Enable()方法进行注册
  • 如果成功则返回true,如果提供的task未注册则返回false
  • 对于mORMot服务上的后台进程,考虑使用TRestServer的TimerEnable/TimerDisable方法及其TSynBackgroundTimer线程*

function EnQueue(const aOnProcess: TOnSynBackgroundTimerProcess; const aMsgFmt: RawUtf8; const Args: array of const; aExecuteNow: boolean = false): boolean; overload;

*在task的下次执行期间添加一条格式化消息进行处理

  • 提供的消息将被添加到与aOnProcess相关联的内部FIFO列表中,然后每次调用时作为aMsg参数提供
  • 如果aExecuteNow为true,则不会等待下一个aOnProcessSecs的发生
  • aOnProcess应通过之前的调用到Enable()方法进行注册
  • 如果成功则返回true,如果提供的task未注册则返回false*

function EnQueue(const aOnProcess: TOnSynBackgroundTimerProcess; const aMsg: RawUtf8; aExecuteNow: boolean = false): boolean; overload;

*在task的下次执行期间添加一条消息进行处理

  • 提供的消息将被添加到与aOnProcess相关联的内部FIFO列表中,然后每次调用时作为aMsg参数提供
  • 如果aExecuteNow为true,则不会等待下一个aOnProcessSecs的发生
  • aOnProcess应通过之前的调用到Enable()方法进行注册
  • 如果成功则返回true,如果提供的task未注册则返回false*

function ExecuteNow(const aOnProcess: TOnSynBackgroundTimerProcess): boolean;

*立即执行一个task,不等待下一个aOnProcessSecs的发生

  • aOnProcess应通过之前的调用到Enable()方法进行注册
  • 如果成功则返回true,如果提供的task未注册则返回false*

function ExecuteOnce(const aOnProcess: TOnSynBackgroundTimerProcess): boolean;

*立即执行一个task,不等待下一个aOnProcessSecs的发生

  • aOnProcess不应通过之前的调用到Enable()方法进行注册*

procedure Enable(const aOnProcess: TOnSynBackgroundTimerProcess; aOnProcessSecs: cardinal);

*定义一个在周期秒数上运行的task的处理方法

  • 对于mORMot服务上的后台进程,考虑使用TRest的TimerEnable/TimerDisable方法及其关联的BackgroundTimer线程*

procedure WaitUntilNotProcessing(timeoutsecs: integer = 10);

等待直到没有后台task正在处理


property Processing: boolean read fProcessing;

如果当前正在处理某些tasks,则返回TRUE


property Task: TSynBackgroundTimerTaskDynArray read fTask;

对内部task列表的低级访问


property Tasks: TDynArrayLocked read fTasks;

对内部task列表的线程安全包装的低级访问 Low-level access to the internal task list wrapper and safe


1.3.16. TBlockingProcess

TBlockingProcess = class(TSynEvent)

A semaphore used to wait for some process to be finished
- used e.g. by TBlockingCallback in mormot.rest.server.pas
- once created, process would block via a WaitFor call, which would be released when NotifyFinished is called by the process background thread


constructor Create(aTimeOutMs: integer); reintroduce; overload; virtual;

Initialize the semaphore instance
- specify a time out milliseconds period after which blocking execution should be handled as failure (if 0 is set, default 3000 would be used)
- an associated mutex would be created and owned by this instance


constructor Create(aTimeOutMs: integer; aSafe: PSynLocker); reintroduce; overload; virtual;

Override to reset associated params initialize the semaphore instance
- specify a time out milliseconds period after which blocking execution should be handled as failure (if 0 is set, default 3000 would be used)
- an associated mutex shall be supplied


destructor Destroy; override;

Finalize the instance


function NotifyFinished(alreadyLocked: boolean = false): boolean; virtual;

Should be called by the background process when it is finished
- the caller would then let its WaitFor method return
- returns TRUE on success (i.e. status was not evRaised or evTimeout)
- if the instance is already locked (e.g. when retrieved from TBlockingProcessPool.FromCallLocked), you may set alreadyLocked=TRUE


function Reset: boolean; virtual;

Just a wrapper to reset the internal Event state to evNone
- may be used to re-use the same TBlockingProcess instance, after a successfull WaitFor/NotifyFinished process
- returns TRUE on success (i.e. status was not evWaiting), setting the current state to evNone, and the Call property to 0
- if there is a WaitFor currently in progress, returns FALSE


function WaitFor: TBlockingEvent; reintroduce; overload; virtual;

Called to wait for NotifyFinished() to be called, or trigger timeout
- returns the final state of the process, i.e. evRaised or evTimeOut


function WaitFor(TimeOutMS: integer): TBlockingEvent; reintroduce; overload;

Called to wait for NotifyFinished() to be called, or trigger timeout
- returns the final state of the process, i.e. evRaised or evTimeOut


procedure Lock;

Just a wrapper around fSafe^.Lock


procedure Unlock;

Just a wrapper around fSafe^.Unlock


property Event: TBlockingEvent read fEvent;

The current state of process
- use Reset method to re-use this instance after a WaitFor process


property TimeOutMs: integer read fTimeOutMS;

The time out period, in ms, as defined at constructor level


1.3.17. TBlockingProcessPoolItem

TBlockingProcessPoolItem = class(TBlockingProcess)

A semaphore used in the TBlockingProcessPool
- such semaphore have a Call field to identify each execution


property Call: TBlockingProcessPoolCall read fCall;

An unique identifier, when owned by a TBlockingProcessPool
- Reset would restore this field to its 0 default value


1.3.18. TBlockingProcessPool

TBlockingProcessPool = class(TSynPersistent)

Manage a pool of TBlockingProcessPoolItem instances
- each call will be identified via a TBlockingProcessPoolCall unique value
- to be used to emulate e.g. blocking execution from an asynchronous event-driven DDD process
- it would also allow to re-use TEvent system resources


constructor Create(aClass: TBlockingProcessPoolItemClass = nil); reintroduce;

Set TBlockingProcessPoolItem.Call initialize the pool, for a given implementation class


destructor Destroy; override;

Finalize the pool
- would also force all pending WaitFor to trigger a evTimeOut


function FromCall(call: TBlockingProcessPoolCall; locked: boolean = false): TBlockingProcessPoolItem; virtual;

Retrieve a TBlockingProcess from its call identifier
- may be used e.g. from the callback of the asynchronous process to set some additional parameters to the inherited TBlockingProcess, then call NotifyFinished to release the caller WaitFor
- if leavelocked is TRUE, the returned instance would be locked: caller should execute result.Unlock or NotifyFinished(true) after use


function NewProcess(aTimeOutMs: integer): TBlockingProcessPoolItem; virtual;

Book a TBlockingProcess from the internal pool
- returns nil on error (e.g. the instance is destroying)
- or returns the blocking process instance corresponding to this call; its Call property would identify the call for the asynchronous callback, then after WaitFor, the Reset method should be run to release the mutex for the pool


1.3.19. TSynParallelProcessThread

TSynParallelProcessThread = class(TSynBackgroundThreadMethodAbstract)

Thread executing process for TSynParallelProcess


1.3.20. TSynParallelProcess

TSynParallelProcess = class(TSynPersistentLock)

Allow parallel execution of an index-based process in a thread pool
- will create its own thread pool, then execute any method by spliting the work into each thread


constructor Create(ThreadPoolCount: integer; const ThreadName: RawUtf8; const OnBeforeExecute: TOnNotifyThread = nil; const OnAfterExecute: TOnNotifyThread = nil; MaxThreadPoolCount: integer = 32); reintroduce; virtual;

Initialize the thread pool
- you could define some callbacks to nest the thread execution, e.g. assigned to TRestServer.BeginCurrentThread/EndCurrentThread
- up to MaxThreadPoolCount=32 threads could be setup (you may allow a bigger value, but interrest of this thread pool is to have its process saturating each CPU core)
- if ThreadPoolCount is 0, no thread would be created, and process would take place in the current thread


destructor Destroy; override;

Finalize the thread pool


procedure ParallelRunAndWait(const Method: TOnSynParallelProcess; MethodCount: integer; const OnMainThreadIdle: TNotifyEvent = nil);

Run a method in parallel, and wait for the execution to finish
- will split Method[0..MethodCount-1] execution over the threads
- in case of any exception during process, an ESynParallel exception would be raised by this method
- if OnMainThreadIdle is set, the current thread (which is expected to be e.g. the main UI thread) won't process anything, but call this event during waiting for the background threads


property ParallelRunCount: integer read fParallelRunCount;

How many threads have been activated


property ThreadName: RawUtf8 read fThreadName;

Some text identifier, used to distinguish each owned thread


property ThreadPoolCount: integer read fThreadPoolCount;

How many threads are currently in this instance thread pool


1.3.21. TSynThread

TSynThread = class(TThreadAbstract)

A simple TThread with a "Terminate" event run in the thread context
- the TThread.OnTerminate event is run within Synchronize() so did not match our expectations to be able to release the resources in the thread context which created them (e.g. for COM objects, or some DB drivers)
- used internally by THttpServerGeneric.NotifyThreadStart() - you should not have to use the protected fOnThreadTerminate event handler
- also define a Start method for compatibility with older versions of Delphi


constructor Create(CreateSuspended: boolean); reintroduce; virtual;

Initialize the thread instance, in non suspended state


function SleepOrTerminated(MS: cardinal): boolean;

Safe version of Sleep() which won't break the thread process
- returns TRUE if the thread was Terminated
- returns FALSE if successfully waited up to MS milliseconds


property StartNotified: TObject read fStartNotified write fStartNotified;

Ensure fOnThreadTerminate is called only if NotifyThreadStart has been done


1.3.22. TNotifiedThread

TNotifiedThread = class(TSynThread)

Abstract class to implement a thread with start/stop notifications
- e.g. a server thread
- do not use this class, but rather the THttpServer, THttpApiServer or TWebSocketServer (as defined in mormot.net.websock)


constructor Create(CreateSuspended: boolean; const OnStart, OnStop: TOnNotifyThread; const ProcessName: RawUtf8); reintroduce; virtual;

Initialize the server instance, in non suspended state


procedure SetServerThreadsAffinityPerCpu( const log: ISynLog; const threads: TThreadDynArray);

Assign each thread to a single logical CPU core
- for instance, for a HTTP server, it may ensure better scalability with short-living requests and high number of threads


procedure SetServerThreadsAffinityPerSocket( const log: ISynLog; const threads: TThreadDynArray);

Assign each thread to a single hardware CPU socket
- for instance, for a HTTP server, it may ensure better scalability on complex hardware with several physical CPU packages - but it is very picky, so should be enabled only with proper testing on the actual HW


1.3.23. TLoggedThread

TLoggedThread = class(TSynThread)

Abstract class to implement a thread with logging notifications


constructor Create(CreateSuspended: boolean; Logger: TSynLogClass; const ProcName: RawUtf8); reintroduce; virtual;

Initialize the server instance, in non suspended state


procedure TerminateAndWaitFinished(TimeOutMs: integer = 5000); virtual;

Notify the thread to be terminated, and wait for DoExecute to finish


property LogClass: TSynLogClass read fLogClass;

The associated logging class


property ProcessName: RawUtf8 read fProcessName;

The name of this thread, as supplied to SetCurrentThreadName()


1.3.24. TLoggedWorkThread

TLoggedWorkThread = class(TLoggedThread)

A class able to run some process in a background thread
- with proper logging and eventual ending notification


constructor Create(Logger: TSynLogClass; const ProcessName: RawUtf8; const NameValuePairs: array of const; const OnExecute: TOnLoggedWorkProcess; const OnExecuted: TNotifyEvent = nil); reintroduce; overload;

This constructor will directly start the thread in background
- with the context as a TDocVariantData object with name/value pairs


constructor Create(Logger: TSynLogClass; const ProcessName: RawUtf8; Sender: TObject; const OnExecute: TNotifyEvent; const OnExecuted: TNotifyEvent = nil); reintroduce; overload;

This constructor will directly start the thread in background
- with the context as a regular TNotifyEvent


1.3.25. TSynThreadPoolWorkThread

TSynThreadPoolWorkThread = class(TSynThread)

Defines the work threads used by TSynThreadPool


constructor Create(Owner: TSynThreadPool); reintroduce;

Exception-safe call of fOwner.Task() initialize the thread


destructor Destroy; override;

Finalize the thread


procedure Execute; override;

Will loop for any pending task, and execute fOwner.Task()


property Owner: TSynThreadPool read fOwner;

The associated thread pool


1.3.26. TSynThreadPool

TSynThreadPool = class(TObject)

A simple Thread Pool, used e.g. for fast handling HTTP/1.0 requests
- implemented over I/O Completion Ports under Windows, or a classical Event-driven approach under Linux/POSIX


constructor Create(NumberOfThreads: integer = 32; aOverlapHandle: THandle = INVALID_HANDLE_VALUE; const aName: RawUtf8 = '');

Initialize a thread pool with the supplied number of threads
- abstract Task() virtual method will be called by one of the threads
- up to 256 threads can be associated to a Thread Pool
- on Windows, can optionaly accept aOverlapHandle - a handle previously opened using Windows Overlapped I/O (IOCP)
- on POSIX, aQueuePendingContext=true will store the pending context into an internal queue, so that Push() returns true until the queue is full


destructor Destroy; override;

Shut down the Thread pool, releasing all associated threads


function Push(aContext: pointer; aWaitOnContention: boolean = false): boolean;

Let a task (specified as a pointer) be processed by the Thread Pool
- returns false if there is no idle thread available in the pool and Create(aQueuePendingContext=false) was used (caller should retry later); if aQueuePendingContext was true in Create, or IOCP is used, the supplied context will be added to an internal list and handled when possible
- if aWaitOnContention is default false, returns immediately when the queue is full; set aWaitOnContention=true to wait up to ContentionAbortDelay ms and retry to queue the task


property ContentionAbortCount: cardinal read fContentionAbortCount;

How many tasks were rejected due to thread pool contention
- if this number is high, consider setting a higher number of threads, or profile and tune the Task method


property ContentionAbortDelay: integer read fContentionAbortDelay write fContentionAbortDelay;

Milliseconds delay to reject a connection due to contention
- default is 5000, i.e. 5 seconds wait for some room to be available in the IOCP or aQueuePendingContext internal list
- during this delay, no new connection is available (i.e. Accept is not called), so that a load balancer could detect the contention and switch to another instance in the pool, or a direct client may eventually have its connection rejected, so won't start sending data


property ContentionCount: cardinal read fContentionCount;

How many times the pool waited for an available slot in the queue
- contention won't fail immediately, but will retry until ContentionAbortDelay
- any high number here may better increase the threads count
- use this property and ContentionTime to compute the average contention time


property ContentionTime: Int64 read fContentionTime;

Total milliseconds spent waiting for an available slot in the queue
- contention won't fail immediately, but will retry until ContentionAbortDelay
- any high number here requires code refactoring of the Task method


property RunningThreads: integer read fRunningThreads;

How many threads are currently processing tasks in this thread pool
- is in the range 0..WorkThreadCount


property WorkThread: TSynThreadPoolWorkThreads read fWorkThread;

Low-level access to the threads defined in this thread pool


property WorkThreadCount: integer read fWorkThreadCount;

How many threads are available in the pool
- maps Create() parameter, i.e. 32 by default


1.4. Types implemented in the mormot.core.threads unit

1.4.1. TBlockingEvent

TBlockingEvent = ( evNone, evWaiting, evTimeOut, evRaised );

The current state of a TBlockingProcess instance


1.4.2. TBlockingProcessPoolCall

TBlockingProcessPoolCall = type integer;

Used to identify each TBlockingProcessPool call
- allow to match a given TBlockingProcessPoolItem semaphore


1.4.3. TBlockingProcessPoolItemClass

TBlockingProcessPoolItemClass = class of TBlockingProcessPoolItem;

Class-reference type (metaclass) of a TBlockingProcess


1.4.4. TEvent

TEvent = syncobjs.TEvent;

Defined here to avoid explicit link to syncobjs in uses clause
- note that you may better use TSynEvent from mormot.core.os.pas


1.4.5. TOnIdleSynBackgroundThread

TOnIdleSynBackgroundThread = procedure(Sender: TSynBackgroundThreadAbstract; ElapsedMS: integer) of object;

Idle method called by TSynBackgroundThreadAbstract in the caller thread during remote blocking process in a background thread
- typical use is to run Application.ProcessMessages, e.g. for TRestClientUri.Uri() to provide a responsive UI even in case of slow blocking remote access
- provide the time elapsed (in milliseconds) from the request start (can be used e.g. to popup a temporary message to wait)
- is call once with ElapsedMS=0 at request start
- is call once with ElapsedMS=-1 at request ending
- see TLoginForm.OnIdleProcess and OnIdleProcessForm in mORMotUILogin.pas


1.4.6. TOnLoggedWorkProcess

TOnLoggedWorkProcess = procedure(const Context: TDocVariantData) of object;

Event called in a background thread by TLoggedWorkThread.Create


1.4.7. TOnNotifyThread

TOnNotifyThread = procedure(Sender: TThread) of object;

Event prototype used e.g. by TSynBackgroundThreadAbstract and TSynThread callbacks


1.4.8. TOnProcessSynBackgroundThread

TOnProcessSynBackgroundThread = procedure(Sender: TSynBackgroundThreadEvent; ProcessOpaqueParam: pointer) of object;

Background process method called by TSynBackgroundThreadEvent
- will supply the OpaqueParam parameter as provided to RunAndWait() method when the Process virtual method will be executed


1.4.9. TOnProcessSynBackgroundThreadProc

TOnProcessSynBackgroundThreadProc = procedure(ProcessOpaqueParam: pointer);

Background process procedure called by TSynBackgroundThreadProcedure
- will supply the OpaqueParam parameter as provided to RunAndWait() method when the Process virtual method will be executed


1.4.10. TOnSynBackgroundThreadProcess

TOnSynBackgroundThreadProcess = procedure(Sender: TSynBackgroundThreadProcess) of object;

Event callback executed periodically by TSynBackgroundThreadProcess


1.4.11. TOnSynBackgroundTimerProcess

TOnSynBackgroundTimerProcess = procedure(Sender: TSynBackgroundTimer; const Msg: RawUtf8) of object;

Event callback executed periodically by TSynBackgroundThreadProcess
- Msg is '' if there is no pending message in this task FIFO
- Msg is set for each pending message in this task FIFO
- on mORMot 1, there was a TWaitEvent parameter which is now removed


1.4.12. TOnSynParallelProcess

TOnSynParallelProcess = procedure(IndexStart, IndexStop: integer) of object;

Callback implementing some parallelized process for TSynParallelProcess
- if 0<=IndexStart<=IndexStop, it should execute some process


1.4.13. TPendingTaskListItemDynArray

TPendingTaskListItemDynArray = array of TPendingTaskListItem;

Internal list definition, used by TPendingTaskList storage


1.4.14. TSynBackgroundThreadProcessStep

TSynBackgroundThreadProcessStep = ( flagIdle, flagStarted, flagFinished, flagDestroying );

State machine status of the TSynBackgroundThreadAbstract process


1.4.15. TSynBackgroundThreadProcessSteps

TSynBackgroundThreadProcessSteps = set of TSynBackgroundThreadProcessStep;

State machine statuses of the TSynBackgroundThreadAbstract process


1.4.16. TSynBackgroundTimerTaskDynArray

TSynBackgroundTimerTaskDynArray = array of TSynBackgroundTimerTask;

Protect Msg[] list stores TSynBackgroundTimer internal registration list


1.4.17. TThreadDynArray

TThreadDynArray = array of TThread;

A dynamic array of TThread


1.4.18. TWaitResult

TWaitResult = syncobjs.TWaitResult;

Defined here to avoid explicit link to syncobjs in uses clause
- note that you may better use TSynEvent from mormot.core.os.pas


1.5. Constants implemented in the mormot.core.threads unit

1.5.1. THREADPOOL_MAXTHREADS

THREADPOOL_MAXTHREADS = 256;

Allow up to 256 * 2MB = 512MB of RAM for the TSynThreadPoolWorkThread stack


1.5.2. wrSignaled

wrSignaled = syncobjs.wrSignaled;

Please favor TSynEvent from mormot.core.os instead of TEvent defined here to avoid explicit link to syncobjs in uses clause

热门相关:我的绝色美女房客   傲娇小萌妃:殿下太腹黑   我的全世界就是你   穹顶之上   兵王传说