基於發佈/訂閱模型的應用程序的主循環設計
Table of Contents
1 什麼是應用程序主循環?
咱們在執行一個應用程序時,不少時候該應用程序不會一閃而過,而是會打開一個可與用戶交互的UI,持續處理用戶輸入。咱們寫的應用程序代碼老是有限的,而咱們寫代碼時用到的三種編程結構(順序、條件、循環)只有循環結構纔會永不停歇的執行下去。這意味着,咱們一般遇到的應用程序中一定有一段循環代碼來保證程序永遠執行下去。這就是應用程序主循環。git
2 幀循環
應用程序在每一次循環中,執行的都是相同的代碼,咱們能夠把每一次循環稱做一個邏輯幀,主循環也可稱爲幀循環。github
程序每秒的邏輯幀數稱做邏輯幀率,對於3D遊戲或一些圖形軟件而言,邏輯幀率就是它們的顯示幀率,通常控制在60或30。咱們這裏提到了邏輯幀率,假如邏輯幀的平均執行時長爲 t(s),那麼,邏輯幀率=1/t。邏輯幀的執行時長是指從進入本次循環到執行完本次循環的 CPU 時鐘。能夠想象,若是不在每幀中加入休眠指令,那麼,對大多數程序而言,邏輯幀的執行時長几乎是能夠忽略的。這個時候討論幀率沒有任何意義,並且無論應用程序的邏輯有多簡單,不在主循環中加休眠指令的應用程序其 CPU 的佔用率都會超高。編程
3 每次循環都作些什麼?
這個問題的答案不是普適的。但據我觀察,絕大部分應用程序的設計都有發佈/訂閱模型的影子。在發佈/訂閱模型中,參與者包括髮布者和訂閱者。這就相似於咱們看新聞,咱們訂閱的感興趣的條目會自動地發佈給咱們。基於發佈/訂閱模型設計的應用程序其主循環能夠充當發佈者的角色。服務器
4 消息驅動
咱們前面談到主循環能夠充當發佈者的角色,那麼,對應用程序而言,發佈和訂閱的具體對象是什麼呢?結構化的數據,有時也稱爲事件或消息,咱們在這裏統一稱爲消息。通過大量的工程實踐,人們發現對於服務器應用而言,須要用到的消息只有兩類:*定時器消息*和*網絡消息*;對於有用戶交互的應用而言,須要用到的消息除了上述兩類外還有鍵盤、鼠標以及其餘設備的輸入消息。據此分析,咱們可將應用程序消息概括爲如下三類:網絡
- 定時器消息
- 網絡消息
- 設備輸入消息
咱們前面談到主循環能夠充當發佈者的角色,其實這是不嚴謹的。對於定時器消息,咱們能夠認爲是主循環根據當前時間向訂閱者發佈的。但,網絡消息呢?主循環明顯不能產生網絡消息,網絡消息只能由以太網接口從物理鏈路收取,而後再交付給應用程序。針對網絡消息,主循環充當的角色頂多算是二次發佈者,它不是消息來源。同理,主循環也不是設備輸入消息的消息來源,它只負責分發它們。將主循環理解爲消息分發器或許更爲合適。對於大部分應用程序而言,咱們老是能夠將其主循環設計爲:less
while (1) { dispatchTimerMessages(); // 分發定時器消息 dispatchNetworkMessages(); // 分發網絡消息 dispatchInputMessages(); // 分發設備輸入消息 }
5 Win32 程序的消息循環
不單單是 Win32 程序,基於 Qt、Gtk 等界面庫的應用程序都有相似消息循環的概念。函數
MSG msg; ZeroMemory(&msg, sizeof(MSG)); while(TRUE) { while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if(msg.message == WM_QUIT) return; TranslateMessage(&msg); DispatchMessage(&msg); } UINT dwResult = MsgWaitForMultipleObjects(1, &m_hTickEvent, FALSE, INFINITE, QS_ALLINPUT); if(dwResult == WAIT_OBJECT_0) { ... // 其餘邏輯 } else { continue; } }
上面展現了 Win32 程序的消息循環代碼。 PeekMessage 從消息隊列中取出一個消息, TranslateMessage 填入消息參數, *DispatchMessage 分發消息*。咱們提到了消息隊列的概念,編寫過 Win32 程序的人應該知道,這個消息隊列並無出如今咱們的應用程序中。實際上這個消息隊列是系統幫咱們維護的,當設備驅動程序捕獲到設備輸入時,系統會將該輸入事件投入到相關進程的消息隊列中, PeekMessage 能從該消息隊列取出消息。post
DispatchMessage 分發消息必然是分發給訂閱者了,那麼在 Win32 程序中怎麼訂閱各種設備消息呢?ui
// 註冊窗口類 WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wcex.lpfnWndProc = (WNDPROC)_MainWndProc; // 消息訂閱者 wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = g_hInstance; wcex.hIcon = LoadIcon(g_hInstance, (LPCTSTR)IDD_GAME_DIALOG); wcex.hCursor = LoadCursor( NULL, IDC_ARROW ); wcex.hbrBackground = (HBRUSH)NULL; //GetStockObject(WHITE_BRUSH); wcex.lpszMenuName = (LPCTSTR)NULL; wcex.lpszClassName = MAINWINDOW_CLASS; wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL); RegisterClassEx(&wcex);
如上所示,在註冊窗口類時,咱們訂閱了設備消息處理函數。 MainWndProc 函數體形如:
LRESULT CALLBACK _MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch(message) { case WM_MOUSEWHEEL: // todo break; case WM_KEYDOWN: // todo break; default: return DefWindowProc(hWnd, message, wParam, lParam); } }
綜上所述, Win32 應用程序的主循環就是一個不斷的取消息、分發消息的過程。
6 服務器程序執行流程示例
void EventDispatcher::processUntilBreak() { if(mBreakProcessing != DispatcherStatus_BreakProcessing) mBreakProcessing = DispatcherStatus_Running; // 主循環 while(mBreakProcessing != DispatcherStatus_BreakProcessing) { // 執行一幀 this->processOnce(true); } } int EventDispatcher::processOnce(bool shouldIdle) { if(mBreakProcessing != DispatcherStatus_BreakProcessing) mBreakProcessing = DispatcherStatus_Running; // 分發用戶自定義任務消息 this->processTasks(); // 分發定時器消息 if(mBreakProcessing != DispatcherStatus_BreakProcessing) { this->processTimers(); } this->processStats(); // 分發網絡消息 if(mBreakProcessing != DispatcherStatus_BreakProcessing) { return this->processNetwork(shouldIdle); } return 0; } void EventDispatcher::processTasks() { mpTasks->process(); } void EventDispatcher::processTimers() { uint64 now = timestamp(); mNumTimerCalls += mpTimers->process(now); } void EventDispatcher::processStats() { if (timestamp() - mLastStatisticsGathered >= stampsPerSecond()) { mOldSpareTime = mTotSpareTime; mTotSpareTime = mAccSpareTime + mpPoller->spareTime(); mLastStatisticsGathered = timestamp(); } } int EventDispatcher::processNetwork(bool shouldIdle) { double maxWait = shouldIdle ? this->calculateWait() : 0.0; return mpPoller->processPendingEvents(maxWait); }
在上面的程序中,咱們在主函數中調用 processUntilBreak() 函數便可進入主循環。完整的程序可經過 https://github.com/ruleless/snail 得到。
正如咱們前面所說,在此示例中,主循環分發了定時器和網絡消息。
能夠認爲,服務器是由網絡消息驅動的,服務器的主要業務就是接入客戶端和處理客戶請求。