多线程板块的前几篇把基础聊完了——线程创建、互斥锁、条件变量、原子操作。今天把这些知识串起来,做一个完整的组件:线程池。

线程池在机器人系统里用得非常多。ROS2的executor底层就是一个线程池,负责分发回调函数。你的SLAM系统、路径规划模块,凡是有批量异步任务的地方,线程池都是标配。

先说为什么需要线程池。

假设你的机器人每帧要处理100个特征点的匹配任务。如果每个任务都创建一个线程,就是100个线程。线程的创建和销毁是有开销的——创建线程需要分配栈空间、调用系统API,大概耗时几十微秒。100个线程就是几毫秒,而你的帧周期可能只有10ms。

更糟的是,100个线程同时跑,CPU频繁做上下文切换,实际效率可能还不如串行执行。

线程池的思路是:预先创建固定数量的线程,让它们从任务队列里取任务执行。线程不用反复创建销毁,数量也可控。

手写一个线程池

面试的时候,面试官可能让你手写一个简化版的线程池。把前面学的mutex、条件变量、thread全用上:

class ThreadPool {
    vector<thread> workers_;
    queue<function<void()>> tasks_;
    mutex mtx_;
    condition_variable cv_;
    bool stop_ = false;
    
public:
    ThreadPool(size_t threadCount) {
        for (size_t i = 0; i < threadCount; ++i) {
            workers_.emplace_back([this] {
                while (true) {
                    function<void()> task;
                    {
                        unique_lock<mutex> lock(mtx_);
                        cv_.wait(lock, [this] { 
                            return stop_ || !tasks_.empty(); 
                        });
                        if (stop_ && tasks_.empty()) return;
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    task();  // 执行任务(在锁外执行)
                }
            });
        }
    }
    
    void enqueue(function<void()> task) {
        {
            lock_guard<mutex> lock(mtx_);
            tasks_.push(std::move(task));
        }
        cv_.notify_one();
    }
    
    ~ThreadPool() {
        {
            lock_guard<mutex> lock(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
        for (auto& w : workers_) {
            w.join();
        }
    }
};

核心逻辑很简单:构造函数里创建N个工作线程,每个线程循环从任务队列里取任务执行。enqueue方法把任务塞进队列并通知一个线程。析构函数设置stop标志,唤醒所有线程,然后join等待它们退出。

使用方式:

ThreadPool pool(4);  // 4个工作线程

// 提交100个特征点匹配任务
for (const auto& feature : features) {
    pool.enqueue([feature] {
        matchFeature(feature);
    });
}

线程池的参数选择

线程池的大小怎么定?一个常见的经验公式是:线程数 = CPU核心数。因为线程数超过核心数也没有意义——CPU同一时刻只能执行那么多任务,多余的线程只是在排队等。

但如果是IO密集型任务(比如读写文件、网络通信),线程数可以设大一些。因为IO操作会让线程阻塞,阻塞期间CPU可以执行其他线程。

// 计算密集型:线程数 = 核心数
unsigned int computeThreads = std::thread::hardware_concurrency();

// IO密集型:线程数 = 核心数 * 2 或更多
unsigned int ioThreads = std::thread::hardware_concurrency() * 2;

在机器人系统里,大部分任务是计算密集型的(矩阵运算、点云处理),所以线程数一般等于核心数。

不过线程池也有局限:它适合互相独立的任务。如果任务之间有依赖关系(比如任务B必须等任务A完成),普通的线程池就不太好处理了。这种情况需要更高级的任务调度框架,或者用future链来编排依赖。

在机器人开发中的应用

ROS2的executor就是一个线程池。你可以创建一个MultiThreadedExecutor,指定线程数,然后把节点加进去:

rclcpp::executors::MultiThreadedExecutor executor(
    rclcpp::ExecutorOptions(), 4);  // 4个线程
executor.add_node(myNode);
executor.spin();  // 4个线程并行执行回调

在SLAM系统里,线程池常用于并行处理特征提取。比如一帧图像要提取500个ORB特征点,每个特征点的描述子计算是独立的,可以丢给线程池并行处理:

ThreadPool pool(std::thread::hardware_concurrency());
vector<future<void>> futures;

for (auto& keypoint : keypoints) {
    futures.push_back(pool.enqueueWithFuture([&keypoint] {
        computeDescriptor(keypoint);
    }));
}

// 等待所有任务完成
for (auto& f : futures) f.get();

面试中的关键考点

"线程池的核心组件有哪些?"工作线程、任务队列、同步机制(mutex+条件变量)。工作线程循环取任务执行,任务队列缓冲待处理的任务,同步机制保证队列的线程安全。

"怎么让线程池支持返回值?"用std::packaged_task和std::future。把任务包装成packaged_task,返回future给调用者,调用者通过future.get()获取结果。

"线程池怎么优雅地关闭?"设置一个stop标志,通知所有线程。线程检查到stop标志为true且任务队列为空时退出循环。主线程join所有工作线程,确保它们都安全退出。注意要在锁的保护下设置stop标志,防止竞态条件。

线程池的常见坑

实际用线程池最容易踩的坑有三个。第一个是"任务里抛异常导致线程退出"——如果任务函数里抛了异常但没catch,线程就直接退出了,线程池的线程数会越来越少,最终可能导致线程全部退出。解决办法是在任务执行的外层加一层try-catch:

try {
    task();
} catch (const std::exception& e) {
    std::cerr << "Task exception: " << e.what() << std::endl;
}

第二个坑是"任务提交太快队列爆掉"。如果生产者提交任务的速度远大于消费者处理的速度,任务队列会无限增长。解决方案是加一个队列上限,满了就阻塞等待或者选择丢弃新任务。第三个坑是"线程池析构时有任务还没执行完"——上面的实现在析构时会丢弃队列里未执行的任务,如果需要保证所有任务都执行完毕再退出,要加一个drain逻辑。面试的时候能提到这些实际踩坑的宝贵经验,面试官会觉得你不是只会写demo,而是真正在生产环境中摸爬滚打过的开发者。

给正在准备面试的你

线程池是面试里的"综合题"——它把线程创建、mutex、条件变量、function、move语义这些知识点全串在一起了。能完整写出来,说明你的C++多线程编程基础是扎实的。

不要求一字不差地背下来,但要理解每个组件的作用和设计原因:为什么用条件变量(等任务)、为什么在锁外执行任务(减少锁竞争)、为什么析构要notify_all(唤醒阻塞的线程让它们退出)。

下篇进入网络编程板块——socket编程与机器人之间的通信。机器人之间的协作、机器人与云端服务器的通信,都离不开网络编程的基础知识。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列 上一篇:第57篇 多线程之原子操作与内存序——高性能无锁编程入门 下一篇预告:第59篇 C++网络编程基础——socket编程与机器人通信

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