当前位置: 首页 > 编程日记 > 正文

200行代码解读TDEngine背后的定时器

640?wx_fmt=jpeg


作者 | beyondma来源 | CSDN博客

导读:最近几周,本文作者几篇有关陶建辉老师最新的创业项目-TdEngine代码解读文章出人意料地引起了巨大的反响,原以为C语言已经是昨日黄花,不过从读者的留言来看,C语言还是老当益壮,依旧有着巨大的影响力,作者就以此为契机不断向陶老师请教,这次再给大家带来TdEngine计时器的代码解读。


其主要源码地址如下:https://github.com/taosdata/TDengine/blob/master/src/util/src/ttimer.c


TDEngine为何要自己实现Timer


其实一开始在读到这段代码时笔者也有类似的疑问,因为操作系统的内核基本都实现了定时器的功能,可以直接调用,但是深入思考一下就会发现由于TdEginge本身是个时序数据库的应用,而由于数据库的特殊性,其对库底层的需求其实与操作系统的内核需求类似,我们知道直接调用操作系统的timer需要在到时后启动一个对应的线程去处理对应的中断请求,而这对于TdEginge这种动辙需要上万个定时器的数据库应用来说无疑是一笔巨大的开销,这显然不是陶老师这种极端要求效率的程序员能够接受的。


所以TDEngine的定时器的基本思路是基于操作系统的timer,来封装自身的定时器功能,使所有的timer控制器运行在一个线程池,而在同一timer控制器下的timer则运行在同一线程内以此来达到节约资源的目的。


TDEngine Timer的基本工作原理


简单来说TDEngine Timer的工作流程如下:


一.Timer初始化


主要完成以下工作:


  1. 初始化Timer线程池

  2. 启动操作系统的Timer,并注册处理timer循环处理函数taosTimerLoopFunc。


二.Timer启动


  1. 计算当前需要启动timer的调起序列号(index),即taosTimerLoopFunc运行在第几个Loop时会调起当前的timer的处理函数。

  2. 对于调起序列号相同的timer加入双链表tmrList[当前index],其在链表中的位置依据其到期时间的先后排序,注:(由于之前启动的操作系统timer也是有循环周期的,所以TDEngine timer也可能不是要在当前周期内调起。所以调起序列号相同,但是调起周期运可能有前后次序,其在链表中的位置其实是要买调起周期的先后排序的。)


工作过程图示:由于作者美工基础较差,所以虽然花了很久但是下图的效果其实一般,请大家能够直观感受即可。


640?wx_fmt=gif

简要地讲,taosTimerLoopFunc函数循环处理tmrList,并将当前启动序列号对应的tmrList交由taosTmrProcessList处理,taosTmrProcessList调用处在当前启动序列号(index)且处在当前循环序列号(cycle)的timer的回调(handler)函数。


结合代码的解读


  1. 初始化函数的解读,具体代码及注释如下:


void *taosTmrInit(int maxNumOfTmrs, int resolution, int longest, char *label) {	static pthread_once_t tmrInit = PTHREAD_ONCE_INIT;	tmr_ctrl_t *          pCtrl;	pthread_once(&tmrInit, taosTmrModuleInit);//pthread_once保证TmrModule只会被运行一次	int tmrCtrlId = taosAllocateId(tmrIdPool);	if (tmrCtrlId < 0) {	tmrError("%s bug!!! too many timers!!!", label);	return NULL;	}	pCtrl = tmrCtrl + tmrCtrlId;	tfree(pCtrl->tmrList);	tmrMemPoolCleanUp(pCtrl->poolHandle);	memset(pCtrl, 0, sizeof(tmr_ctrl_t));	pCtrl->tmrCtrlId = tmrCtrlId;	strcpy(pCtrl->label, label);	pCtrl->maxNumOfTmrs = maxNumOfTmrs;	if ((pCtrl->poolHandle = tmrMemPoolInit(maxNumOfTmrs + 10, sizeof(tmr_obj_t))) == NULL) {	tmrError("%s failed to allocate mem pool", label);	tmrMemPoolCleanUp(pCtrl->poolHandle);	return NULL;	}	if (resolution < MSECONDS_PER_TICK) resolution = MSECONDS_PER_TICK;//初始化分辨率	pCtrl->resolution = resolution;	pCtrl->maxTicks = resolution / MSECONDS_PER_TICK;//初始化最大的tick	pCtrl->ticks = rand() / pCtrl->maxTicks;	pCtrl->numOfPeriods = longest / resolution;//初始化最大周期	if (pCtrl->numOfPeriods < 10) pCtrl->numOfPeriods = 10;	pCtrl->tmrList = (tmr_list_t *)malloc(sizeof(tmr_list_t) * pCtrl->numOfPeriods);//初始化tmrList目前还都是NULL	for (int i = 0; i < pCtrl->numOfPeriods; i++) {	pCtrl->tmrList[i].head = NULL;	pCtrl->tmrList[i].count = 0;	}	if (pthread_mutex_init(&pCtrl->mutex, NULL) < 0) {	tmrError("%s failed to create the mutex, reason:%s", label, strerror(errno));	taosTmrCleanUp(pCtrl);	return NULL;	}	pCtrl->signature = pCtrl;	numOfTmrCtrl++;	tmrTrace("%s timer ctrl is initialized, index:%d", label, tmrCtrlId);	return pCtrl;//初始化完成	
}


