高性能定时器介绍及代码逐行解析--时间堆
简介
在《Linux高性能服务器编程》中,介绍了三种定时方法:
- socket选项SO_RCVTIMEO和SO_SNDTIMEO
- SIGALRM信号
- I/O复用系统调用的超时参数
基础知识
- 非活跃,是指客户端(这里是浏览器)与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。
- 定时事件,是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源。
- 定时器,是指利用结构体或其他形式,将多种定时事件进行封装起来。具体的,这里只涉及一种定时事件,即定期检测非活跃连接,这里将该定时事件与连接资源封装为一个结构体定时器。
- 定时器容器,是指使用某种容器类数据结构,将上述多个定时器组合起来,便于对定时事件统一管理。具体的,项目中使用升序链表将所有定时器串联组织起来。
在web服务中,如果某一用户connect()到服务器之后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。这时候就应该利用定时器把这些超时的非活动连接释放掉,关闭其占用的文件描述符。这种情况也很常见,当你登录一个网站后长时间没有操作该网站的网页,再次访问的时候你会发现需要重新登录。
时间堆原理
将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔,这样,一旦心搏函数tick被调用,到期的一定是超时时间最小的时间器。然后,再从剩下的定时器中找出超时时间最小的那个座位心搏间隔,如此反复直至定时器执行完。
从上面的运行过程我们可以看出,最小堆很适合这种定时方案。
但是使用最小堆虽然可以很方便的取得当前时间最小的时间器,但是却不支持随机访问,非常不便于维护连接的过期时间。由于最小堆是一种完全二叉树,所以我们可以用数组来组织其中的元素。
对于数组中的任意一个位置i上的元素,其作儿子节点在位置2i+1上,右儿子节点在位置2i+2上,父节点在位置「(i-1)/2」上。用数组来表示堆不仅节省空间,而且更容易实现堆的插入、删除等操作。
那么如何维护这个数组使它具有最小堆堆性质呢?假设我们已经有一个包含N个元素的数组,现在要把它初始化为一个最小堆。我们可以将元素挨个插入数组中,但是这样效率偏低。正确的做法是:对数组的第「(N-1)/2」~0个元素执行下虑操作,即可保证该数组构成一个最小堆。这是因为包含N个元素的完全二叉树有「(N-1)/2」个非叶节点,这些非叶节点正是该完全二叉树的第0~「(N-1)/2」个节点。我们只需要确保这些非叶子节点构成的子树都具有堆序性质即可。
用数组模拟堆的算法代码(向上调整、向下调整等)见此:《敬请期待。。》
代码实现
// heaptimer.h
#ifndef HEAP_TIMER_H
#define HEAP_TIMER_H#include <queue>
#include <unordered_map>
#include <time.h>
#include <algorithm>
#include <arpa/inet.h>
#include <functional>
#include <assert.h>
#include <chrono>
#include "../log/log.h"typedef std::function<void()> TimeoutCallBack;
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::milliseconds MS;
typedef Clock::time_point TimeStamp;struct TimerNode { // 使用结构体作为时间器int id; // 每个定时器所唯一的TimeStamp expires; // 设置该定时器的过期时间TimeoutCallBack cb; // 当超时时执行回调函数,用来关闭过期连接// 由于需要维护最小堆,所以需要重载比较运算符,方便定时器之间的比较bool operator<(const TimerNode& t) {return expires < t.expires;}
};class HeapTimer { // 管理所有的定时器的堆
public:HeapTimer() { heap_.reserve(64); }~HeapTimer() { clear(); }// 调整指定id的结点void adjust(int id, int newExpires);// 添加一个定时器void add(int id, int timeOut, const TimeoutCallBack& cb);// 删除指定id的节点,并执行其中的回调函数void doWork(int id);void clear();// 处理过期的定时器void tick();// 清除idx = 0的定时器void pop();// 下一次处理过期定时器的时间int GetNextTick();private:// 直接操作时间堆的函数不需要暴露给用户// 删除数组下标为i处的定时器void del_(size_t i);// 向上调整void siftup_(size_t i);// 向下调整bool siftdown_(size_t index, size_t n);// 交换节点void SwapNode_(size_t i, size_t j);// 使用vector来模拟最小堆,里面存的类型是前面定义的时间器std::vector<TimerNode> heap_; // map中存储的信息为<时间器的id,该时间器在vector中的数组下标>std::unordered_map<int, size_t> ref_;
};#endif //HEAP_TIMER_H
#include "heaptimer.h"void HeapTimer::siftup_(size_t i) {assert(i >= 0 && i < heap_.size());size_t j = (i - 1) / 2;while(j >= 0) {if(heap_[j] < heap_[i]) { break; }SwapNode_(i, j);i = j;j = (i - 1) / 2;}
}void HeapTimer::SwapNode_(size_t i, size_t j) {assert(i >= 0 && i < heap_.size());assert(j >= 0 && j < heap_.size());std::swap(heap_[i], heap_[j]);ref_[heap_[i].id] = i;ref_[heap_[j].id] = j;
} bool HeapTimer::siftdown_(size_t index, size_t n) {assert(index >= 0 && index < heap_.size());assert(n >= 0 && n <= heap_.size());size_t i = index;size_t j = i * 2 + 1;while(j < n) {if(j + 1 < n && heap_[j + 1] < heap_[j]) j++;if(heap_[i] < heap_[j]) break;SwapNode_(i, j);i = j;j = i * 2 + 1;}return i > index;
}void HeapTimer::add(int id, int timeout, const TimeoutCallBack& cb) {assert(id >= 0);size_t i;if(ref_.count(id) == 0) {/* 新节点:堆尾插入,调整堆 */i = heap_.size(); ref_[id] = i; // map更新heap_.push_back({id, Clock::now() + MS(timeout), cb}); // 将新节点插入堆中siftup_(i); // 此时新节点为叶节点,向上维护} else {/* 已有结点:调整堆 */i = ref_[id];heap_[i].expires = Clock::now() + MS(timeout); // 更新其超时时间heap_[i].cb = cb;if(!siftdown_(i, heap_.size())) {siftup_(i);}}
}void HeapTimer::doWork(int id) {/* 删除指定id结点,并触发回调函数 */if(heap_.empty() || ref_.count(id) == 0) {return;}size_t i = ref_[id];TimerNode node = heap_[i];node.cb(); // 执行其该定时器的任务del_(i); // 执行结束将定时器删除
}void HeapTimer::del_(size_t index) {/* 删除指定位置的结点 */assert(!heap_.empty() && index >= 0 && index < heap_.size());/* 将要删除的结点换到队尾,然后调整堆 */size_t i = index;size_t n = heap_.size() - 1;assert(i <= n);if(i < n) {SwapNode_(i, n);if(!siftdown_(i, n)) {siftup_(i);}}/* 队尾元素删除 */ref_.erase(heap_.back().id);heap_.pop_back();
}void HeapTimer::adjust(int id, int timeout) {/* 调整指定id的结点 */assert(!heap_.empty() && ref_.count(id) > 0);heap_[ref_[id]].expires = Clock::now() + MS(timeout);;siftdown_(ref_[id], heap_.size());
}void HeapTimer::tick() {/* 清除超时结点 */if(heap_.empty()) {return;}while(!heap_.empty()) {TimerNode node = heap_.front();if(std::chrono::duration_cast<MS>(node.expires - Clock::now()).count() > 0) { break; }node.cb();pop();}
}void HeapTimer::pop() {assert(!heap_.empty());del_(0);
}void HeapTimer::clear() {ref_.clear();heap_.clear();
}int HeapTimer::GetNextTick() {tick();size_t res = -1;if(!heap_.empty()) {res = std::chrono::duration_cast<MS>(heap_.front().expires - Clock::now()).count();if(res < 0) { res = 0; }}return res;
}
相关文章:
高性能定时器介绍及代码逐行解析--时间堆
简介 在《Linux高性能服务器编程》中,介绍了三种定时方法: socket选项SO_RCVTIMEO和SO_SNDTIMEOSIGALRM信号I/O复用系统调用的超时参数 基础知识 非活跃,是指客户端(这里是浏览器)与服务器端建立连接后,…...
汇编语言学习笔记五
div指令 除法, 被除数:默认是放在ax或者dx中,其位数为16位,则在ax中,如位数为32位,则高位在dx中,低位在ax中 除数:放在寄存器或者内存单元中,有8位和16位两种。 结果&am…...
Linux下的epf 是什么?
EPF (Extended Page Frame) 是 Linux 内核中的一个功能,它用于管理大内存系统中的物理页框。具体来说,当系统中的物理内存超过 1TB 时,传统的页表结构会变得非常庞大和复杂,给内存管理带来很大的困难。 EPF 架构通过将物理地址分…...
如何在广告形式选择上化解用户厌恶和变现瓶颈?
用户讨厌广告,这似乎是一个共识。在日复一日的使用中,用户会遇到各种各样的广告形式,从搜索结果中的广告链接,到视频中不间断的广告,再到流行应用中的推广内容。 无处不在的广告已经让用户不胜其烦,这也…...
【Android入门到项目实战-- 9.2】—— 传感器实战使用教程(靠近黑屏和计步器)
上篇文章介绍了传感器的基础用法(如有需要,可先移步),下面将通过两个实战案例学习具体如何使用。 一、靠近黑屏 这是距离传感器的简单应用。 –检测手机是否贴在耳朵上正在打电话,以便自动熄灭屏幕达到省电的目的。也…...
软件项目生命周期模型
目录 瀑布模型 快速原型模型 敏捷模型 迭代模型(增量模型) 螺旋模型 瀑布模型 定义:早就计划好了,按计划顺序(计划、设计、开发、测试、维护)线性执行 适用于:需求明确、变化少的项目 缺…...

linux系统TP-ti,tsc2046外设调试
一、整体调试思路 tp外设属于比较常见且比较简单的外设,今天以ti,tsc2046这款为例简述下tp外设的调试。 整体思路 1、配置设备树----驱动调试的device部分 2、tp驱动编译及匹配—driver部分 3、驱动整体调试 二、配置设备树 对于ti,tsc2046我们可以参考内核Docum…...
ChatGPT指令大全
1. 写报告:我现在正在 [报告的情境与目的]。我的简报主题是 [主题],请提供 [数字] 种开头方式,要简单到 [目标族群] 能听懂,同时要足够能吸引人,让他们愿意专心听下去。 2. 研究报告:写出一篇有关 [知识] …...

【Vue面试题】Vue2.x生命周期?
文章目录 1.有哪些生命周期(系统自带)?beforeCreate( 创建前 )created ( 创建后)beforeMount (挂载前)mount (挂载后)beforeUpdate (更新前)updated (更新后)beforeDestroy(销毁前)destroy(销毁后…...

运算放大器 - 笔记 02 -恒流源
恒流源 / 电流源 一、方案一二、方案二三、方案三四、方案四 前言:最近在学习运放,三极管,二极管,场效应管等器件的组合电路。捡起了以前的模电知识,写下笔记,以防再度忘记。 本文使用Multisim仿真软件进行…...

Python:Python进阶:Python字符串驻留技术
Python字符串驻留技术 1.什么是字符串驻留2. 为什么要驻留字符串3. Python的字符串驻留4. Python 字符驻留原理4.1 如何驻留字符串4.2 如何清理驻留的字符串 5. 字符串驻留的实现5.1. 变量、常量与函数名5.2 字典的键5.3 任何对象的属性5.4 显式地驻留 6 字符串驻留的其他发现 …...

2022年 全国职业院校技能大赛(中职组)网络安全赛项 正式赛卷 A模块 做题记录
评分标准文件及环境 评分标准:ZZ-2022029 网络安全赛项正式赛卷.zip 自己做的Linux靶机: 自己做的Windows靶机: 文章目录 评分标准文件及环境A-1 任务一 登录安全加固1. 密码策略(Windows,Linux)a. 最小密…...
华为OD机试 - 优选核酸检测点(Python)
题目描述 张三要去外地出差,需要做核酸,需要在指定时间点前做完核酸,请帮他找到满足条件的核酸检测点。 给出一组核酸检测点的距离和每个核酸检测点当前的人数给出张三要去做核酸的出发时间 出发时间是10分钟的倍数,同时给出张三做核酸的最晚结束时间题目中给出的距离是整…...

windows怎么把包含某个关键词的文件移动到一个文件夹中
文章目录 windows怎么把包含某个关键词的文件移动到一个文件夹中问题来源省流版本操作过程具体问题方法一:使用cmd终端解决方法二:使用python脚本 总结 windows怎么把包含某个关键词的文件移动到一个文件夹中 问题来源 今天想移动window文件࿰…...

Unity 后处理(Post-Processing) -- (2)创建后处理配置文件
通过前面一小节,我们初步认识了后处理是什么,在Unity中简单的试了试后处理的效果。本节我们来创建一个我们自己的后处理配置文件(post-processing profile)。 一个后处理配置文件包含了一系列为了达到特定视觉效果的后处理效果的配…...

BI 商业智能和报表,傻傻分不清楚?一文给你讲透
我们经常所听到的大数据、商业智能BI、数据分析、数据挖掘等我们都统称为数据信息化。数据信息化可以帮助企业全面的了解企业的经营管理,从经验驱动到数据驱动,降低情绪、心理等主观影响,形成以数据为基础的业务决策支撑,提高决策…...
CSS布局基础(传统布局小结)
传统布局小结 传统布局方式标准流浮动流定位伪类元素CSS应用对象应用到自身应用到其他元素 传统布局方式 传统布局采用 标准流 浮动流 定位的方式实现布局效果,也就是通常所说的 DIV CSS 布局。 标准流 标准流中的元素在 页面默认的 维度,块级元素…...

【五一创作】Qt quick基础1(包含基本元素Text Image Rectangle的使用)
Qt quick基础1(包含基本元素Text Image Rectangle的使用) 目录 Qt quick基础1(包含基本元素Text Image Rectangle的使用)前言qt中有直接设计ui的拖拽式的widget,为什么还需要Qtquick?QML语言Qt 版本创建一个Qt quick项…...

LVS+Keepalived 高可用群集部署
一、LVSKeepalived 高可用群集 在这个高度信息化的 IT 时代,企业的生产系统、业务运营、销售和支持,以及日常管理等环节越来越依赖于计算机信息和服务,对高可用(HA)技术的应用需求不断提高,以便提供持续的…...

小黑子—Java从入门到入土过程:第八章
Java零基础入门8.0 Java系列第八章1. 双列集合 Map1.1 Map 集合中常见的API1.2 Map 集合的遍历方式1.2 - I 第一种遍历方式:键找值KeySet 方法1.2 - II 第二种遍历方式:键值对 entrySet 方法1.2 - III 第三种遍历方式:lambda表达式 1.3 HashM…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...