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. Centos7 安装python3与python2.7 共存

    前言 在centos7服务器上,linux系统默认安装有python2.7,这是系统服务等会依赖到的,所以系统的python2.7是不可以卸载的,避免系统出现问题.那么问题就来了,我们现在使用的pyt ...

  2. Mac之终端工具iterm2

    1. 安装 iTerm2 下载地址:https://www.iterm2.com/downloads.html 下载的是压缩文件,解压后是执行程序文件,你可以直接双击,或者直接将它拖到 Applica ...

  3. linux模拟HID USB设备及wireshark USB抓包配置

    目录 1. 内核配置 2. 设备配置 附 wireshark USB抓包配置 笔者开发USB设备时的一些记录 1. 内核配置 内核启用USB Gadget,使用fs配置usb device信息. De ...

  4. Flex 弹性布局备忘录

    概述 Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性 这也是我目前用的最多的一种布局方案,相比Grid布局此种布局方案相对较简单, ...

  5. MongoDB|TOMCAT定时切割日志文件的脚本

    MongoDB用过一段时间后,日志较大,需要定时进行日志切割. 一.切割bash: splitlogmongo.sh #!/bin/bash log_dir="/home/mongodb/l ...

  6. 鸿蒙应用开发从入门到入行 - 篇4:层叠布局、自定义组件、ForEach

    第四篇 - 层叠布局.自定义组件.ForEach循环生成组件 导读:在本篇文章里,您将掌握层叠布局.自定义组件的用法,特别是自定义组件将来的开发中必然会用,其中应该特别关注自定义组件的一些规范与装饰器 ...

  7. 【Web前端】【开源分享】H5登陆界面 - 2021年12月24日

    点我下载

  8. 安装Spring源码时报错No such property: values for class: org.gradle.api.internal.tasks.DefaultTas

    IDEA进行项目拉取时:No such property: values for class: org.gradle.api.internal.tasks.DefaultTas 修改spring-be ...

  9. 龙哥量化:通达信分时均线在5分钟K线图上显示

    代写技术指标.量化策略,微信:Long622889 龙哥QQ:591438821 T1:=DAY<>REF(DAY,1);T2:=BARSLAST(T1)+1;JX:SUM(AMO,T2) ...

  10. 解决容器Docker内部启动计划任务crontab不生效不执行的问题

    docker使用centos7镜像 首先编辑crond,注释#session    required   pam_loginuid.so: vi /etc/pam.d/crond 然后重启crond服 ...