2. 模块初化函数:我们看到在初始化函数中调用了模块初始化函数进行线程池及操作系统定时器的启动处理,其具体代码及注释如下:


void taosTmrModuleInit(void) {	tmrIdPool = taosInitIdPool(maxNumOfTmrCtrl);//初始化id池	memset(tmrCtrl, 0, sizeof(tmrCtrl));	#ifdef LINUX	pthread_t      thread;	pthread_attr_t tattr;	pthread_attr_init(&tattr);	pthread_attr_setdetachstate(&tattr, PTHREAD_CREATE_DETACHED);	if (pthread_create(&thread, &tattr, taosProcessAlarmSignal, NULL) != 0) {	tmrError("failed to create timer thread");//初始化操作系统timer	return;	}	pthread_attr_destroy(&tattr);	
#else	taosInitTimer(taosTimerLoopFunc, MSECONDS_PER_TICK);	
#endif	tmrQhandle = taosInitScheduler(10000, taosTmrThreads, "tmr");//初始化handle的Scheduler	tmrTrace("timer module is initialized, thread:%d", taosTmrThreads);	
}


3.timer启动函数


tmr_h taosTmrStart(void (*fp)(void *, void *), int mseconds, void *param1, void *handle) {	tmr_obj_t * pObj, *cNode, *pNode;	tmr_list_t *pList;	int         index, period;	tmr_ctrl_t *pCtrl = (tmr_ctrl_t *)handle;	if (handle == NULL) return NULL;	period = mseconds / pCtrl->resolution;//初始化period数	if (pthread_mutex_lock(&pCtrl->mutex) != 0)	tmrError("%s mutex lock failed, reason:%s", pCtrl->label, strerror(errno));	pObj = (tmr_obj_t *)tmrMemPoolMalloc(pCtrl->poolHandle);	if (pObj == NULL) {	tmrError("%s reach max number of timers:%d", pCtrl->label, pCtrl->maxNumOfTmrs);	pthread_mutex_unlock(&pCtrl->mutex);	return NULL;	}	pObj->cycle = period / pCtrl->numOfPeriods;//初始化周期数,即使用period除以每个循环中共有几个period,得到需要在第几个循环中调起timer	pObj->param1 = param1;	pObj->fp = fp;	pObj->timerId = pObj;	pObj->pCtrl = pCtrl;	index = (period + pCtrl->periodsFromStart) % pCtrl->numOfPeriods;//初始化启动序列号	int cindex = (pCtrl->periodsFromStart) % pCtrl->numOfPeriods;	pList = &(pCtrl->tmrList[index]);	pObj->index = index;	cNode = pList->head;	pNode = NULL;	while (cNode != NULL) {	if (cNode->cycle < pObj->cycle) {	pNode = cNode;	cNode = cNode->next;	} else {	break;	}	}	pObj->next = cNode;	pObj->prev = pNode;	if (cNode != NULL) {	cNode->prev = pObj;	}	if (pNode != NULL) {	pNode->next = pObj;	} else {	pList->head = pObj;	}	pList->count++;	pCtrl->numOfTmrs++;	//以上为将相同index的timer按照先后顺序在pList中排序,具体下面会有图例展示。	if (pthread_mutex_unlock(&pCtrl->mutex) != 0)	tmrError("%s mutex unlock failed, reason:%s", pCtrl->label, strerror(errno));	tmrTrace("%s %p, timer started, fp:%p, tmr_h:%p, index:%d, total:%d cindex:%d", pCtrl->label, param1, fp, pObj, index,	pCtrl->numOfTmrs, cindex);	return (tmr_h)pObj;	
}


