Timer的实现挺值得拿出来聊一聊的

Anoii网络库的事件循环:

Timer是使用poll的timeout参数实现的,所以定时的精度是毫秒,对于一个网络库而言这足够了。如果不够的话,还可以使用timerfd来提升精度。

由于需要计算poll的timeout是多少,就需要组织起来所有的Timer,计算下一次触发timeout的时间。所以我将所有的Timer用std::set组织起来了。具体的说是std::set<mstime_t, Timer*>,第一个参数是该Timer被触发的时间,第二个参数就是Timer本身。实际上组织Timer的对象就是TimerQueue

使用std::set而不是std::map<mstime_t, Timer*>的原因是,同一个时间可能存在两个Timer被触发,另一个做法是unordered_map,不过没必要这么做,而且用它的人也少。

难点

实现可以见https://github.com/Afeather2017/anoii/blob/master/src/timer_queue.cc

设计一个Timer的难点是,如何确保其生命周期?

如果一个Timer的生命周期与程序差不多长,那么就不存在这种问题,但是如果一个临时的对象使用Timer呢?

Anoii中用于建立连接的Connector就存在这样的问题:

Connector在建立连接的过程中需要进行重试,每次重试的时间间隔是1s, 2s, 4s, 8s...,这样就必须使用定时器了。但是Connector建立连接的过程中,用户应当要能够随时取消建立连接,这意味Connector必须要能够取消Timer。

总不能Connector释放了,Timer还继续使用Connector吧?那么如何设计一个Timer的取消?

智能指针

最容易想到的方案是,使用智能指针。Connector保留一份Timer的shared_ptr,然后Timer在调用之前检查一下引用计数,如果为1那么这个Timer就没必要继续调用了。反过来Timer也许要保留一份Connector的weak_ptr,在调用之前要升级为shared_ptr再调用。但是由于我确保了Connector和Timer的处理都是在同一个线程的,这种做法似乎不是很必要。

直接释放内存

调用取消的时候马上就释放Timer是不可行的。对于一个多线程程序而言,一个线程调用Timer的取消的时候,另一个线程可能还在使用Timer。虽然可以通过确保二者在同一个线程来解决这个场景的问题,但是如果Timer进行了自取消呢?

设置标志位

所以给Timer设计一个统一的取消方式:Timer在调用回调的时候,根据返回值决定是否释放该Timer。返回为false就释放该Timer,返回为true,则该Timer继续定时。

然后,给Timer添加一个标志位,如果它被取消了,就设置该标志。如果标志被设置了,那么在下一次处理Timer的时候就释放它。设置标志位的好处是,别的对象可以正常取消Timer,一个Timer可以取消另一个Timer,Timer也可以自取消。

取消的操作移交到处理该Timer的线程操作,所以不需要锁也可以正常设置该标志位。

取消操作的幂等性

接下来还有一个问题,Timer的重复取消问题。

如果一个Timer被取消了,也被释放了,那么此时设置标志位就是个问题。

解决这个问题的方法是,在TimerQueue中添加一个std::map<TimerId, Timer*>,TimerId是一个64位整数,它只会递增。添加Timer的时候往里面添加其Id以及Timer指针,取消的时候检查一下该Id是否还存在,否则就直接返回。

这样一来,取消操作就具有幂等性了

改进设计

  1. 由于Timer的回调都能够返回值了,那么下一次定时的长度不妨就直接用这个返回值来做。即如果回调返回的值小于0,就可以释放该定时器,如果大于0,那么下一次定时的时间是当前时间+返回的值。这样一来Connector的逐渐增加定时时长就可以轻易的实现了,这样还可以节省malloc次数

  2. Timer自己都带有标志位了,那么就让标志位为其Id得了。第一个Id的值是1,Id大于0表示该Timer还有效,否则无效且可以被释放。取消的时候设置为-Id,这样也不用担心负数设置的时候出现值溢出(因为整数的负数范围比正数的大1,据说Java有个著名的库,好像是Netty,曾经在这上面踩坑,即-INT_MIN = INT_MIN的问题)

