1. 定时接口
- sleep函数族: sleep,usleep,nanosleep,clock_nanosleep
特点:有一定的精度,但是会使线程挂起。
- 信号:alarm,setitimer
特点:采用了信号SIGALRM,由于SIGALRM信号不可靠,会造成超时通知不可靠,而且多线程中处理信号比较复杂。
- Linux定时接口:timer_create/timer_settime
特点:先创建,再设置。创建时可以配置回调;精度在纳秒级别;需要librt库;想要与业务程序配合,需要进行一定的封装,封装成本比较高。
- 具有定时功能的API: 多路复用。在Linux上的多路复用机制有select/poll/epoll。
特点:poll/epoll是毫秒级的(millisecond),select超时参数是struct timeval,是微秒级的(microsecond)。
epoll的优势很明显,能将定时功能完美的融入已有的event loop里,有着天然的高并发的能力,millisecond级的精度也足够用。
2. 系统时间
- time 特点:精度太低,不适合精确定时。
- ftime 特点:毫秒级精度,但是被废弃了。
- gettimeofday 特点:使用了vdso技术,精度达到微秒级,并且在x86-64平台上该函数的调用不是系统调用(vdso),POSIX.1-2008中也将这个函数废弃了。
- Time Stamp Counter 使用汇编指定获取时间戳的计数器,精度应该是最高的,效率可能也应该是最高的。
特点: 一条汇编指令rdtscp即可,libco就是优先使用这个方法获取时间的。
- clock_gettime 默认是nanosecond 级精度,是系统调用(_sys_clock_gettime()),会有开销。
特点:调用频繁的话,可能造成损失性能。但是Linux 2.6.32后可以指定参数 CLOCK_REALTIME_COARSE 和 CLOCK_MONOTONIC_COARSE,粗粒度地获取时间,而不需要发生上下文切换。
和gettimeofday()一样也是vdso技术,使用_COARSE后缀获取的时间,精度是millisecond级。
3. 定时器的设计
从上述的总结,开源实现和别人的总结中多路复用模式可以达到更好的精度和并发。
epoll 每次只能设置一个超时时间,不能满足需求。
① 结合clock_gettime获取系统时间,然后再设置epoll_wait的定时时间。
这种设计下需要使用容器保存所有的定时时间,epoll每次配置最快超时的时间为epoll_wait的定时时间。
需要选择合适的超时时间保存的容器,保证可以用O(1)获取到最小的那个超时时间。
② 结合Timerfd API
Linux内核2.6.25版本中添加的接口,把超时事件触发为文件描述符,超时发生后,文件描述符可读。
这种机制下超时事件成为了普通的IO事件,也可以设置阻塞/非阻塞,timerfd的精度达到了纳秒级。
Note:libevent2.1中也支持了timerfd,特点是:高效,精确。
每一个超时事件都用timerfd_create()创建对应的fd,放到epoll中统一管理。
每增加一个定时事件,需要增加3个系统调用。文件描述符是稀缺资源。定时器过多会浪费。
libevent使用的方法:每个event loop共享一个timerfd,每次循环事件之前,取出最近一个超时事件的时间,将timerfd设置为这个超时时间。
4. 超时时间的容器
- 最小堆实现
libevent 使用的这种模式。
- 时间轮实现
libco 使用的这种模式。
- 最小堆和时间轮的时间复杂度
Type add exec
list O(1) O(n)
min-heap O(lgn) O(1)
time wheel O(1) O(n)