可能各位读者也被以上代码中的pobj,cnode,pnode搞的晕头转向,下面我们以3个timer为例,假如他们的index和cycle都相同,那么他们分别调用完taosTmrStart之后tmrList会是什么情况,可以参考下表。


640?wx_fmt=png640?wx_fmt=png


 4.loopFunc和taosTmrProcessList


具体代码及注释如下:


void taosTmrProcessList(tmr_ctrl_t *pCtrl) {	unsigned int index;	tmr_list_t * pList;	tmr_obj_t *  pObj, *header;	pthread_mutex_lock(&pCtrl->mutex);	index = pCtrl->periodsFromStart % pCtrl->numOfPeriods;//计算当前index	pList = &pCtrl->tmrList[index];	while (1) {	header = pList->head;	if (header == NULL) break;	if (header->cycle > 0) {/*如当前index对应的tmrList[index]的cycle大于0则下面会将其cycle减1*/	pObj = header;	while (pObj) {	pObj->cycle--;	pObj = pObj->next;	}	break;	}	pCtrl->numOfTmrs--;	tmrTrace("%s %p, timer expired, fp:%p, tmr_h:%p, index:%d, total:%d", pCtrl->label, header->param1, header->fp,	header, index, pCtrl->numOfTmrs);	pList->head = header->next;/*如运行到此处则当前header已经expired,重新整理pList*/	if (header->next) header->next->prev = NULL;	pList->count--;	header->timerId = NULL;	SSchedMsg schedMsg;	schedMsg.fp = NULL;	schedMsg.tfp = header->fp;	schedMsg.ahandle = header->param1;	schedMsg.thandle = header;	taosScheduleTask(tmrQhandle, &schedMsg);	tmrMemPoolFree(pCtrl->poolHandle, (char *)header);	}	pCtrl->periodsFromStart++;	pthread_mutex_unlock(&pCtrl->mutex);	
}


void *taosTimerLoopFunc(int signo) {	tmr_ctrl_t *pCtrl;	int         count = 0;	for (int i = 1; i < maxNumOfTmrCtrl; ++i) {	pCtrl = tmrCtrl + i;	if (pCtrl->signature) {	count++;	pCtrl->ticks++;	if (pCtrl->ticks >= pCtrl->maxTicks) {/*如当前的ticks已经大于maxTicks则需要调起taosTmrProcessList对当前index的timer进行处理*/	taosTmrProcessList(pCtrl);	pCtrl->ticks = 0;	}	if (count >= numOfTmrCtrl) break;	}	}	return NULL;	
}


最后我也想留一个开放性问题,也就是tmrList使用双链表实现的最大好处是什么,能否使用单链表实现?欢迎读者留言说出你的看法。


原文链接:

https://blog.csdn.net/BEYONDMA/article/details/98473143


(*本文为 AI科技大本营转载文章,转载请联系原作者)


社群福利

扫码添加小助手,回复:大会,加入2019 AI开发者大会福利群,每周一、三、五更新技术福利,还有不定期的抽奖活动~

640?wx_fmt=jpeg


精彩推荐



640?wx_fmt=jpeg


60+技术大咖与你相约 2019 AI ProCon!大会早鸟票已售罄,优惠票速抢进行中......2019 AI开发者大会将于9月6日-7日在北京举行,这一届AI开发者大会有哪些亮点?一线公司的大牛们都在关注什么?AI行业的风向是什么?2019 AI开发者大会,倾听大牛分享,聚焦技术实践,和万千开发者共成长。


