文章目錄
- 引言
- 問題描述和分析
- 監控shell腳本
- shell腳本解釋
- 問題根源追溯
- 解決方案一:增大mmap區域
- 解決方案二:優化線程棧空間
- 解決方案三:引入線程池
- 參考文章
引言
在高并發和長周期運行的環境中,頻繁創建std::thread
線程可能導致mmap虛擬地址空間耗盡,進而引發資源不足的錯誤。
本文提出的增大mmap區域、優化線程棧空間以及引入線程池的策略,能夠有效地管理線程資源,提高應用的穩定性和效率。
問題描述和分析
為處理一些異步任務請求,我們頻繁創建std::thread
線程來執行任務。盡管初期運行順利,但隨時間推移,后面會遇到“Resource temporarily unavailable”的異常,直接影響了系統的響應時間和整體穩定性。
為精確診斷,我們設計了一套監控機制,實時捕捉并記錄所有線程狀態變化,同時對core dump進行深入解析,以識別故障線程。分析結果表明,問題源于std::thread
線程創建階段,具體表現為EAGAIN錯誤——指示系統資源暫時不可用。
監控shell腳本
iterate_threads_info() {local pids=$(pidof "$1")local output_file="$1.info"# Write header to output file{echo "Record start: $(date)"echo "--------------------------------------------------------------"} >> "$output_file"# Iterate over each process and its threadsfor pid in $pids; doecho "Process $pid" >> "$output_file"cat /proc/$pid/maps >> "$output_file"for tid in /proc/$pid/task/*; dotid=$(basename "$tid"){echo "--------------------------------------------------------------"echo "Thread: $tid"echo "--------------------------------------------------------------"echo "status:"cat /proc/$pid/task/$tid/statusecho "stack:"cat /proc/$pid/task/$tid/stackecho "syscall:"cat /proc/$pid/task/$tid/syscallecho ""} >> "$output_file"doneecho "" >> "$output_file"done
}
shell腳本解釋
-
參數
$1
: 這個參數是函數iterate_threads_info
的輸入參數,它表示要查詢的進程的名稱。在腳本執行時,你會把要查詢的進程的名稱作為腳本的第一個參數傳遞給這個函數。local pids=$(pidof "$1") local output_file="$1.info"
pidof "$1"
:使用pidof
命令獲取指定進程名稱(由$1
提供)對應的進程ID(PID)。這些PID將存儲在pids
變量中。"$1.info"
:構造一個輸出文件名,使用傳遞給函數的進程名稱$1
加上.info
后綴。這個文件名用于存儲進程及其線程的詳細信息。
-
輸出文件:
output_file
變量用來存儲輸出文件的名稱,在函數執行時會根據傳遞給函數的進程名稱動態生成。 -
循環遍歷進程和線程:
- 首先,對于每一個通過
pidof "$1"
獲取的進程ID,腳本會將進程ID打印到輸出文件中,并輸出該進程的maps
文件內容。 - 然后,使用
for tid in /proc/$pid/task/*
遍歷該進程的所有線程(/proc/$pid/task/
下的所有文件和目錄),其中$pid
是當前進程的PID。 - 對于每個線程,輸出它的
status
、stack
和syscall
文件內容到輸出文件中,以及相關的分隔符和空行用于格式化輸出。
- 首先,對于每一個通過
問題根源追溯
進一步探究線程創建流程,從C++標準庫std::thread
出發,經由POSIX線程API pthread_create
,直至內核層面的clone
及do_fork
函數。核心發現:內核在嘗試分配新線程所需mmap區域時,因虛擬地址空間不足,觸發了EAGAIN錯誤。
解決方案一:增大mmap區域
針對虛擬地址空間不足的問題,我們通過修改內核參數來增大mmap區域。默認情況下,TASK_UNMAPPED_BASE
的值為TASK_SIZE / 3
,這個值大約是進程虛擬地址空間的1/3,系統通常會將大部分的虛擬地址空間分配給已映射的區域(如代碼段、堆、棧等),只留少量空間給未映射區域。
我們將TASK_UNMAPPED_BASE
的值從默認的0x2AAA8000調整至0x10000000。這一調整實際上是將未映射區域的起始地址向高地址移動,擴展了系統的虛擬地址空間中可供動態分配的內存空間。重啟服務后,線程創建成功率大幅提升,系統運行穩定無阻。
解決方案二:優化線程棧空間
除了調整mmap區域外,優化線程棧空間也是提高資源利用率的有效手段。過大的棧空間預分配可能無意間擠占了寶貴的虛擬地址空間。
臨時調整棧空間大小(會話級):
ulimit -s 102400
上述命令可即時將棧空間大小設為100MB,適用于當前會話。
永久調整棧空間大小:
編輯/etc/security/limits.conf
,添加如下行:
* soft stack 102400
此設置確保系統長期維持100MB的棧空間大小,防止因分配不當引發的創建失敗。
使用C++接口設置創建的線程棧大小
pthread_attr_t attribute;
pthread_t thread;pthread_attr_init(&attribute);
pthread_attr_setstacksize(&attribute, 10240); // 設置線程棧的大小為10K
pthread_create(&thread,&attribute,foo,0);
pthread_join(thread,0);
解決方案三:引入線程池
為從根本上解決頻繁線程創建帶來的問題,可以采用線程池(Thread Pool)的設計模式。線程池預先創建一組固定數量的工作線程,等待任務到來時再分配給空閑線程執行,而非每次任務都創建新線程。
不僅可以解決mmap虛擬地址空間耗盡的問題,還顯著提高了系統性能和資源利用率,使任務執行更加平滑,避免因線程創建失敗導致的服務中斷,
#ifndef THREAD_POOL_H
#define THREAD_POOL_H#include <mutex>
#include <queue>
#include <thread>
#include <vector>
#include <functional>class ThreadPool {public:explicit ThreadPool(size_t threads);void enqueue(const std::function<void()>& task);~ThreadPool();private:// 線程池配置size_t threads_;// 工作線程std::vector<std::thread> workers_;// 任務隊列和同步std::mutex queue_mutex_;std::queue<std::function<void()>> tasks_;bool stop_;
};// 構造函數創建工作線程
inline ThreadPool::ThreadPool(size_t threads) : threads_(threads), stop_(false) {for (size_t i = 0; i < threads_; ++i) {workers_.emplace_back([this] {while (true) {std::function<void()> task;{std::unique_lock<std::mutex> lck(queue_mutex_);// 等待任務或停止信號queue_mutex_.wait(lck, [this] { return this->stop_ || !this->tasks_.empty(); });if (this->stop_ && this->tasks_.empty()) {return;}// 獲取下一個任務task = std::move(this->tasks_.front());this->tasks_.pop();}// 執行任務task();}});}
}// 將任務排隊到線程池中執行
void ThreadPool::enqueue(const std::function<void()>& task) {{std::unique_lock<std::mutex> lck(queue_mutex_);// 停止后不接受任務if (stop_) {throw std::runtime_error("Enqueue on stopped ThreadPool");}// 將任務添加到隊列中tasks_.push(task);}queue_mutex_.notify_one();
}// 析構函數等待工作線程終止
inline ThreadPool::~ThreadPool() {{std::unique_lock<std::mutex> lck(queue_mutex_);stop_ = true;}queue_mutex_.notify_all();for (std::thread& worker : workers_) {worker.join();}
}#endif
參考文章
一個std::thread()線程創建失敗問題分析過程