官术网_书友最值得收藏!

1.1 C++RAII慣用法

什么是RAII?容筆者先賣個關子,給大家講個故事。

1.1.1 版本1:最初的寫法

在筆者剛學習服務器開發的時候,公司給筆者安排了一個練習:在 Windows 系統上寫一個 C++程序,用該程序實現一個簡單的服務,在客戶端連接上來時,給客戶端發一條“HelloWorld”消息后關閉連接,不用保證客戶端一定能收到。

如果熟悉基礎網絡編程知識,那么你會覺得這很容易,因為這個程序描述的就是TCP網絡通信的基本流程,其程序實現流程如下。

(1)創建socket。

(2)綁定IP地址和端口號。

(3)在該 IP 地址和端口號上啟動監聽,循環等待客戶端連接的到來,在客戶端連接成功后,向其發送一條“HelloWorld”消息,然后斷開連接。

在 Windows 上使用網絡通信 API 之前,需要使用 WSAStartup 函數初始化socket庫;在程序結束時需要使用WSACleanup函數清理socket庫。

筆者很快就將程序寫出來了:

以上代碼雖然滿足了公司的要求,但是有些地方不太令人滿意,因為代碼中充斥著用于避免出錯的重復資源清理邏輯:closesocket(sockSrv)和WSACleanup()。

這樣的場景在實際開發過程中經常存在,例如下面這段偽代碼:

我們可以將上述場景理解為“先分配資源,再進行相關操作,在任意中間步驟出錯時都對相應的資源進行回收,如果中間步驟沒有出錯,則在資源使用完畢后對其進行回收”。上述偽代碼片段中釋放資源的重要性不言而喻:因為分配了堆內存,所以不釋放會造成內存泄露。但是這樣編寫代碼太容易出錯了!我們必須時刻保持警惕,在任意出錯的步驟中都要記得加上回收資源的代碼。這樣的編碼方式不僅容易出錯,還會導致大量的代碼重復。那有沒有辦法解決這類問題呢?有,使用goto語句。

1.1.2 版本2:使用goto語句

還是以前面網絡通信的代碼為例,如果使用goto語句,則該代碼可以簡化如下:

使用 goto 語句后,一旦某個中間步驟出錯,則跳轉到統一的清理點進行資源清理操作。

但是,我們總被告知要慎用goto語句,因為它會讓程序的結構變得混亂和難以維護。姑且不論這是否正確,如果不用 goto 語句,那么有沒有更好的實現方式呢?有,使用do...while(0)循環。

1.1.3 版本3:使用do...while(0)循環

以上代碼使用do...while(0)循環改進后如下:

以上代碼利用 do...while(0)循環中的 break 特性巧妙地將資源回收操作集中到一個地方,使用 for 循環也能達到同樣的效果。我們同樣可以使用 do...while(0)改造上面堆內存分配與釋放的示例,偽代碼如下:

這是do...while(0)的一個妙用。但是,在C++中有更好的寫法來代替do...while(0),即RAII慣用法。

1.1.4 版本4:使用RAII慣用法

RAII(Resource Acquisition Is Initialization,資源獲取就是初始化)指資源在我們拿到時就已經初始化,一旦不再需要該資源,就可以自動釋放該資源。

對于 C++來說,資源在構造函數中初始化(可以在構造函數中調用單獨的初始化函數),在析構函數中釋放或清理。常見的情形就是在函數調用中創建C++對象時分配資源,在 C++對象出了作用域時將其自動清理和釋放(不管這個對象是如何出作用域的,不管是否因為某個中間步驟不滿足條件而導致提前返回,也不管是否正常走完全部流程后返回)。

還是以上面網絡通信的例子來說,初始化程序時需要分配兩種資源:Windows 的socket網絡庫和一個用于監聽的socket。首先,初始化好Windows socket網絡庫;然后創建一個用于監聽的socket。在程序結束時,我們需要清理這兩種資源。

使用RAII慣用法改進后的代碼如下:

以上代碼并沒有在構造函數中分配資源,而是單獨使用一個DoInit方法初始化資源,并在析構函數中回收相應的資源。這樣在main函數中就不用擔心任何中間步驟失敗而忘記釋放資源了,因為一旦main函數調用結束,serverSocket對象就會自動調用其析構函數回收相應的資源。這就是RAII慣用法的原理!

嚴格來說,以上代碼中ServerSocket的成員變量m_bInit應該被設計成類靜態成員,調用 WSAStartup 和 WSACleanup 的函數應該被設計成類的靜態方法,因為它們只需在程序初始化和退出時各調用一次就可以了。

希望讀者能理解 RAII 慣用法,因為它在 C++中太常用了。我們也可以使用 RAII 慣用法再次改寫上文中分配堆內存的偽代碼示例:

其中,heapObj對象一旦出了其作用域,該程序就會自動調用其析構函數釋放堆內存。當然,RAII 慣用法中對資源分配和釋放的定義可以延伸出各種外延和內涵,例如對多線程鎖的獲取和釋放。我們在實際開發中也常常遇到以下情形:

這是一段很常見的邏輯:為了避免死鎖,我們必須在每個可能退出的分支上都釋放鎖。隨著邏輯寫得越來越復雜,我們忘記在某個退出的分支上釋放鎖的可能性也越來越大。而RAII慣用法正好解決了這個問題:我們可以將鎖包裹成一個對象,在構造函數中獲取鎖,在析構函數中釋放鎖。偽代碼如下:

使用 RAII 慣用法之后,我們就再也不必在每個函數出口處都加上釋放鎖的代碼了,因為在函數調用結束后會自動釋放鎖。

對于以上代碼,有經驗的讀者可能一眼就看出來了:這不就是C++11中std::lock_guard和 boost 庫中 boost::mutex::scoped_lock 的實現原理嗎?確實是,本書后續章節會詳細介紹操作系統和C++11提供的各類鎖的用法。

1.1.5 小結

資源泄露和死鎖等問題具有非常強的隱蔽性,如果在生產環境中出現這些問題,則難以復現、排查和定位問題。理解并熟練使用RAII慣用法不僅能讓我們的代碼更加簡潔和模塊化,也能讓我們在開發階段避免一部分資源泄漏和死鎖問題。

主站蜘蛛池模板: 南澳县| 永清县| 平阳县| 黑山县| 许昌县| 柳州市| 汝阳县| 宁远县| 容城县| 句容市| 富平县| 称多县| 宾川县| 岳池县| 新乡市| 泰宁县| 阿鲁科尔沁旗| 理塘县| 宁德市| 翁牛特旗| 平凉市| 安义县| 岳普湖县| 昭平县| 广平县| 金坛市| 类乌齐县| 通城县| 威宁| 湖州市| 当雄县| 白山市| 惠东县| 天峨县| 垫江县| 普洱| 新乐市| 罗田县| 凤庆县| 嘉义县| 青海省|