定位和分析解决std::thread创建失败的问题和解决方法(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 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。
    • 对于每个线程,输出它的 statusstacksyscall 文件内容到输出文件中,以及相关的分隔符和空行用于格式化输出。

根因分析

进一步探究线程创建流程,从C++标准库std::thread出发,经由POSIX线程API pthread_create,直至内核层面的clonedo_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()线程创建失败问题分析过程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值