推荐阅读

  • 七夕大礼包:26个AI学习资源送给你!

  • 玩王者荣耀用不好英雄?两阶段算法帮你精准推荐精彩视频

  • 用Python给女友准备个绝对甜蜜的七夕礼物

  • 突发!Python再次第一,Java和C下降,凭什么?

  • 白话中台战略:中台是个什么鬼?

  • 伟创力回应扣押华为物资;谷歌更新图片界面;Python 3.8.0b3 发布 | 极客头条

  • 沃尔玛也要发币了,Libra忙活半天为他人做了嫁衣?

  • 知名饮料制造商股价暴涨500%惊动FBI,只因在名字中加入了"区块链" ?


640?wx_fmt=png你点的每个“在看”,我都认真当成了喜欢


相关文章:

fastJson结合Nutz.Mapl的进阶应用

为什么80%的码农都做不了架构师&#xff1f;>>> 今天要做一堆数据的序列化, 反序列化, 序列化没问题, 反序列化却遇到了点小意外, 这一堆数据不是一个类!!!!!!当然可以通过类内部的一个类型对象来判断, 但是fastJson并没有这个功能, 只能自己一个一个的遍历一个一个…

OpenCV实现遍历文件夹下所有文件

OpenCV中有实现遍历文件夹下所有文件的类Directory&#xff0c;它里面包括3个成员函数&#xff1a;(1)、GetListFiles&#xff1a;遍历指定文件夹下的所有文件&#xff0c;不包括指定文件夹内的文件夹&#xff1b;(2)、GetListFolders&#xff1a;遍历指定文件夹下的所有文件夹…

阿里、京东、快手、华为......他们是如何构建一个个推荐系统“帝国”的?

推荐系统在人们的日常生活中随处可见&#xff0c;成为我们生命中不可或缺的一部分。作为当今应用最为广泛和成熟的 AI 技术之一&#xff0c;它是信息生产者、传播者与用户之间的桥梁&#xff0c;可以让信息最精准、最高效地到达需求不一的用户面前。每天打开手机或电脑端的大部…

前端基础_ES6

声明 三大关键字声明变量&#xff1a;var &#xff08;ES5语法&#xff09; let &#xff08;ES6语法&#xff09;声明常量&#xff1a;const (ES6语法) var 声明变量特性1、支持 函数作用域2、支持 JS预解析 &#xff08;所谓变量提升&#xff09;3、支持 重复声明 &#xff…

5大典型模型测试单机训练速度超对标框架,飞桨如何做到?

导读&#xff1a;飞桨&#xff08;PaddlePaddle&#xff09;致力于让深度学习技术的创新与应用更简单。在单机训练速度方面&#xff0c;通过高并行、低开销的异步执行策略和高效率的核心算子&#xff0c;优化静态图训练性能&#xff0c;在Paddle Fluid v1.5.0的基准测试中&…

windowsXP用户被禁用导致不能网站登录

1、查看系统事件&#xff0c;发现弹出如下的错误 2、根据上面的错误&#xff0c;我们很容易就可以判断是禁用了账户引起的 2.1后面进入计算机管理&#xff0c;再进入用户管理 2.2双击点开Internet来宾用于&#xff0c;发现此用户已经停用了。 2.3双击点开与IIS访问有关用户&…

从头到尾使用Geth的说明-3-geth参数说明和环境配置

1.参数说明 ETHEREUM选项:--config value TOML 配置文件--datadir "/home/user4/.ethereum" 数据库和keystore密钥的数据目录--keystore keystore存放目录(默认在datadir内)--nousb …

OpenSSL中对称加密算法DES常用函数使用举例

主要包括3个文件&#xff1a; 1. cryptotest.h:#ifndef _CRYPTOTEST_H_ #define _CRYPTOTEST_H_#include <string>using namespace std;typedef enum {GENERAL 0,ECB,CBC,CFB,OFB,TRIPLE_ECB,TRIPLE_CBC }CRYPTO_MODE;string DES_Encrypt(const string cleartext, const…

从原理到落地,七大维度读懂协同过滤推荐算法

作者丨gongyouliu来源 | 大数据与人工智能导语&#xff1a;本文会从协同过滤思想简介、协同过滤算法原理介绍、离线协同过滤算法的工程实现、近实时协同过滤算法的工程实现、协同过滤算法应用场景、协同过滤算法的优缺点、协同过滤算法落地需要关注的几个问题等7个方面来讲述。…

sql查询语句优化需要注意的几点

