线程池进阶:动态调整线程数、任务优先级队列、线程池的优雅关闭

线程池这东西,基础版本写起来不难。但真正用到生产环境,你会发现坑一个接一个。我早期做过一个后台服务,线程池大小写死了,结果高峰期请求量暴增,任务队列塞满,内存直接爆了。嗯,从那以后我就开始琢磨——线程池必须能动态调整。

这一章,咱们就聊聊线程池的三个进阶话题:动态调整线程数任务优先级队列优雅关闭。说白了,就是让线程池从「能用」变成「好用」。

一、动态调整线程数

为什么需要动态调整?你想想看,系统负载是波动的。白天高峰期线程不够用,任务排队;晚上低峰期线程太多,白白浪费资源。我习惯的做法是:让线程池根据任务队列长度或CPU使用率,自动增减线程数。

1.1 核心思路

动态调整的核心逻辑其实不复杂:

  • 定期检查任务队列长度
  • 如果队列积压超过阈值,增加工作线程
  • 如果线程空闲时间过长,减少工作线程
  • 设置上下限,防止线程数失控

1.2 代码实现

class DynamicThreadPool {
public:
    DynamicThreadPool(size_t min_threads, size_t max_threads)
        : min_threads_(min_threads)
        , max_threads_(max_threads)
        , current_threads_(min_threads)
        , stop_(false) {
        
        // 启动调整线程
        adjuster_ = std::thread([this] { adjust_loop(); });
        
        // 创建工作线程
        for (size_t i = 0; i < min_threads; ++i) {
            workers_.emplace_back([this] { worker_loop(); });
        }
    }

private:
    void adjust_loop() {
        while (!stop_) {
            std::this_thread::sleep_for(std::chrono::seconds(5));
            
            size_t queue_size = tasks_.size();
            size_t active = active_threads_.load();
            
            // 队列积压,增加线程
            if (queue_size > threshold_ && current_threads_ < max_threads_) {
                size_t to_add = std::min(queue_size / 10, max_threads_ - current_threads_);
                for (size_t i = 0; i < to_add; ++i) {
                    workers_.emplace_back([this] { worker_loop(); });
                }
                current_threads_ += to_add;
            }
            
            // 线程空闲,减少线程
            if (queue_size == 0 && active < current_threads_ / 2 
                && current_threads_ > min_threads_) {
                // 发送退出信号给空闲线程
                idle_exit_.store(true);
                idle_cv_.notify_all();
            }
        }
    }

    std::vector<std::thread> workers_;
    std::atomic<size_t> active_threads_{0};
    std::atomic<size_t> current_threads_;
    size_t min_threads_;
    size_t max_threads_;
    std::atomic<bool> stop_;
    std::atomic<bool> idle_exit_{false};
    std::condition_variable idle_cv_;
    static constexpr size_t threshold_ = 100;
};
注意:动态调整线程数时,一定要处理好线程的退出逻辑。我曾经遇到过一个bug——减少线程时没有正确同步,导致工作线程还在执行任务就被销毁了,数据直接损坏。血的教训。

二、任务优先级队列

有些场景下,任务有轻重缓急。比如一个日志系统,写错误日志的优先级应该高于写调试日志。普通的FIFO队列就不够用了。

2.1 优先级队列的设计

我建议用std::priority_queue作为底层容器,配合互斥锁实现线程安全。但要注意一点:优先级队列的top()返回的是最高优先级元素,而pop()移除它。这正好符合我们的需求。

template<typename T>
class PriorityTaskQueue {
public:
    void push(T task, int priority) {
        std::lock_guard<std::mutex> lock(mutex_);
        queue_.emplace(priority, std::move(task));
        cv_.notify_one();
    }

    T pop() {
        std::unique_lock<std::mutex> lock(mutex_);
        cv_.wait(lock, [this] { return !queue_.empty() || stop_; });
        
        if (stop_ && queue_.empty()) {
            return T{};  // 哨兵值
        }
        
        T task = std::move(queue_.top().second);
        queue_.pop();
        return task;
    }

private:
    std::priority_queue<std::pair<int, T>> queue_;
    std::mutex mutex_;
    std::condition_variable cv_;
    bool stop_ = false;
};
小技巧:优先级用int表示,数值越大优先级越高。我习惯把紧急任务设为100,普通任务设为0,后台任务设为-100。这样一目了然。

