Swoole 源码分析之 Timer 定时器模块
原文首发链接:Swoole 源码分析之 Timer 定时器模块
大家好,我是码农先森。
引言
Swoole 中的毫秒精度的定时器。底层基于 epoll_wait 和 setitimer 实现,数据结构使用最小堆,可支持添加大量定时器。
在同步 IO 进程中使用 setitimer 和信号实现,如 Manager 和 TaskWorker 进程,在异步 IO 进程中使用 epoll_wait/kevent/poll/select 超时时间实现。
定时器的添加和删除,全部为内存操作。在官方的基准测试脚本中,添加或删除 10 万个随机时间的定时器耗时为 0.08s 左右,因此性能是非常高效的。
源码拆解
我们在分析源代码之前,先看这段使用定时器的代码。Timer::after 函数是设置一个一次性的定时器,也就是执行一次就结束了,常用于执行一次性任务的场景。Timer::tick 函数会每间隔一段时间执行一次,类似一个闹钟的机制,常用于需要定时执行任务的场景。
<?php
// 设置一个一次性定时器
Swoole\Timer::after(1000, function(){
echo " timer after timeout\n";
});
// 设置一个间隔时钟定时器
Swoole\Timer::tick(1000, function(){
echo "timer tick timeout\n";
});
按照之前分析源代码的策略,先对整个源码的调用流程进行梳理,以便于让我们有个整体的印象,调用流程如下图所示。