为了获得稳定的执行性能&#xff0c;SQL语句越简单越好。对复杂的SQL语句&#xff0c;要设法对之进行简化。 常见的简化规则如下&#xff1a; 1&#xff09;不要有超过5个以上的表连接&#xff08;JOIN&#xff09; 2&#xff09;考虑使用临时表或表变量存放中间结果。 3&#…

决策树算法原理(ID3,C4.5)

决策树算法原理(CART分类树) CART回归树 决策树的剪枝 决策树可以作为分类算法&#xff0c;也可以作为回归算法&#xff0c;同时特别适合集成学习比如随机森林。 1. 决策树ID3算法的信息论基础 1970年昆兰找到了用信息论中的熵来度量决策树的决策选择过程&#xff0c;昆兰把这…

对称加密算法之RC4介绍及OpenSSL中RC4常用函数使用举例

RC4是一种对称密码算法&#xff0c;它属于对称密码算法中的序列密码(streamcipher,也称为流密码)&#xff0c;它是可变密钥长度&#xff0c;面向字节操作的流密码。 RC4是流密码streamcipher中的一种&#xff0c;为序列密码。RC4加密算法是Ron Rivest在1987年设计出的密钥长度…

SpringMVC中实现的token,防表单重复提交

一&#xff1a;首先创建一个token处理类 &#xff0c;这里的类名叫 TokenHandlerprivate static Logger logger Logger.getLogger(TokenHandler.class);static Map<String, String> springmvc_token new HashMap<String, String>();//生成一个唯一值的tokenSupp…

利用CxImage实现编解码Gif图像代码举例