2.2 优先级反转问题

这里有个坑——优先级反转。什么意思?就是高优先级任务被低优先级任务阻塞了。比如线程池所有线程都在处理低优先级任务,高优先级任务只能排队等着。

解决办法有两个:

  • 预留几个「高优先级专用线程」,只处理高优先级任务
  • 任务抢占:当高优先级任务入队时,中断当前低优先级任务

我个人更倾向于第一种方案,简单可靠。第二种方案实现复杂,而且中断任务可能导致数据不一致。

三、线程池的优雅关闭

线程池关闭这件事,看似简单,实则暗藏杀机。直接std::terminate?那正在执行的任务怎么办?队列里还没执行的任务怎么办?

3.1 关闭的三个阶段

我总结了一个三阶段关闭法:

  1. 拒绝新任务:设置标志位,push()直接返回false
  2. 等待执行中的任务完成:让工作线程把当前任务跑完
  3. 清理未执行的任务:队列里剩下的任务,要么丢弃,要么持久化
void shutdown() {
    // 第一阶段:拒绝新任务
    stop_accept_.store(true);
    
    // 第二阶段:通知所有工作线程准备退出
    stop_.store(true);
    cv_.notify_all();
    
    // 第三阶段:等待所有线程结束
    for (auto& worker : workers_) {
        if (worker.joinable()) {
            worker.join();
        }
    }
    
    // 第四阶段:处理剩余任务
    std::lock_guard<std::mutex> lock(mutex_);
    while (!tasks_.empty()) {
        auto task = std::move(tasks_.front());
        tasks_.pop();
        // 这里可以记录日志,或者持久化到磁盘
        // 我习惯把未执行的任务写入一个文件,下次启动时重新加载
        save_to_disk(std::move(task));
    }
}

关键点:关闭过程中,工作线程要能区分「正常退出」和「异常退出」。正常退出时,线程应该处理完当前任务再退出。异常退出时,可能需要回滚操作。

3.2 超时关闭

有些任务可能卡住了,一直不结束。这时候不能无限等下去。我建议加一个超时机制:

void shutdown_with_timeout(std::chrono::seconds timeout) {
    stop_accept_.store(true);
    stop_.store(true);
    cv_.notify_all();
    
    auto deadline = std::chrono::steady_clock::now() + timeout;
    
    for (auto& worker : workers_) {
        if (worker.joinable()) {
            auto remaining = deadline - std::chrono::steady_clock::now();
            if (remaining > std::chrono::seconds(0)) {
                worker.join();  // 正常等待
            } else {
                worker.detach();  // 超时,分离线程
                // 记录警告日志
                log_warning("Worker thread timed out, detached");
            }
        }
    }
}
警告:detach()是最后的手段。分离后的线程如果持有资源,可能导致资源泄漏。我只有在万不得已时才用这个方案。

四、整体架构图

下面这张图展示了线程池进阶版本的完整架构。你可以看到动态调整、优先级队列、优雅关闭三个模块如何协同工作。

线程池进阶架构图 任务提交接口 任务优先级队列 高优先级 → 中优先级 → 低优先级 工作线程池 线程 1 线程 2 线程 3 线程 4 线程 N 动态 调整 模块 优雅 关闭 模块 三个核心模块协同工作:动态调整线程数、优先级调度、优雅关闭 任务提交 任务调度 动态调整 优雅关闭

五、避坑指南

最后,分享几个我踩过的坑:

  • 线程数不要频繁调整:创建和销毁线程是有代价的。我建议至少间隔5秒做一次调整,避免抖动。
  • 优先级队列不要无限增长:设置最大容量,超过后拒绝低优先级任务。否则内存会爆。
  • 关闭时注意死锁:如果工作线程持有锁,而关闭线程也在等同一个锁,就会死锁。我习惯在关闭前先释放所有锁。
  • 任务优先级不要太多:3-5个优先级足够了。太多优先级反而增加调度开销,得不偿失。

嗯,这一章的内容就到这里。线程池进阶其实还有很多细节,比如任务窃取、工作窃取、自适应调度等等。但掌握了动态调整、优先级队列和优雅关闭这三个核心能力,你的线程池已经能应对大部分生产场景了。

公众号:蓝海资料掘金营,微信deep3321