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. Codeforces Round #826 (Div

    Codeforces Round #826 (Div. 3) Minimize the Thickness 给定数组a,要求将数组a分成若干个子序列,并且使得每个子序列中的元素和都相等,设这些子序列中 ...

  2. Sealos AI Proxy 发布!一个平台调用所有大模型,再也不用到处找 API 了

    你是一位开发者,你需要调用各类 AI 模型,每次调用模型,都要在不同的平台间反复横跳,你大概会遇到以下问题: 获取 API Key 流程繁琐:需访问多个厂商的官网,查阅各自的使用文档,并按照规定的步骤 ...

  3. 论文解读《From Generation to Judgment: Opportunities and Challenges of LLM-as-a-judge》

    发表时间:2024 期刊会议:arxiv 论文单位:Arizona State University 论文作者:Dawei Li, Bohan Jiang, Liangjie Huang, Alimo ...

  4. JAVA开发规范v1.0

    01-中铜国贸JAVA开发规范v1.0 一.编程规约 (一)命名风格 [强制]代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束. 反例:_name / _name / $Obje ...

  5. 企业IT基础资源管理的“帮帮团”上线啦——源启云原生基础设施管理平台

    为助力企业提升基础资源一体化管理和交付效率,以更先进的基础设施管理方式来满足现代企业业务持续扩展和复杂化的需要,中电金信运用基础设施即代码(Infrastructure as Code,简称IaC)技 ...

  6. gitlab16 gitlab-runner

    gitlab-runner verify --delete FederatedKMeansSecureModelInference gitlab-runner register  --url http ...

  7. Linux命令行/终端连接(隐藏)SSID的WiFi

    推荐看完Linux命令行/终端连接隐藏SSID的WiFi(续篇)和本文后,再按照实际情况采用network-manager或者ifupdown 多数Linux系统默认自带有线网络的驱动和配置软件,但是 ...

  8. 鲲鹏cpu

    cat /etc/openEuler-release sudo dmidecode -t processor |grep Version   lscpu  

  9. docker-compose.yml 使用说明

    docker-compose.yml 结构 docker-compose.yml文件分为三个主要部分:services.networks.volumes..services主要用来定义各个容器.net ...

  10. Qt音视频开发29-ffmpeg中x264/x265编码库支持

    一.前言 有了解码当然对应又有编码,编码是信息从一种形式或格式转换为另一种形式的过程也称为计算机编程语言的代码简称编码.用预先规定的方法将文字.数字或其它对象编成数码,或将信息.数据转换成规定的电脉冲 ...