Gif(Graphics Interchange Format&#xff0c;图形交换格式)是由CompuServe公司在1987年开发的图像文件格式&#xff0c;分为87a和89a两种版本。Gif是基于LZW算法的无损压缩算法。Gif图像是基于颜色表的&#xff0c;最多只支持8位(256色)。Gif减少了图像调色板中的色彩数量&…

SpringBoot b2b2c 多用户商城系统 ssm b2b2c

来源&#xff1a; SpringBoot b2b2c 多用户商城系统 ssm b2b2c用java实施的电子商务平台太少了&#xff0c;使用spring cloud技术构建的b2b2c电子商务平台更少&#xff0c;大型企业分布式互联网电子商务平台&#xff0c;推出PC微信APP云服务的云商平台系统&#xff0c;其中包括…

AI“生死”落地:谁有资格入选AI Top 30+案例?

2019 年&#xff0c;人工智能应用落地的重要性正在逐步得到验证&#xff0c;这是关乎企业生死攸关的一环。科技巨头、AI 独角兽还有起于草莽的创业公司在各领域进行着一场多方角斗。进行平台布局的科技巨头们&#xff0c;正在加快承载企业部署 AI 应用的步伐&#xff0c;曾经无…

liunx 下su 和sudo 的区别

一. 使用 su 命令临时切换用户身份1、su 的适用条件和威力su命令就是切换用户的工具&#xff0c;怎么理解呢&#xff1f;比如我们以普通用户beinan登录的&#xff0c;但要添加用户任务&#xff0c;执行useradd &#xff0c;beinan用户没有这个权限&#xff0c;而这个权限恰恰由…

非对称加密算法之RSA介绍及OpenSSL中RSA常用函数使用举例

RSA算法&#xff0c;在1977年由Ron Rivest、Adi Shamirh和LenAdleman&#xff0c;在美国的麻省理工学院开发完成。这个算法的名字&#xff0c;来源于三位开发者的名字。RSA已经成为公钥数据加密标准。 RSA属于公开密钥密码体制。公开密钥体制就是产生两把密钥&#xff0c;一把…

依图科技CEO朱珑:“智能密度”对AI发展意味着什么?

8月9日&#xff0c;由中央网信办、工业和信息化部、公安部联合指导&#xff0c;厦门市政府主办的“中国人工智能峰会”于厦门召开。中国工程院院士、北京大学教授高文&#xff0c;依图科技创始人兼CEO朱珑博士等出席峰会并发表了主题演讲。当前&#xff0c;人工智能正在扮演越来…

Office 2016使用NTKO OFFICE控件提示“文件存取错误”的解决办法

2019独角兽企业重金招聘Python工程师标准>>> 之前使用NTKO&#xff0c;电脑安装的说OFFICE2007,但是前2天电脑固态硬盘坏了 &#xff0c;重新安装了系统&#xff0c;安装的说win10和office2016&#xff0c;再访问网站使用ntko时&#xff0c;却提示“文件存取错误”&…

如何制作一个类似Tiny Wings的游戏 Cocos2d-x 2.1.4

在第一篇《如何使用CCRenderTexture创建动态纹理》基础上&#xff0c;增加创建动态山丘&#xff0c;原文《How To Create A Game Like Tiny Wings with Cocos2D 2.X Part 1》&#xff0c;在这里继续以Cocos2d-x进行实现。有关源码、资源等在文章下面给出了地址。 步骤如下&…

腾讯优图开源业界首个3D医疗影像大数据预训练模型

整理 | Jane出品 | AI科技大本营&#xff08;ID&#xff1a;rgznai100&#xff09;近日&#xff0c;腾讯优图首个医疗AI深度学习预训练模型 MedicalNet 正式对外开源。这也是全球第一个提供多种 3D 医疗影像专用预训练模型的项目&#xff0c;将为全球医疗AI发展提供基础。许多研…

接口冲突的一种解决方法

问题描述&#xff1a;在一个大的项目中往往会包括很多模块&#xff0c;会有不同的部门或公司来负责实现某个模块&#xff0c;也有可能有第三方或客户的参与。假如他们都用到了某个开源软件&#xff0c;底层模块根据自身的需求对这个开源软件进行了修改或裁减。上层也用到了此开…

程序员:请你不要对业务「置之不理」

成长是条孤独的路&#xff0c;一个人会走得更快&#xff1b;有志同道合者同行&#xff0c;会走得更远。本篇内容整理自 21 天鲲鹏新青年计划线上分享内容。鲲鹏新青年计划是由 TGO 鲲鹏会组织的线上分享活动&#xff0c;希望能帮助更多同学一起学习、成长。12 月 28 日&#xf…

史上最简单的人脸识别项目登上GitHub趋势榜

来源 | GitHub Trending整理 | Freesia译者 | TommyZihao出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09;导读&#xff1a;近日&#xff0c;一个名为 face_recognition 的人脸识别项目登上了 GitHub Trending 趋势榜&#xff0c;赚足了眼球。自开源至截稿&#xff0…

Centos 64位 Install certificate on apache 即走https协议

2019独角兽企业重金招聘Python工程师标准>>> 一: 先要apache 请求ssl证书的csr 一下是步骤&#xff1a; 重要注意事项 An Important Note Before You Start 在生成CSR文件时同时生成您的私钥&#xff0c;如果您丢了私钥或忘了私钥密码&#xff0c;则颁发 证书给您…

C/C++中“#”和“##”的作用和用法

在C/C的宏中&#xff0c;”#”的功能是将其后面的宏参数进行字符串化操作(Stringfication)&#xff0c;简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。而”##”被称为连接符(concatenator)&#xff0c;用来将两个子串Token连接为一个Token。注意这里连接…

国贫县山西永和:“一揽子”保险“保”脱贫

永和是吕梁山特困连片地区的深度贫困县&#xff0c;生产生活条件极差。 范丽芳 摄 永和是吕梁山特困连片地区的深度贫困县&#xff0c;生产生活条件极差。 范丽芳 摄 中新网太原1月16日电 题&#xff1a;国贫县山西永和&#xff1a;“一揽子”保险“保”脱贫 作者范丽芳 李海金…

内存泄漏检测工具VLD在VS2010中的使用举例

Visual LeakDetector(VLD)是一款用于Visual C的免费的内存泄露检测工具。它的特点有&#xff1a;(1)、它是免费开源的&#xff0c;采用LGPL协议&#xff1b;(2)、它可以得到内存泄露点的调用堆栈&#xff0c;可以获取到所在文件及行号&#xff1b;(3)、它可以得到泄露内存的完整…

天下武功,唯快不破,论推荐系统的“实时性”

作者 | 王喆转载自知乎王喆的机器学习笔记导读&#xff1a;周星驰著名的电影《功夫》里面有一句著名的台词——“天下武功&#xff0c;无坚不摧&#xff0c;唯快不破”。如果说推荐系统的架构是那把“无坚不摧”的“玄铁重剑”&#xff0c;那么推荐系统的实时性就是“唯快不破”…