08 | Swoole 源码分析之 Timer 定时器模块

news2024/11/16 16:34:15

原文首发链接:Swoole 源码分析之 Timer 定时器模块
大家好,我是码农先森。

引言

Swoole 中的毫秒精度的定时器。底层基于 epoll_waitsetitimer 实现,数据结构使用最小堆,可支持添加大量定时器。

在同步 IO 进程中使用 setitimer 和信号实现,如 ManagerTaskWorker 进程,在异步 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_afterswoole_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(&params[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 中只支持到秒级别。
  • 数据结构使用最小堆支持添加大量定时器,全部为内存操作且十分高效。
  • 定时器在实际的业务场景中应用也是非常广泛,常用于延时或定时执行的任务中,例如:订单超时未付款自动取消等场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1574998.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

three.js能实现啥效果?看过来,这里都是它的菜(01)

经常有老铁问我&#xff0c;这里炫酷效果是如何实现的&#xff0c;还有问我想实现什么效果怎么办&#xff0c;甚至还有想实现动态效果&#xff0c;但是描述不出来的。 好吧&#xff0c;统统满足老铁们呢&#xff0c;本期开始分享three.js效果图&#xff0c;并附带简要简介&…

usbserial驱动流程解析_Part2_初始化流程_以probe为例(echo cat测试回环打印不停问题解决)

usb转串口设备需要注册usb侧和serial侧两侧的操作&#xff0c;本文将简要分析二者的初始化流程以及一些关键函数的初始化流程。 module_init(usb_serial_init); tty设备初始化 内核会直接调用usb_serial_init&#xff0c;开始进行usb和serial的初始化&#xff0c;首先是进行t…

全面探究 LangChain Text Splitters

全面探究 LangChain Text Splitters 0. 引言1. 文本拆分器的类型2. 探究各个文本拆分器2-1. Split by HTML header2-2. Split by HTML section2-3. Split by character2-4. Split code2-5. MarkdownHeaderTextSplitter2-6. Recursively split JSON2-7. Recursively split by ch…

lv17 CGI移植 5-1

简介 CGIC是一个支持CGI开发的开放源码的标准C库&#xff0c;可以免费使用&#xff0c;只需要在开发的站点和程序文档中有个公开声明即可&#xff0c;表明程序使用了CGIC库&#xff0c;用户也可以购买商业授权而无需公开声明。 CGIC能够提供以下功能&#xff1a; 分析数据&a…

疲劳驾驶预警系统项目知识点整理

参考&#xff1a; 重磅&#xff01;头部姿态估计「原理详解 实战代码」来啦&#xff01;-阿里云开发者社区 (aliyun.com) Dlib模型之驾驶员疲劳检测三&#xff08;瞌睡点头&#xff09;_疲劳检测 点头-CSDN博客 python毕业设计 深度学习疲劳检测 驾驶行为检测 - opencv cnn…

面试:HashMap

目录 1、底层数据结构&#xff0c;1.7 与1.8有何不同? 2、为何要用红黑树&#xff0c;为何一上来不树化&#xff0c;树化阈值为何是8&#xff0c;何时会树化&#xff0c;何时会退化为链表? 3、索引如何计算? hashCode都有了&#xff0c;为何还要提供hash()方法?数组容量为…

交换机与队列的简介

1.流程 首先先介绍一个简单的一个消息推送到接收的流程&#xff0c;提供一个简单的图 黄色的圈圈就是我们的消息推送服务&#xff0c;将消息推送到 中间方框里面也就是 rabbitMq的服务器&#xff0c;然后经过服务器里面的交换机、队列等各种关系&#xff08;后面会详细讲&…

保研复习数据结构-图(10)

一.图的定义和基本术语 1.什么是图&#xff1f; 图(Graph)是由顶点的有穷非空集合V(G)和顶点之间边的集合E(G)组成&#xff0c;通常表示为:G(V,E)&#xff0c;其中&#xff0c;G表示图&#xff0c;V是图G中顶点的集合&#xff0c;E是图G中边的集合。 2.什么是完全图&#xf…

【第十二篇】使用BurpSuite实现CSRF(实战案例)

CSRF存在前提:简单的身份验证只能保证请求是发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的 业务场景:新增、删除、收藏、编辑、保存使用Burp发现CSRF漏洞的过程如下。 1、如图,存在修改邮箱的功能点如下: 2、修改邮箱的流量包,此时邮箱已被修改: 思路:是…

12、最小覆盖子串

如何想到这个解法 问题的特点&#xff1a; 首先&#xff0c;认识到这是一个关于子串的问题&#xff0c;而且需要考虑子串的最小长度。这提示我们可能需要使用一种方式来逐步探索不同的子串。滑动窗口的适用性&#xff1a;滑动窗口是处理子串问题的常用技巧&#xff0c;特别是当…

X年后,ChatGPT会替代底层程序员吗?

能不能替代&#xff0c;真的很难说&#xff0c;因为机器换掉人&#xff0c;这其实是一个伦理问题。 其实说白了&#xff0c;任何行业在未来都会被AI或多或少的冲击到&#xff0c;因为ChatGPT做为一个可以持续提升智能的AI&#xff0c;在某些方面的智能程度超过人类并不是什么难…

笛卡尔树[天梯赛二叉树专项训练]

文章目录 题目描述思路AC代码 题目描述 输入样例1 6 8 27 5 1 9 40 -1 -1 10 20 0 3 12 21 -1 4 15 22 -1 -1 5 35 -1 -1 输出样例1 YES 输入样例2 6 8 27 5 1 9 40 -1 -1 10 20 0 3 12 11 -1 4 15 22 -1 -1 50 35 -1 -1 输出样例2 NO思路 见注释 AC代码 #include <bits/st…

5. 4 二重循环将二维数组的某列、某矩形转大写

5. 4 二重循环将二维数组的某列、某矩形转大写 1. 把每一行的b都变成大写 assume cs:codesg,ds:data,ss:stack data segmeNTstr db aaaaabbbbbcccccdb aaaaabbbbbcccccdb aaaaabbbbbcccccdb aaaaabbbbbccccc,$ data endsstack segmentdb 10 dup(0) stack endscodesg SEgments…

【Vue】Vue3中的OptionsAPI与CompositionAPI

文章目录 OptionsAPICompositionAPI对比总结 OptionsAPI 中文名:选项式API通过定义methods,computed,watch,data等属性方法&#xff0c;处理页面逻辑。以下是OptionsAPI代码结构 实例代码: <script lang"ts">// js或者tsimport { defineComponent } from vu…

豆瓣9.7,这部Java神作第3版重磅上市!

Java 程序员们开年就有重磅好消息&#xff0c;《Effective Java 中文版&#xff08;原书第 3 版&#xff09;》要上市啦&#xff01; 该书的第1版出版于 2001 年&#xff0c;当时就在业界流传开来&#xff0c;受到广泛赞誉。时至今日&#xff0c;已热销近20年&#xff0c;本书…

React - 连连看小游戏

简介 小时候经常玩连连看小游戏。在游戏中&#xff0c;当找到2个相同的元素就可以消除元素。 本文会借助react实现连连看小游戏。 实现效果 实现难点 1.item 生成 1. 每一个图片都是一个item&#xff0c;items数组的大小为size*size。 item对象包括grid布局的位置&#xff0c;…

UE小:UE5.3无法创建C++工程

当您在使用Unreal Engine (UE) 构建项目时&#xff0c;如果遇到以下问题&#xff1a; Running C:/Program Files/Epic Games/UE\_5.3/Engine/Build/BatchFiles/Build.bat -projectfiles -project"C:/UEProject/Shp\_1/Shp\_1.uproject" -game -rocket -progress Usi…

网络基础三——初识IP协议

网络基础三 ​ 数据通过应用层、传输层将数据传输到了网络层&#xff1b; ​ 传输层协议&#xff0c;如&#xff1a;TCP协议提供可靠性策略或者高效性策略&#xff0c;UDP提供实时性策略&#xff0c;保证向下层交付的数据是符合要求的的&#xff1b;而网络层&#xff0c;如&a…

websokcet服务端实现

一/websokcet服务端实现 步骤一&#xff1a; springboot底层帮我们自动配置了websokcet&#xff0c;引入maven依赖 1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</arti…

分享10个免费高可用的GPT3.5和4.0网站并做功能测试【第一个】

1.介绍 网址&#xff1a;直接点&#xff1a;aicnn 或者 www.aicnn.cn 基于ChatGPT可以实现智能聊天、绘画生成、高清文本转语音、论文润色等多种功能&#xff0c;基于sd和mj实现的绘画功能&#xff0c;下面是功能测试&#xff1a; 博主从 1.GPT3.5是否完全免费/是否限制频率、…