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视频部分的模块结构,以进一步了解其实现框架,只有了 ...
随机推荐
- 基于pdfbox实现的pdf添加文字水印工具
简述 最近有个需求需要给pdf加文字水印,于是开始搜索大法,但是发现网络上的代码基本都是将字体文件直接放在jar包里面.个人强迫症发作(手动狗头),想要像poi一样直接加载系统字体,于是研究了一下午p ...
- 美丽的夕阳qsnctfwp
题目附件 查看图片,放大左侧发现建筑物上 8 个字:龙腾公寓/福阳集团 根据文字在搜索引擎中查找,并由此确定城市 通过百度地图全景地图查看当地桥梁,并与照片比对 调整地图比例尺,记录桥名 根据提示qs ...
- .NET Emit 入门教程:第六部分:IL 指令:8:详解 ILGenerator 指令方法:类型转换指令
前言: 经过前面几篇的学习,我们了解到指令的大概分类,如: 参数加载指令,该加载指令以 Ld 开头,将参数加载到栈中,以便于后续执行操作命令. 参数存储指令,其指令以 St 开头,将栈中的数据,存储到 ...
- 力扣168(java)-Excel表列名称(简单)
题目: 给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称. 例如: A -> 1B -> 2C -> 3...Z -> 26AA -> ...
- 【实用教程】在配备持久内存的实例上部署Redis应用
简介:配备持久内存的实例(例如re7p.r7p.re6p)提供了超大CPU内存配比,Redis应用运行在这类实例上可以大幅度降低单GiB内存的成本.本文以部分操作系统为例,介绍如何在这类实例上快速部署 ...
- 用手机「3D探店」是种什么体验?
简介: 未来场景尽在眼前!阿里云3D全景网站通过云端算法技术自动建模,将线下场景1:1真实还原到线上,让用户足不出户就可以感受到真实的3D空间漫游效果. 在手机里用3D探店打卡是种什么样的体验? 走进 ...
- [FAQ] SSH 免密登录主机/服务器 怎么操作 ?
1. 生成公私钥对,保存好. 命令:ssh-keygen -t rsa -C "xxx" 2. 将公钥传到远程主机的 ~/.ssh/authorized_keys 之中. 命令:s ...
- dotnet 在 UOS 国产系统上使用 MonoDevelop 进行拖控件开发 GTK 应用
先从一个 Hello World 应用开始,试试和古老的 WinForms 一样的拖控件式开发 在创建完成一个 GTK# 2.0 应用之后,咱可以试试开始拖控件的开发,当然这个开发方式开发出来的应用界 ...
- Pod进阶篇:污点-容忍度-亲和性-Affinity-调度(5)
一.Pod资源清单详细解读 apiVersion: v1 #版本号,例如 v1 kind: Pod #资源类型,如 Pod metadata: #元数据 name: string # Pod 名字 n ...
- mosquitto移植到ARM
了解mosquitto的小伙伴多数都是想在arm中进行开发,所以将mosquitto移植到ARM板上就尤为重要了,当然也有在x86中进行应用开发的,想了解linux中安装mosquitto可以看我 ...