swoole_timer.cc 这个源码文件中定义了两个函数 swoole_timer_after、swoole_timer_tick。从这段代码中可以看出唯一的区别是,在调用 timer_add 函数时的传参有所不同,一个是 false,一个是 true,表示的是是否需要持久化的执行任务。另外 timer_add 函数实现了一些根据细化的逻辑,例如:参数的解析、一些检查判断的工作。最后,根据 persistent 参数判断是否执行持久化的操作。
// 定义 PHP 函数 swoole_timer_after
// swoole-src/ext-src/swoole_timer.cc:221
static PHP_FUNCTION(swoole_timer_after) {
timer_add(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
}
// 定义 PHP 函数 swoole_timer_tick
// swoole-src/ext-src/swoole_timer.cc:225
static PHP_FUNCTION(swoole_timer_tick) {
timer_add(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
}
// 添加定时任务到定时器中, 并根据持久性标志判断是否需要一直执行
// swoole-src/ext-src/swoole_timer.cc:155
static void timer_add(INTERNAL_FUNCTION_PARAMETERS, bool persistent) {
zend_long ms;
Function *fci = (Function *) ecalloc(1, sizeof(Function));
TimerNode *tnode;
// 解析参数
ZEND_PARSE_PARAMETERS_START(2, -1)
Z_PARAM_LONG(ms)
Z_PARAM_FUNC(fci->fci, fci->fci_cache)
Z_PARAM_VARIADIC('*', fci->fci.params, fci->fci.param_count)
ZEND_PARSE_PARAMETERS_END_EX(goto _failed);
// 检查定时器值 ms 是否小于预定义的最小值 SW_TIMER_MIN_MS
if (UNEXPECTED(ms < SW_TIMER_MIN_MS)) {
php_swoole_fatal_error(E_WARNING, "Timer must be greater than or equal to " ZEND_TOSTR(SW_TIMER_MIN_MS));
_failed:
efree(fci);
RETURN_FALSE;
}
// 进行额外的检查
// no server || user worker || task process with async mode
if (!sw_server() || sw_server()->is_user_worker() ||
(sw_server()->is_task_worker() && sw_server()->task_enable_coroutine)) {
php_swoole_check_reactor();
}
// 使用指定的毫秒数、持久性标志、回调函数 timer_callback 和函数指针 fci 添加一个定时器
tnode = swoole_timer_add((long) ms, persistent, timer_callback, fci);
if (UNEXPECTED(!tnode)) {
php_swoole_fatal_error(E_WARNING, "add timer failed");
goto _failed;
}
// 为定时器节点 tnode 设置类型和析构函数
tnode->type = TimerNode::TYPE_PHP;
tnode->destructor = timer_dtor;
// 根据持久性标志,会一直执行定时的任务
if (persistent) {
if (fci->fci.param_count > 0) {
uint32_t i;
zval *params = (zval *) ecalloc(fci->fci.param_count + 1, sizeof(zval));
for (i = 0; i < fci->fci.param_count; i++) {
ZVAL_COPY(¶ms[i + 1], &fci->fci.params[i]);
}
fci->fci.params = params;
} else {
fci->fci.params = (zval *) emalloc(sizeof(zval));
}
fci->fci.param_count += 1;
ZVAL_LONG(fci->fci.params, tnode->id);
} else {
// 只会执行一次
sw_zend_fci_params_persist(&fci->fci);
}
sw_zend_fci_cache_persist(&fci->fci_cache);
RETURN_LONG(tnode->id);
}
在 timer.cc 源码文件中 swoole_timer_add 这个函数会检查是否已经有可用的定时器管理对象,如果没有的话会进行实例化创建一个,然后通过 SwooleTG.timer->add() 方法添加一个定时器任务。
// 这段代码用于添加一个定时器到 Swoole 框架中的定时器管理器中
// swoole-src/src/wrapper/timer.cc:40
TimerNode *swoole_timer_add(long ms, bool persistent, const TimerCallback &callback, void *private_data) {
// 这里检查定时器是否可用
if (sw_unlikely(!swoole_timer_is_available())) {
// 如果定时器不可用,则会创建一个新的对象
SwooleTG.timer = new Timer();
// 并对其进行初始化
if (sw_unlikely(!SwooleTG.timer->init())) {
// 若初始化失败,就会释放内存
delete SwooleTG.timer;
SwooleTG.timer = nullptr;
return nullptr;
}
}
// 调用定时器对象的 add 方法,向定时器中添加一个定时器
return SwooleTG.timer->add(ms, persistent, private_data, callback);
}
这个函数 *Timer::add 会构建一个新的定时器节点,并且设置一些属性值,例如:类型、执行时间、回调函数等。最后,会将定时器节点加入到最小堆的数据结构中。
// 用于向定时器管理器中添加一个新的定时器节点
// swoole-src/src/core/timer.cc:106
TimerNode *Timer::add(long _msec, bool persistent, void *data, const TimerCallback &callback) {
// 检查传入的毫秒数 _msec 是否小于等于 0
if (sw_unlikely(_msec <= 0)) {
swoole_error_log(SW_LOG_WARNING, SW_ERROR_INVALID_PARAMS, "msec value[%ld] is invalid", _msec);
return nullptr;
}
// 获取当前相对毫秒数,并检查其是否小于 0
int64_t now_msec = get_relative_msec();
if (sw_unlikely(now_msec < 0)) {
return nullptr;
}
// 创建一个新的定时器节点 tnode
// 并设置节点的数据、类型、执行时间、间隔、状态、回调函数、轮数以及析构函数
TimerNode *tnode = new TimerNode();
tnode->data = data;
tnode->type = TimerNode::TYPE_KERNEL;
tnode->exec_msec = now_msec + _msec;
tnode->interval = persistent ? _msec : 0;
tnode->removed = false;
tnode->callback = callback;
tnode->round = round;
tnode->destructor = nullptr;
// 更新下一个计划触发时间
// 如果当前没有下一个计划或者新的时间比当前下一个计划更早
// 则更新为新的时间。
if (next_msec_ < 0 || next_msec_ > _msec) {
set(this, _msec);
next_msec_ = _msec;
}
// 给定时器节点分配一个唯一的ID
tnode->id = _next_id++;
if (sw_unlikely(tnode->id < 0)) {
tnode->id = 1;
_next_id = 2;
}
// 将节点加入堆中,同时更新堆的索引
tnode->heap_node = heap.push(tnode->exec_msec, tnode);
if (sw_unlikely(tnode->heap_node == nullptr)) {
delete tnode;
return nullptr;
}
// 记录节点信息
map.emplace(std::make_pair(tnode->id, tnode));
swoole_trace_log(SW_TRACE_TIMER,
"id=%ld, exec_msec=%" PRId64 ", msec=%ld, round=%" PRIu64 ", exist=%lu",
tnode->id,
tnode->exec_msec,
_msec,
tnode->round,
count());
// 返回新添加的定时器节点
return tnode;
}
总结
- Swoole 中实现了毫秒精度的定时器,而原生的 PHP 中只支持到秒级别。
- 数据结构使用最小堆支持添加大量定时器,全部为内存操作且十分高效。
- 定时器在实际的业务场景中应用也是非常广泛,常用于延时或定时执行的任务中,例如:订单超时未付款自动取消等场景。
Swoole 源码分析之 Timer 定时器模块的更多相关文章
- jQuery1.9.1源码分析--数据缓存Data模块
jQuery1.9.1源码分析--数据缓存Data模块 阅读目录 jQuery API中Data的基本使用方法介绍 jQuery.acceptData(elem)源码分析 jQuery.data(el ...
- jQuery 源码分析(十) 数据缓存模块 data详解
jQuery的数据缓存模块以一种安全的方式为DOM元素附加任意类型的数据,避免了在JavaScript对象和DOM元素之间出现循环引用,以及由此而导致的内存泄漏. 数据缓存模块为DOM元素和JavaS ...
- Hadoop2源码分析-HDFS核心模块分析
1.概述 这篇博客接着<Hadoop2源码分析-RPC机制初识>来讲述,前面我们对MapReduce.序列化.RPC进行了分析和探索,对Hadoop V2的这些模块都有了大致的了解,通过对 ...
- Tornado源码分析 --- 静态文件处理模块
每个web框架都会有对静态文件的处理支持,下面对于Tornado的静态文件的处理模块的源码进行分析,以加强自己对静态文件处理的理解. 先从Tornado的主要模块 web.py 入手,可以看到在App ...
- Python 源码分析:queue 队列模块
起步 queue 模块提供适用于多线程编程的先进先出(FIFO)数据结构.因为它是线程安全的,所以多个线程很轻松地使用同一个实例. 源码分析 先从初始化的函数来看: 从这初始化函数能得到哪些信息呢?首 ...
- jQuery 源码分析(十六) 事件系统模块 底层方法 详解
jQuery事件系统并没有将事件监听函数直接绑定到DOM元素上,而是基于数据缓存模块来管理监听函数的,事件模块代码有点多,我把它分为了三个部分:分底层方法.实例方法和便捷方法.ready事件来讲,好理 ...
- jQuery 源码分析(十三) 数据操作模块 DOM属性 详解
jQuery的属性操作模块总共有4个部分,本篇说一下第2个部分:DOM属性部分,用于修改DOM元素的属性的(属性和特性是不一样的,一般将property翻译为属性,attribute翻译为特性) DO ...
- jQuery源码分析(九) 异步队列模块 Deferred 详解
deferred对象就是jQuery的回调函数解决方案,它解决了如何处理耗时操作的问题,比如一些Ajax操作,动画操作等.(P.s:紧跟上一节:https://www.cnblogs.com/grea ...
- flappy pig小游戏源码分析(4)——核心pig模块(未完待续)
热身之后,我们要动点真格的了,游戏叫flappy pig,我们的pig终于要出场了. 老规矩,看看目录结构,读者对着目录结构好好回想我们已经讲解的几个模块: 其中game.js是游戏主程序,optio ...
- WebRTC源码分析四:视频模块结构
转自:http://blog.csdn.net/neustar1/article/details/19492113 本文在上篇的基础上介绍WebRTC视频部分的模块结构,以进一步了解其实现框架,只有了 ...
随机推荐
- Python设计模式----2.工厂模式
工厂方法模式是简单工厂模式的衍生,解决了许多简单工厂模式的问题 首先完全实现'开-闭 原则',实现了可扩展.其次更复杂的层次结构,可以应用于产品结果复杂的场合. 工厂方法模式的对简单工厂模式进行了抽象 ...
- 冒泡排序的基本实现【数据结构与算法—TypeScript 实现】
笔记整理自 coderwhy 『TypeScript 高阶数据结构与算法』课程 概念 本质:相邻元素两两比较并交换位置,使整个序列按照特定的顺序排列 特性 复杂度分析 时间复杂度: 最好情况:O(n) ...
- ArcMap分别求取矢量要素各区域的面积
本文介绍基于ArcMap软件,自动批量计算矢量图层中各个要素的面积的方法. 一次,遇到一个问题,需要分别计算ArcMap软件中一个图层的所有面要素的面积.如图,这个图层中包括多个省级行政区矢量 ...
- 初接触:从创建工程到导出gerber(学习Altium Designer)
学习Altium Designer Altium Designer的工程文件后缀为.PrjPcb,主要包含Source Documents和Libraries.Source Documents里面有S ...
- 力扣392(java)-判断子序列(简单)
题目: 给定字符串 s 和 t ,判断 s 是否为 t 的子序列. 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串.(例如,"ace&quo ...
- 基于 eBPF 的 Kubernetes 可观测实践
简介: 阿里云可观测团队构建了 kubernetes 统一监控,无侵入式地提供多语言.应用性能黄金指标,支持多种协议,结合 Kubernetes 管控层与网络系统层监控,提供全栈一体式的可观测体验.通 ...
- 最佳实践|从Producer 到 Consumer,如何有效监控 Kafka
简介: 对于运维人而言,如何安装维护一套监控系统,或如何进行技术选型,从来不是工作重点.如何借助工具对所需的应用.组件进行监控,发现并解决问题才是重中之重.随着 Prometheus 逐渐成为云原生时 ...
- 高德AR驾车导航解决方案
简介: 高德从2018年首创了车载AR导航后,已经先后在后视镜.智能车盒.前装整车厂.后装车机产品.行车记录仪等众多场景落地应用,搭建了非常完整的AR导航生态. 日前,高德地图最新发布了v10.60新 ...
- 3千字带你搞懂XXL-JOB任务调度平台
思维导图 文章已收录Github精选,欢迎Star:https://github.com/yehongzhi/learningSummary 一.概述 在平时的业务场景中,经常有一些场景需要使用定时任 ...
- 国内唯一连续入选Gartner,Quick BI是如何做到的?
简介:阿里云Quick BI凭借灵活的公共云部署,私有化独立部署能力.无缝对接各类云上数据库和自建数据库.可视化搭建分析.高效数据处理能力与强大数据计算能力,使得在2022年持续入选Gartner ...