上篇聊了C17的新特性今天进入一个在机器人开发里绕不开的话题——多线程。面试的时候面试官问你在项目里用过线程吗我说用过。面试官接着问为什么机器人系统需要多线程我说因为要并行处理。面试官说能具体点吗我当时的回答太笼统了。今天结合实际场景把这个话题讲透。为什么机器人必须用多线程一个典型的移动机器人同时要做这些事读取传感器数据激光雷达、IMU、编码器、运行定位算法SLAM、执行路径规划、发送电机控制指令、处理通信消息。这些任务的频率不一样。IMU可能1000Hz激光雷达10Hz路径规划1Hz通信消息不定。如果全塞在一个线程里串行执行高频任务会被低频任务拖死。想象一下你的路径规划正在算A这时候IMU数据来了但得等A算完才能处理。等了两秒机器人的姿态已经偏了好几度。控制回路延迟这么大机器人不走直线是必然的。所以必须用多线程传感器采集一个线程、定位算法一个线程、路径规划一个线程、控制回路一个线程。各线程独立运行互不阻塞。std::thread最基础的线程创建C11引入了std::thread创建线程非常简单#include thread #include iostream void sensorLoop() { while (true) { // 读取传感器数据 std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } int main() { std::thread t(sensorLoop); // 创建并启动线程 // t.join(); // 等待线程结束 t.detach(); // 或者分离让线程独立运行 }join()和detach()必须选一个。join是等线程跑完detach是让线程独立运行。如果不调用任何一个thread的析构函数会直接调std::terminate——程序崩溃。这个坑我当年踩过。创建了一个thread对象忘了join或detach程序跑着跑着就abort了。debug了半小时才找到原因。线程也可以带参数void processCamera(int cameraId, std::string configPath) { // 处理特定相机的数据 } std::thread t1(processCamera, 0, cam_front.yaml); std::thread t2(processCamera, 1, cam_rear.yaml); t1.join(); t2.join();mutex保护共享数据多线程一上来就会遇到的问题数据竞争。class SharedMap { std::mapstring, double landmarks_; public: void update(const string name, double value) { landmarks_[name] value; // 多线程同时写数据竞争 } double get(const string name) { return landmarks_[name]; // 同时读写也是数据竞争 } };两个线程同时操作map轻则数据错乱重则段错误。解决办法是用互斥锁mutexclass SharedMap { std::mapstring, double landmarks_; std::mutex mtx_; public: void update(const string name, double value) { std::lock_guardstd::mutex lock(mtx_); landmarks_[name] value; } double get(const string name) { std::lock_guardstd::mutex lock(mtx_); return landmarks_[name]; } };lock_guard是RAII风格的锁管理——构造时加锁析构时自动解锁。不管函数怎么退出正常return还是抛异常锁都能正确释放。在机器人开发里典型需要加锁的场景地图数据的读写、传感器配置的更新、全局状态变量的修改。但要注意锁的粒度越小越好——不要锁整个函数只锁真正访问共享数据的那几行。atomic轻量级的线程安全有些操作只需要保证原子性不需要复杂的锁机制。比如一个计数器#include atomic class FrameCounter { std::atomicint count_{0}; public: void increment() { count_; } // 原子操作线程安全 int get() const { return count_.load(); } };std::atomic保证了操作的原子性。count_在多线程下不会出现丢失更新的问题。相比mutexatomic的优势是无锁。它通常用硬件级别的原子指令实现比如x86的LOCK前缀不需要操作系统介入性能更好。但atomic只适合简单的操作——加减、赋值、比较交换CAS。复杂的数据结构map、vector还是得用mutex。在机器人开发里atomic常用于帧计数器、状态标志位、简单的统计量。比如一个std::atomicbool isRunning_来控制线程的启停比加mutex轻量得多。面试中的并发考点面试官考并发编程几个经典问题join和detach有什么区别join会阻塞当前线程等待目标线程执行完毕detach让目标线程独立运行之后无法再同步它。实际开发中ROS节点通常用detach因为回调函数需要一直运行而计算任务通常用join等结果算完再继续。什么是数据竞争两个或以上线程同时访问同一内存位置且至少有一个是写操作。数据竞争是未定义行为——程序可能崩溃、可能数据错乱、可能看起来正常但在不同编译器上表现不同。lock_guard和unique_lock有什么区别lock_guard简单轻量构造加锁、析构解锁不能手动控制。unique_lock更灵活可以手动lock/unlock支持延迟加锁也能配合条件变量使用。性能上lock_guard略优因为编译器有更多优化空间。再聊一个面试中必问的话题死锁的预防和条件变量的使用。死锁产生的四个条件是互斥、持有并等待、不可抢占、循环等待。在代码层面最常见的死锁场景是两个线程分别持有两把锁然后各自尝试获取对方持有的锁。预防死锁最简单的方法是保证所有线程按相同的顺序获取锁——比如永远先锁A再锁B就不会出现循环等待。C17还提供了std::lock和std::scoped_lock来同时锁定多把锁避免死锁。条件变量std::condition_variable则是线程间协作的关键工具——生产者往队列里放数据后通知消费者消费者等待通知后再取数据处理。在机器人系统里传感器数据从采集线程传到处理线程条件变量是最常用的同步机制。面试时能讲清楚死锁预防和条件变量的配合使用面试官会认为你对并发编程有实战经验。给正在准备面试的你C并发编程在面试里考的是意识。不要求你写多复杂的并发代码但你得知道什么时候该用线程、共享数据怎么保护、mutex和atomic各自适合什么场景。机器人系统天然需要多线程——传感器采集、定位、规划、控制每个模块都有自己的频率。面试的时候能把这个场景讲清楚比背十个API更有说服力。分享一个并发编程的调试经验遇到多线程bug先加日志打印线程ID和时间戳定位是不是竞态条件导致的。GDB的多线程调试功能可以暂停所有线程、查看各线程的调用栈非常实用。另外编译时加上-fsanitizethreadThreadSanitizer可以自动检测数据竞争比人肉排查高效得多。下篇开始进入阶段二C进阶与实战。第一篇聊RAII惯用法——C资源管理的核心思想也是面试区分候选人水平的关键知识点。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第39篇 C17结构化绑定与optional——让代码更优雅的现代特性 下一篇预告第41篇 RAII惯用法——C资源管理的核心思想有任何问题欢迎评论区留言我会尽量回复。