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视频部分的模块结构,以进一步了解其实现框架,只有了 ...
随机推荐
- Qt操作sqlite数据库
代码讲解: 1.检查数据库文件是否存在,如果不存在就创建数据库文件 2.创建 person 表(等下的操作就是操作这个表) 3.查询出 person 表中所有的数据,并显示出来 Pro 文件 添加 S ...
- 可视化库 pygal 无法保存成本地文件
问题:在使用可视化库 pygal 保存图像到本地时,出现报错 第一次报错是,提示没有 cairosvg 这个模块,所以直接通过 pip 安装 pip install cairosvg 安装完了以后 ...
- CentOS 6.5 LAMP分主机平台的搭建及测试
CentOS 6.5 LAMP分主机平台的搭建及测试 看似非常之完备,转来抽空测试学习 原文地址:http://www.it165.net/os/html/201403/7595.html 前言 ...
- c# 解决死锁问题Monitor
前言 在高并发中,一个很关键的问题就是要避免死锁. 那么为什么会产生死锁呢?这种情况多见吗? 举一个例子: 比如方法一中先lock(object1),在lock(object1)中lock(objec ...
- 01矩阵-【BFS】
01矩阵 给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离.两个相邻元素间的距离为 1 ,方格斜方向不计算距离. 示例 1: 输入: [0 0 0 0 1 0 0 0 0] 输出: ...
- WPF随笔收录-RestSharp下载文件406问题
一.前言 在项目开发过程中,涉及到通过http下载文件的需求,最近遇到一个406问题,由于第一次接触这个问题,也被问题卡了好久,在网上风暴了很久才找到解决办法: 二.解决方法 解决的办法就是在requ ...
- KubeNode:阿里巴巴云原生 容器基础设施运维实践
简介: 目前 KubeNode 已经覆盖了阿里巴巴集团的所有的 ASI 集群,接下来,将随着阿里巴巴集团"统一资源池"的项目,推进 KubeNode 覆盖更大的范围.更多的场景,让 ...
- 一文详解Redis中BigKey、HotKey的发现与处理
简介: 在Redis的使用过程中,我们经常会遇到BigKey(下文将其称为"大key")及HotKey(下文将其称为"热key").大Key与热Key如果未能及 ...
- 基于 MaxCompute 的智能推荐解决方案
简介: 在互联网行业红利已过.在获客成本越来越高.在用户在线时长全网基本无增长以及信息大爆炸的情况下,如何更好的转化新用户和提升老用户粘性就变得至关重要,智能化的个性化推荐无疑是经过验证的重要手段之一 ...
- GitHub 的 Action 判断仅在主仓库才执行脚本
我有一个 GitHub 项目,这个项目配置了仅需要在源仓库才能执行的 Action 如推送 NuGet 等发布动作.如何在 Action 里面设置让 Fork 的仓库不执行 Action 的步骤 想要 ...