所以最终的Timer如下

class Timer {
Timer(std::function<mstime_t(mstime_t)> cb, TimerId id);
void Cancel();
mstime_t Call(mstime_t mstime);
std::function<mstime_t(mstime_t)> cb_;
TimerId timer_id_;
friend TimerQueue;
};

已经修复的BUG1: 定时器时间错误

在编译安卓的时候,发现chargenserver时不时的会阻塞而没有反应。由于缺乏日志,在安卓上又缺乏调试手段,所以一开始没有找到问题。

发现阻塞问题后,我仔细回想了一下哪些地方可能造成长时间阻塞的。第一个是read没有设置非阻塞,第二个是poll中的timeout值为-1。于是在这些地方加了日志,发现poll的timeout设置为-1。果不其然,发现了timeout为-1。

-1的来源是TimerQueue中的SleepTime():

mstime_t TimerQueue::SleepTime() {
struct timeval tv;
gettimeofday(&tv, NULL);
mstime_t temp = timers_.begin()->first - (tv.tv_sec * 1000 + tv.tv_usec / 1000);
if (temp <= 0) {
return 0;
}
return temp;
}

在temp获取之后,加了日志,发现temp的值为负数。

为什么?后面仔细一想,这个函数的行为应当是这样的才对:

  1. 如果没有定时,返回-1
  2. 如果有定时,获取第一个定时器的时间,与当前时间相减,如果是负数,返回0,否则返回相减的值

但是上面代码的行为是:

  1. 如果没有定时,返回timers_.begin()->first - 当前时间,但是由于没有定时,所以timers_.begin()的值可能是一个垃圾值,所以返回的值无法确定,返回-1也是有可能的
  2. 如果有定时,返回第一个定时器与当前的差值,如果为负数,返回-1,这会让poll阻塞了,否则返回差值

所以修改很简单:

mstime_t TimerQueue::SleepTime() {
if (timers_.size() == 0) return -1;
struct timeval tv;
gettimeofday(&tv, NULL);
mstime_t temp = timers_.begin()->first - (tv.tv_sec * 1000 + tv.tv_usec / 1000);
if (temp <= 0) {
return 0;
}
return temp;
}

