微信后台 phxrpc (v0.8) 之 Timer(二)

一.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<::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…

其他的正常的堆的操作就没什么好说的了。

你可能感兴趣的:(RPC,C/Cpp)