寫下這個給自己備忘,關於事件循環以及多線程方面的東西我還需要多多學習。首先我們都知道程序有一個主線程,在GUI程序中這個主線程也叫GUI線程,圖形和繪圖相關的函數都是由主線程來提供。主線程有個事件循環Event Loop,其實就是一個死循環在不斷的等待你的消息隊列,通過消息隊列完成響應用戶操作,繪圖,以及相關操作。我們都知道QDialog有一個exec函數,這個函數會形成“模態”對話框,然后等待用戶去輸入OK還是Cancel,否則他絕不返回,如下
void test() { QDialog dialog; dialog.exec(); qDebug() << "finish exec"; }
我們可以看這個簡單的例子,可以看到,當dialog被exec之后,我們的qDebug是不會輸出的,除非我們人為去點了對話框的OK,否則,就會一直卡在exec之上。這個時候可能同學會有我一開始一樣的誤解,我們會誤以為此時事件循環停止了,其實並不是停止,而是阻塞住了。為什么會阻塞?因為test這個函數沒有返回嘛!
m_stateManager->postEvent(ev);
投遞事件官方API也說的很清楚,會立即返回,所以別去擔心此時投遞的事件進入不到消息隊列,真正要關心的是此時dialog的exec讓你的主線程阻塞了,這個時候消息隊列上的事件都不會進行操作,都在等待dialog的返回,只有dialog返回,接下來的事件才會依次進行。要記住,消息是可以正常投遞的。
那么,有沒有辦法可以讓dialog.exec()立即返回,同時我的對話框還在呢?方法是有的
void test() { metaObject()->invokeMethod(this, "invokeTest", Qt::QueuedConnection); qDebug() << "finish exec"; } Q_INVOKABLE void invokeTest() { QDialog dialog; dialog.exec(); }
答案就是用Qt提供的元對象系統Meta Object System的invokeMethod,並且將第三個參數設為Qt:QueuedConnection。從字面上我們也可以看的出來,這個調用不會去立即調用,相反他是異步的,他會把這個函數投遞到事件隊列里,也就是說,這個例子中的qDebug會輸出,輸出之后,事件隊列才會去調用dialog.exec()這個函數,當然了,調用這個函數之后又會達到我們一開始的那個阻塞的效果,你通過異步到最后的觸發,始終你需要去面對exec給你當前事件循環造成阻塞的問題。
讓我們再深入一點,當一個事件循環的時候還算簡單。但是我們知道,Qt中對於狀態機他是有一個異步的事件循環的,也就是說外面有事件循環,狀態機本身也有事件循環。比如
m_stateManager->postEvent(ev);
m_tool->run();
你給狀態機投遞了一個事件,他根據狀態遷移去調用你的tool,這一切看起來很美好,但如果你此時的tool是個跟之前一樣的阻塞的exec呢?讓我們來看一下。
void Tool::run() { QDialog dialog; dialog.exec(); }
對於這個情況,當我們運行tool之后,我們的狀態機就跟之前的主事件事件循環一樣被阻塞了,也就是說如果我此時繼續
m_stateManager->postEvent(ev2);
和之前一樣,這個postEvent會立即返回,因為投遞到事件隊列都是立即返回的。但是關鍵的問題在於你的狀態機整個事件循環都停止不動了,都在等你之前的tool運行結束,但因為你之前的tool是個dialog.exec()你必須手動去點OK,不然你的狀態機事件循環就阻塞不動了,這個時候如果你的客戶不斷的去點你這個tool的event,那會產生噩夢般的效果----你點完OK之后又會來OK之后又會來OK。。。這其實就是你一旦點了OK,你的消息隊列就又可以循環了,之前等待的ev就都會去執行了。而且要注意的就是,此時你的exec的執行在主線程上,只是不能進行返回,但還是可以接收諸如鍵盤,鼠標等事件投遞。
前面也說了,事件循環和狀態機循環是兩個獨立的循環,其實這也很好理解。如果沒有事件循環,狀態機事件怎么知道你有沒有按下這個鍵?從而去投遞給狀態機呢?其實也就是說當你狀態機事件阻塞的時候,你的主事件循環還在不斷的接收你的鍵盤和鼠標的操作,這一點是沒有影響的。
因此,要想實現在tool的時候我還能相應別的狀態事件,其實做法也是一樣的,就是
void Tool::run() { metaObject()->invokeMethod(this, "invokeTest", Qt::QueuedConnection); } Q_INVOKABLE void invokeTest() { QDialog dialog; dialog.exec(); }
立即返回,這個”立即返回“並不是說你的事情做完了,而是你更想讓狀態機能夠進行之后事件的循環,別去因為你的dialog而耽誤了大家。
最后說說模態這個主題,其實模態的理解就是你的消息隊列都在正常進行,因為你不斷的在等待,導致事件循環不能進行下去,必須你這邊正常返回,你接下來的操作才能繼續。
今天又重新思考了一下這個問題。同步的意思似乎就是必須要執行完成才能返回。異步的概念就是立即返回,之后執行,會把他扔到消息隊列里,待同步函數處理完之后,然后去搜索事件隊列進行操作。其實狀態機歸根結底就是一堆信號鏈接,只是他的方向是規矩狀態遷移表來進行。作為主線程的Event Loop來說,當dialog進入exec的時候,就是就是在進行事件隊列,並不是說他此時把事件隊列給阻塞了,這個我之前理解有問題。exec的含義就是去處理事件隊列,去處理事件循環,去檢索當前還有哪些事件可以被處理,從而去正常處理。比如我們有一個主程序窗口MainWindow,有一個Dialog,此時我們去調用dialog的exec,內部會去創建一個QEventLoop,又因為這個dialog的所在線程和MainWindow在同一個線程上,所以看上去似乎是兩個EventLoop,但實際上都是同一個線程的Event Loop(一個線程只能有一個Event Loop,這是原子性問題)。所以在dialog進行exec的時候他會去檢索主線程上的事件隊列去操作。
而我們之前講的狀態機,其實仔細想了想很簡單,你就把他理解成是主程序總的Event Loop中的一個事件,他在進行操作的時候,不返回(tool去調用dialog的exec看上去似乎進行了事件循環在等待你新的event,但別忘了,你本身這個tool的run就是通過事件隊列去觸發的)所以必須要等待這個tool的exec返回,你的事件隊列才能正常下去。
再次強調:
- exec並不是說事件隊列被你阻塞,而是才是讓你進入一個真正等待處理事件隊列的過程。
- 同一個線程只能有一個Event Loop,這個可以參考CP單核心單線程的處理邏輯。
- 在進行事件隊列進行事件操作的時候,其實內部就是同步的方式在進行,必須等待函數全部執行完畢才能真正返回才能真正進行之后的event,這也可以說的通我們之前舉的狀態機的例子,看上去這個狀態機引發了我們的tool,tool中調用了dialog的exec,看上去似乎很美好,在等待狀態事件了。但此時你這個exec不返回,你如何讓事件Event Loop繼續進行下去。
- 同步函數就是必須要等待函數操作完成之后才能返回的函數。異步函數就是直接返回,他具體什么時候進行操作,待具體實現查看。(也可能是本線程的事件隊列,也可能是別的線程進行run)。如果是別的線程進行run的時候,你可能會去想這個立即返回的問題,其實很簡單,Qt的run都是start,其實就跟postEvent一樣,只是簡單的把他注冊給線程管理器,由線程管理器再去跑他的run函數,那你本地的start當然立即返回了。