Anoii网络库之Timer实现的更多相关文章

  1. boost.ASIO-可能是下一代C++标准的网络库

    曾几何时,Boost中有一个Socket库,但后来没有了下文,C++社区一直在翘首盼望一个标准网络库的出现,网络上开源的网络库也有不少,例如Apache Portable Runtime就是比较著名的 ...

  2. Linux多线程服务端编程:使用muduo C++网络库

    内容推荐本 书主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread.这 ...

  3. 公布一个基于 Reactor 模式的 C++ 网络库

    公布一个基于 Reactor 模式的 C++ 网络库 陈硕 (giantchen_AT_gmail) Blog.csdn.net/Solstice 2010 Aug 30 本文主要介绍 muduo 网 ...

  4. Mudo C++网络库第三章学习笔记

    多线程服务器的适用场合与常用编程模型 进程间通信与线程同步; 以最简单规范的方式开发功能正确.线程安全的多线程程序; 多线程服务器是指运行在linux操作系统上的独占式网络应用程序; 不考虑分布式存储 ...

  5. Mudo C++网络库第二章学习笔记

    线程同步的精要 并发有两种基本的模型: 一种是message passing(消息传递); 另一种是shared memory(共享内存); 在分布式系统中(有多台物理机需要通信), 运行在多台机器上 ...

  6. muduo网络库学习笔记(三)TimerQueue定时器队列

    目录 muduo网络库学习笔记(三)TimerQueue定时器队列 Linux中的时间函数 timerfd简单使用介绍 timerfd示例 muduo中对timerfd的封装 TimerQueue的结 ...

  7. libiop网络库数据结构和基础知识

    最近朋友推荐,学习了libiop这个网络库,作者封装的很全面,代码很简洁 适合初学者学习基于事件驱动的网络io 先看看iop_def.h, 这里面定义了常用的数据结构 tag_iop_base_t 主 ...

  8. 网络库libevent、libev、libuv对比

    Libevent.libev.libuv三个网络库,都是c语言实现的异步事件库Asynchronousevent library). 异步事件库本质上是提供异步事件通知(Asynchronous Ev ...

  9. Gevent高并发网络库精解

    进程 线程 协程 异步 并发编程(不是并行)目前有四种方式:多进程.多线程.协程和异步. 多进程编程在python中有类似C的os.fork,更高层封装的有multiprocessing标准库 多线程 ...

  10. Cowboy 开源 WebSocket 网络库

    Cowboy.WebSockets 是一个托管在 GitHub 上的基于 .NET/C# 实现的开源 WebSocket 网络库,其完整的实现了 RFC 6455 (The WebSocket Pro ...

随机推荐

  1. Nuxt.js 应用中的 webpack:progress 事件钩子

    title: Nuxt.js 应用中的 webpack:progress 事件钩子 date: 2024/11/27 updated: 2024/11/27 author: cmdragon exce ...

  2. CSS3 transform转换

    1.先说说css的坐标系: x轴的正方向就是水平向右的方向 y轴的正方向就是垂直向下的方向 z轴的正方向就是屏幕到用户的方向 2.位移 说明:位移是转换属性中的一个值,包含2d与3d 属性值 说明 t ...

  3. 《JavaScript 模式》读书笔记(6)— 代码复用模式1

    我们有开始进入新篇章了.这篇内容主要讲代码复用模式,实际上代码复用,就是继承啊,原型啊,构造函数啊等等这一类的内容.对于前端进阶来说,是很重要的基础知识.这一篇内容会对原型. 继承有很深入的讲解.我也 ...

  4. 树莓派4B 多屏 QT程序窗口全屏 QScreen 只能获取1个屏幕

    直接运行程序时,窗口全屏, 并且QScreen 只能获取1个屏幕,这是由于QT默认使用了EGLFS.(坑了一下午) 所以必须采用命令方式打开程序. ./程序名称 -platform xcb

  5. windows版 nvm 1.1.7 安装(填坑)

    参考https://www.jianshu.com/p/cbf4f76ba0bb安装,注意事项: 1. 最好下载Setup安装版本,带安装界面,这样可以填写安装路径以及Nodejs路径,省去了改文件的 ...

  6. Dockerfile轻松打包jar包生成docker

    1. 创建java目录 mkdir /home/java/ cd /home/java/ 2. 创建Dockerfile #FROM openjdk:8-jdk-alpine #ADD *.jar a ...

  7. ASCII 与 Unicode 中的引号

    原文地址:https://www.cl.cam.ac.uk/~mgk25/ucs/quotes.html 摘要 请不要使用 ASCII 中的重音符号 ` (0x60) 作为左边与 ASCII 中的撇号 ...

  8. ng-alain: delon/abc/sc 简化容器

    简化表单HTML模板的高阶组件,并进一步优化了一些细节: 更友好的表单校验状态 自动化响应式布局 自动维护表单 id 它由 se-container 容器(指令)和 se 组件来表示一个表单,一个简单 ...

  9. 这些“人美话又多”的同事们:2022 Q1 招聘人员 评优名单公布

    ​ ​ 编辑 ​ 编辑 ​ 编辑 ​ 编辑 ​ 编辑 ​ 编辑 ​ 编辑 ​ 编辑 ​ 编辑 欢迎大家后台留言报名哈~ ​

  10. 2053C - Bewitching Stargazer

    简化题意 一个$ 1至n \(的区间,如果其长度是奇数,\)ans \(+=\) mid\(,再分为两个区间\)l\(~\)mid-1\(和\)mid+1\(~\)r\(,否则分为\)l\(~\)mi ...