Libuv 各个回调(异步)事件的调用时机
uv_close、uv_timer_start
uv_close中注册的回调事件(close_cb)查阅官网API文档,Handle句柄是调用uv_close便会立即关闭,而注册的回调事件将推迟到下一次Loop循环中执行。
下面有一个应用场景,与uv_close的调用时机有关,服务端有一个心跳检测机制,判断连接上来每个会话(Session)是否具有活性。如下:
bool TcpSessionMgr::ClientHeartCheck() {TcpSession* pTcpSession = nullptr;NET_UV_LOG(NET_UV_LOG_TYPE::NET_UV_LOG_INFO,"ClientHeartCheck...\n"); //日志for (auto iter = m_sessionMaps.begin();iter != m_sessionMaps.end(); iter++) {pTcpSession = iter->second;if (!pTcpSession->GetActive()) {NET_UV_LOG(NET_UV_LOG_TYPE::NET_UV_LOG_INFO,"client stop heartbeat, sessionID:%d\n",pTcpSession->GetSessionID());pTcpSession->DisConnect(); //m_sessionMaps不是立即删除当前没有活性的session,而是在DisConnect中的异步回调事件(OnSessionClose)中删除。方法定义均在下方。}pTcpSession->SetActive(false);}return true;
}bool TcpSession::DisConnect() {ShutDownHandle((uv_handle_t*)&m_uvTcp, OnSessionClose);return true;
}bool ShutDownHandle(uv_handle_t* handle, uv_close_cb closeCB) {bool result = false;if (0 != uv_is_closing(handle)) {NET_UV_LOG(NET_UV_LOG_TYPE::NET_UV_LOG_ERROR, "handle already close or closing...\n");goto Exit;}uv_close(handle, closeCB);result = true;
Exit:return result;
}void OnSessionClose(uv_handle_t* handle) {TcpSession* pTcpSession = (TcpSession*)handle->data;assert(NULL != pTcpSession);NET_UV_LOG(NET_UV_LOG_TYPE::NET_UV_LOG_INFO, "client closed : %llu\n",(uint64_t)handle);pTcpSession->SetActive(false);//通知逻辑层关闭连接pTcpSession->NotifyRemoveSession(); //在这里通知m_sessionMaps进行删除。//释放pTcpSessionFree(pTcpSession);
}
其中,ClientHeartCheck是一个定时器事件,定时时间为3000。
m_timerID = m_service->RegisterTimer(std::bind(&TcpSessionMgr::ClientHeartCheck, &m_tcpSessionMgr), 3000);//注册定时器,定时时间为3000,即3s执行一次。
在这个过程中,出现问题即原因为:当在ClientHeartCheck方法中pTcpSession->DisConnect();处断点调试,经过3s后,ClientHeartCheck方法已经执行一次,但是OnSessionClose回调事件在第一次并未执行,而是在ClientHeartCheck方法执行第二次时,OnSessionClose回调事件才执行。原因文章开头已经说明,由于添加断点调试时间过长,而uv_close注册的回调事件只有loop第二次循环才执行,断点结束时,导致该定时器已经再次超时,而我们观察uv_run的源码会发现定时器的缓冲队列是在整个uv_run开头执行(uv__run_timers),而控制uv_close的回调事件(uv_process_endgames)是在定时器缓冲队列之后处理的,所以导致我们没来得及删除session,却已经遍历第二次m_sessionMaps容器。
int uv_run(uv_loop_t *loop, uv_run_mode mode) {DWORD timeout;int r;int ran_pending;r = uv__loop_alive(loop);if (!r)uv_update_time(loop);while (r != 0 && loop->stop_flag == 0) {uv_update_time(loop);//执行计时器超时事件uv__run_timers(loop); //这里处理定时器超时事件ran_pending = uv_process_reqs(loop);uv_idle_invoke(loop);uv_prepare_invoke(loop);timeout = 0;if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)timeout = uv_backend_timeout(loop);if (pGetQueuedCompletionStatusEx)uv__poll(loop, timeout);elseuv__poll_wine(loop, timeout);/* Run one final update on the provider_idle_time in case uv__poll** returned because the timeout expired, but no events were received. This* call will be ignored if the provider_entry_time was either never set (if* the timeout == 0) or was already updated b/c an event was received.*/uv__metrics_update_idle_time(loop);uv_check_invoke(loop);uv_process_endgames(loop); //这里处理uv_close的回调事件if (mode == UV_RUN_ONCE) {/* UV_RUN_ONCE implies forward progress: at least one callback must have* been invoked when it returns. uv__io_poll() can return without doing* I/O (meaning: no callbacks) when its timeout expires - which means we* have pending timers that satisfy the forward progress constraint.** UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from* the check.*/uv__run_timers(loop);}r = uv__loop_alive(loop);if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)break;}/* The if statement lets the compiler compile it to a conditional store.* Avoids dirtying a cache line.*/if (loop->stop_flag != 0)loop->stop_flag = 0;return r;
}
在下上述结论之前,博主尝试过将ClientHeartCheck的定时器间隔时间调整至1000(整个Loop循环执行一次时间为1000以上,即1s以上,博主在idle里面Sleep了一秒),那么按照原理,即使没有添加断点调试,m_sessionMaps仍然会遍历两次(即ClientHeartCheck执行两次),经过博主的实操验证,确实如此。
在这个过程中,博主有想过是否uv_close调用后,未执行下次定时事件之前就会调用uv_close的回调事件,但实际上并不会,uv_close的回调事件libuv规定是下次Loop循环时调用。
博主还考虑过是否uv_run中uv__run_timers方法会执行死循环,即执行多次(由于我们添加断点调试,那么定时器一直在超时,如果是死循环,那么会一直调用),但是后来测试(尝试第二次执行ClientHeartCheck后继续加断点,一段时间后运行),发现只会调用两次,不会有第三次,这也印证了uv_close的回调事件在Loop循环的下一次是会执行的。同时博主也通过查看源代码,发现同一个定时器事件,在一次uv__run_timers是不可能执行两次的,原因如下:
void uv__run_timers(uv_loop_t* loop) {struct heap_node* heap_node;uv_timer_t* handle;for (;;) { //死循环//小顶堆结构,获取超时的定时器heap_node = heap_min(timer_heap(loop));if (heap_node == NULL)break;handle = container_of(heap_node, uv_timer_t, heap_node);//如果当前节点的时间(由于是小顶堆,已经是时间历时最久的节点)大于loop当前时间则返回if (handle->timeout > loop->time)break;//移除该定时器节点,如果设置repeat则会重新插入到小顶堆中,博主的封装的定时器默认设置repeatuv_timer_stop(handle);uv_timer_again(handle);//执行超时回调事件handle->timer_cb(handle);}
}
一开始总是恍惚的,既然重新添加到小顶堆中,而且是死循环,再加上我们添加断点调试(人为超时),那么不是会一直超时,一直执行,不也就可以说明为什么ClientHeartCheck调用第二次了吗?但是忽略了一个非常关键的点,那就是loop->time在当前整个uv__run_timers方法中是从而更新过的,那么真的会像Windows系统时间那样一直往前吗?这显然是不会的。
经过上述问题,可以发现,在使用Libuv开发的过程中,我们不要认为注册回调事件就一定按照心中所想调用,应当搞清楚,我们注册进去的回调事件具体是何时调用的,才是最重要的。
下面顺带将常用的回调(异步)事件的调用时机进行梳理一遍。
uv_idle_start
该事件每次Loop循环都会执行一次其回调事件。注意,它的回调执行时机比Prepare Handles要早。
uv_prepare_start
该事件每次Loop循环都会执行一次其回调事件。注意,它的回调执行时机比Idles Handles要晚,但是在IO轮询之前执行。
uv_check_start
该事件每次Loop循环都会执行一次其回调事件。注意,它的回调执行时机在IO轮询之后执行。
正常情况下,所有的I/O回调都在轮询I/O后立刻被调用。但是有些情况下,回调事件可能被推迟至下一次循环迭代中再执行。任何上一次循环中被推迟的回调事件,都会在Pending callbacks这个时候被执行。
close callbacks,如果一个句柄通过uv_close()关闭,那么都会在这个时候被调用(注意是下次迭代调用)。