引言
在高并发和长周期运行的环境中,频繁创建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 threads
for pid in $pids; do
echo "Process $pid" >> "$output_file"
cat /proc/$pid/maps >> "$output_file"
for tid in /proc/$pid/task/*; do
tid=$(basename "$tid")
{
echo "--------------------------------------------------------------"
echo "Thread: $tid"
echo "--------------------------------------------------------------"
echo "status:"
cat /proc/$pid/task/$tid/status
echo "stack:"
cat /proc/$pid/task/$tid/stack
echo "syscall:"
cat /proc/$pid/task/$tid/syscall
echo ""
} >> "$output_file"
done
echo "" >> "$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