一.system_clock和steady_clock比较
下面一段摘录
system_clock:就类似Windows系统右下角那个时钟,是系统时间。明显那个时钟是可以乱设置的。明明是早上10点,却可以设置成下午3点。
steady_clock:则针对system_clock可以随意设置这个缺陷而提出来的,他表示时钟是不能设置的。
steady_clock的实现是使用monotonic时间,而monotonic时间一般是从boot启动后开始计数的。明显这不能获取日历时间(年月日时分秒)。
那么steady_clock有什么用途呢?时间比较!并且是不受用户调整系统时钟影响的时间比较。简单的例子如下:
auto begin = std::chrono::steady_clock::now();
for(int i = 0; i < 10000000; ++i)
{
// 计算...
}
auto end = std::chrono::steady_clock::now();
auto diff = (end - begin).count(); //end-begin得到一个duration类型
std::cout<<diff<<std::endl;
二.定时管理器heap
phx使用了heap(小根堆)来管理超时,一般来说,heap能做的操作是top(), push(), pop()三个操作。但是这里的一些设计要求可以在任何合法的位置删除节点,所以仅仅有pop()是不够的。
看下面一段代码:
int UThreadPoll(UThreadSocket_t & socket, int events, int * revents, int timeout_ms) {
int ret = -1;
// 获得当前正在执行的协程,需要这个协程帮助做点事
socket.uthread_id = socket.scheduler->GetCurrUThread();
// 增加超时事件
socket.event.events = events;
socket.scheduler->AddTimer(&socket, timeout_ms);
// 事件加入epoll
// 理论上讲,AddTimer将这个超时事件加入额外管理的heap定时器管理器就OK了
// 此处为什么还需要加入epoll中进行调度?
// 原因在于,在超时事件还没到达的时候,可能就有事件触发了,使得下面这一轮提前结束!
epoll_ctl(socket.epoll_fd, EPOLL_CTL_ADD, socket.socket, &socket.event);
// 将当前的协程停止,转让CPU给主协程
// 当主协程下次收到这个超时事件的时候会将执行权还给这个协程
// 协程下次还是从当前位置开始执行
socket.scheduler->YieldTask();
// 当超时任务完成,协程继续从此处执行!
// 完成后从epoll中删除定时器
epoll_ctl(socket.epoll_fd, EPOLL_CTL_DEL, socket.socket, &socket.event);
socket.scheduler->RemoveTimer(socket.timer_id);
...
}
上面这段代码将socket加入超时事件中,同时epoll也会去监控这个socket。那么这就可能存在两种情况:
第一:如果谁正常超时事件达到,协程回到当前代码,那么下面的RemoveTimer就相当于是pop()操作,这个是很常规逻辑。
第二:如果在超时之前,epoll就监控到存在事件触发了,那么在超时之前就重新回到这个协程。这个时候节点并没有超时,所以节点可能不是小根堆的根节点。那么删除的是中间的某个节点。这时候之前的pop就不满足要求了,因为pop只能讲根节点弹出。
这里的根本问题不在删除哪个节点,而是我们怎么定位到那个节点,难道要扫描一遍?那样代价有点大。
phx在此处有一个很trick的做法,它将每个socket关联的定时器的index(在vector数组中的下标)保存在socket结构中!!!这样就可以做到O(1)定位。这个有点。。。只能说“6”。
看下具体的UThreadSocket结构:
typedef struct tagUThreadSocket {
UThreadEpollScheduler * scheduler;
int uthread_id;
int epoll_fd;
int socket;
int connect_timeout_ms;
int socket_timeout_ms;
int waited_events;
size_t timer_id; // 保存这个socket的定时器在heap的数组中的位置(如果存在),方便在堆中的查找
struct epoll_event event;
void * args;
} UThreadSocket_t;
这个结构虽然很方便,但是总是觉得怪怪的。。。
OK,找到相应的节点后,那么删除操作就比较简单了。
看下具体的代码:
// 移除一个计时器
void Timer :: RemoveTimer(const size_t timer_id) {
if (timer_id == 0) {
return;
}
size_t now_idx = timer_id - 1;
if (now_idx >= timer_heap_.size()) {
return;
}
TimerObj obj = timer_heap_[now_idx];
UThreadSocketSetTimerID(*obj.socket_, 0);
// 当前与最后一个进行交换
// 然后删除最后一个元素
// 最后需要调整堆(up or down)
std::swap(timer_heap_[timer_heap_.size() - 1], timer_heap_[now_idx]);
timer_heap_.pop_back();
if (timer_heap_.empty()) {
return;
}
// 下面执行up 或者 down逻辑
// 这里需要将移除的节点和最后的节点比较大小
// 从而里判断是需要heap_up还是heap_down
if (timer_heap_[now_idx] < obj) {
heap_up(now_idx + 1);
} else if (timer_heap_[now_idx] == obj) {
UThreadSocketSetTimerID(*timer_heap_[now_idx].socket_, now_idx + 1);
} else {
heap_down(now_idx);
}
}
在heap_up和heap_down函数中,有一行类似如下:
UThreadSocketSetTimerID(*timer_heap_[now_idx].socket_, now_idx + 1);
这个就是每次调整完节点需要在socket结构中重新设置关联的timer在vector中的位置,Orz…
其他的正常的堆的操作就没什么好说的了。