在 POSIX 執行緒程式設計中避免記憶體洩漏
POSIX 執行緒簡介
使用執行緒的主要原因是要提高程式效能。執行緒的建立和管理只需要較小的作業系統開銷和較少的系統資源。一個程序內的所有執行緒共享相同的地址空間,使得執行緒間的通訊更高效,且比程序間通訊更易於實現。例如,如果一個執行緒在等待一個輸入/輸出系統呼叫完成,其他執行緒可以處理 CPU 密集型任務。通過執行緒,可以優先排程重要任務 — 甚至中斷 — 低優先順序任務。可將偶爾發生的任務放在定期排程的任務之間,建立排程靈活性。最後,pthreads 是在多 CPU 計算機上進行並行程式設計的理想之選。
而且使用 POSIX 執行緒或 pthreads 的主要原因更加簡單:作為標準 C 語言執行緒程式設計介面的一部分,它們可高可移植的。
POSIX 執行緒程式設計有諸多優勢,但是如果您不明確一些基本規則,就有可能編寫一些難以除錯的程式碼並造成記憶體洩漏。我們首先回顧一下 POSIX 執行緒,分為可接合執行緒 或分離執行緒。
可接合執行緒
如果您希望生成一個新的執行緒,且需要知道它是如何終止的,那麼您需要一個可接合執行緒。對於可接合執行緒,系統分配專用儲存器來儲存執行緒終止狀態。執行緒終止後狀態得到更新。要獲得執行緒終止狀態,呼叫
pthread_join(pthread_t thread, void** value_ptr)
。
系統為每個執行緒分配底層儲存,包括堆疊、執行緒 ID、執行緒終止狀態等。這個底層儲存將一直保留在程序空間(且不能回收),直至執行緒終止併為其他執行緒所聯接。
分離執行緒
大多數時候,您只需建立一個執行緒,向其分配一些任務,然後繼續處理其他事務。在這些情況下,您不關注執行緒是如何終止的,這時使用分離執行緒是一個很好的選擇。
對於分離執行緒,線上程終止後系統自動回收其底層資源。
識別洩漏
如果您建立一個可接合的執行緒,但是忘記聯接它,其資源或私有記憶體一直儲存在程序空間中,從未進行回收再利用。一定要聯接可接合的執行緒;否則,可能會引起嚴重的記憶體洩漏問題。
例如,Red Hat Enterprise Linux (RHEL4)上的一個執行緒需要一個 10MB 的堆疊,這意味著,如果不聯接它,會有至少 10MB 的記憶體洩漏。假設您設計一個管理器-工作執行緒模式的程式來處理傳入的請求。然後需要建立越來越多的工作執行緒來執行各個任務,最後終止這些執行緒。如果它們是可接合的執行緒,且您沒有呼叫
pthread_join()
清單 1 顯示在忘記聯接可接合執行緒時引發的嚴重記憶體洩漏。您還可以使用該程式碼檢查可在一個程序空間中共存的執行緒體的最大量。
清單 1. 引發記憶體洩漏
#include<stdio.h> #include<pthread.h> void run() { pthread_exit(0); } int main () { pthread_t thread; int rc; long count = 0; while(1) { if(rc = pthread_create(&thread, 0, run, 0) ) { printf("ERROR, rc is %d, so far %ld threads created\n", rc, count); perror("Fail:"); return -1; } count++; } return 0; }
清單 1 中呼叫了 pthread_create()
來建立一個含預設執行緒屬性的新執行緒。預設情況下,新建立的執行緒是可接合的。它不斷建立新的可接合執行緒,直至有故障發生。然後輸出錯誤程式碼和故障原因。
使用以下命令在 Red Hat Enterprise Linux Server 5.4 上編譯清單 1 中的程式碼時: [[email protected] ~]# cc -lpthread thread.c -o thread
, 您將獲得清單 2 所示的結果。
清單 2. 記憶體洩漏結果
[[email protected] ~]# ./thread ERROR, rc is 12, so far 304 threads created Fail:: Cannot allocate memory
在程式碼建立了 304 個執行緒之後,它無法建立更多執行緒。錯誤程式碼是 12
,這表示無更多記憶體可用。
如清單 1 和清單 2 所示,雖然生成了可接合執行緒,但是卻未將其聯接,因此每個終止的可接合執行緒仍然佔用程序空間,洩漏程序記憶體。
RHEL 上的一個 POSIX 執行緒擁有一個大小為 10MB 的私有堆疊。換言之,系統為每個 pthread 分配至少 10MB 的專用儲存。在我們的示例中,304 個執行緒是在程序停止前建立的;這些執行緒佔用 304*10MB 記憶體,合計約 3GB。一個程序的虛擬記憶體的大小是 4GB,其中四分之一的程序空間是為 Linux 核心預留的。這樣一來,就有 3GB 的記憶體空間可用作使用者空間。因此,3GB 記憶體由死執行緒消耗。這是很嚴重的記憶體洩漏。而且很容易理解它發生的速度為何如此之快。
要修復洩漏,您可以新增程式碼呼叫 pthread_join()
,該方法可聯接每個可接合執行緒。
檢測洩漏
如同其他記憶體洩漏中一樣,程序啟動時問題可能沒那麼明顯。這裡介紹一種無需訪問原始碼便可檢測此類問題的方法:
- 計算程序中執行緒堆疊的數量。這包括正在執行的活動執行緒和已終止執行緒的數量。
- 計算程序中正在執行的活動執行緒的數量。
- 比較兩者。如果現有執行緒堆疊的數量大於正在執行的活動執行緒的數量,且在程式執行時這兩個數字的差量在不斷增加,那麼記憶體在洩漏。
這種記憶體洩漏很有可能是因未能聯接可接合執行緒而造成的。
使用 pmap 計算執行緒堆疊數
在一個執行的程序中,執行緒堆疊的數量等於程序中執行緒體的數量。執行緒體包括執行的活動執行緒和可接合的死執行緒。
pmap
是一種用於彙報程序記憶體的 Linux 工具。結合使用以下命令來獲取執行緒堆疊數:
[[email protected] ~]# pmap PID | grep 10240 | wc -l
(10240KB 是 Red Hat Enterprise Linux Server 5.4 上的預設堆疊大小。)
使用 /proc/PID/task 計算活動執行緒數
每次建立一個執行緒且該執行緒在執行時,會有一個條目填充到 /proc/PID/task 中。當執行緒終止時,不管該執行緒是可接合的還是分離的,都會將該條目從 /proc/PID/task 中刪除。因此活動執行緒數可通過執行以下命令得出:
[[email protected] ~]# ls /proc/PID/task | wc -l
.
比較輸出
檢查 pmap PID | grep 10240 | wc -l
的輸出並將其與 ls /proc/PID/task | wc -l
的輸出進行比較。如果所有執行緒堆疊的數量大於活動執行緒的數量,且在程式執行時兩者的差量在持續增長,您可以確定記憶體洩漏問題確實存在。
預防洩漏
在程式設計過程中應當聯接可接合執行緒。如果您在程式中建立可接合的執行緒,切勿忘記呼叫 pthread_join(pthread_t, void**)
來回收分配給執行緒的專用儲存。否則,將引發嚴重的記憶體洩漏問題。
在程式設計後的測試階段,您可以使用 pmap
和 /proc/PID/task
檢測這種洩漏是否存在。如果確實存在,檢查原始碼,看是否聯接了所有可接合執行緒。
就這些內容。只需少量預防工作即可為您省掉大量後續工作,避免令人頭疼的記憶體洩漏問題。