☰
简历技术栈全面复习——接口 、抽象类、Strategy / Adapter和多线程
2026/9/26 11:07:52 网站建设 项目流程

好,继续。现在进入C++17 核心基础。

这一块我们不从“什么是类、什么是变量”这种最基础的内容开始,而是直接围绕你简历里真正会用到的:

C++17 ↓ 对象生命周期 ↓ RAII ↓ 智能指针 ↓ 接口 / 抽象类 ↓ Strategy / Adapter ↓ 多线程 ↓ 锁 ↓ 条件变量 ↓ 队列 ↓ 异常

先把整个体系建立起来。


一、先理解:为什么机器人项目特别依赖 C++?

你的机器人程序里有很多东西:

UDP Socket ZMQ Protobuf GMR EtherCAT 线程 队列 ROS2 传感器 电机

这些东西都有一个共同特点:

资源很多,而且生命周期很重要。

例如:

Socket 线程 内存 文件 ZMQ Context ZMQ Socket EtherCAT Master EtherCAT Domain

如果程序员没有管理好生命周期,就容易出现:

内存泄漏 野指针 重复释放 线程没退出 Socket没关闭 资源提前释放 程序崩溃

所以 C++ 最核心的问题之一其实不是“怎么写语法”。

而是:

谁创建?谁使用?什么时候销毁?


二、先搞懂“生命周期”

比如:

void test() { int x = 10; }

程序执行:

进入 test ↓ 创建 x ↓ 使用 x ↓ 离开 test ↓ x 销毁

这就是:

对象生命周期。


三、对象生命周期为什么重要?

假设:

class Robot { public: Robot() { // 初始化 } ~Robot() { // 清理 } };

使用:

void test() { Robot robot; }

执行:

Robot robot; ↓ 构造函数 ↓ 对象开始存在 ↓ 使用 ↓ 离开作用域 ↓ 析构函数 ↓ 对象销毁

所以:

构造函数 = 创建/初始化资源 析构函数 = 释放/清理资源

这是后面 RAII 的基础。


四、RAII是什么?

这个词你简历里非常重要。

全称:

Resource Acquisition Is Initialization

不用死记英文。

直接记一句:

资源跟着对象走,对象死了,资源自动释放。

例如:

{ std::mutex mutex; // 使用 mutex } // 离开作用域 // 自动释放

更典型:

{ std::lock_guard<std::mutex> lock(mutex); // 临界区 } // 自动 unlock

你甚至不需要:

mutex.lock(); ... mutex.unlock();

因为:

lock_guard创建 ↓ 自动加锁 ↓ 执行代码 ↓ 离开作用域 ↓ 析构 ↓ 自动解锁

这就是 RAII。


五、为什么RAII特别适合机器人程序?

因为机器人程序有很多资源。

比如:

程序启动 ↓ 打开Socket ↓ 创建线程 ↓ 创建ZMQ ↓ 连接EtherCAT ↓ 运行

如果中间出现异常:

程序异常

怎么办?

如果全部靠人工:

open(); ... close();

很容易漏。

RAII则是:

对象创建 ↓ 资源获取 ↓ 使用 ↓ 对象销毁 ↓ 资源自动释放

所以 C++ 工程里非常强调 RAII。


六、智能指针就是RAII思想的一个重要应用

你应该见过:

std::unique_ptr std::shared_ptr std::weak_ptr

先别急着背区别。

先理解:

普通指针 ↓ 我自己管理内存 智能指针 ↓ 对象帮助我管理内存

七、普通new有什么问题?

例如:

Robot* robot = new Robot();

你必须:

delete robot;

否则:

内存泄漏

如果中途:

return;

或者:

throw exception;

就很容易忘记释放。


八、unique_ptr

现代 C++ 更推荐:

auto robot = std::make_unique<Robot>();

它表示:

这个对象只有一个主人。

例如:

robot_controller ↓ unique_ptr ↓ Robot

当:

robot_controller

销毁:

unique_ptr销毁 ↓ Robot自动销毁

不用自己:

delete

九、为什么叫unique?

因为:

unique_ptr

强调:

唯一拥有者。

不能这样:

auto p1 = std::make_unique<Robot>(); auto p2 = p1; // 错

因为一个对象不能同时拥有两个unique_ptr。

但是可以:

auto p2 = std::move(p1);

意思:

p1 ↓ 所有权转移 ↓ p2

转移之后:

p1 = 空 p2 = Robot

十、这个“所有权”概念非常重要

以后看到:

std::move()

不要第一反应认为:

“把对象移动一下。”

更重要的是:

可能发生所有权转移。

比如:

Controller │ │ unique_ptr ↓ Robot

把它移动给:

Manager

变成:

Controller Manager │ │ × ↓ Robot

所有权发生了变化。


十一、shared_ptr

如果一个对象确实需要多个地方共同拥有:

auto robot = std::make_shared<Robot>();

可以:

┌── Controller │ Robot ←───┼── Manager │ └── Monitor

多个shared_ptr:

Controller ↓ shared_ptr ↘ Robot ↗ shared_ptr ↑ Manager

它内部会维护:

引用计数。

例如:

Robot 引用计数 = 3

三个shared_ptr都存在。

销毁一个:

3 → 2

再销毁:

2 → 1

最后一个消失:

1 → 0

于是:

Robot ↓ 自动销毁

十二、unique_ptr和shared_ptr怎么选?

先记一个非常实用的原则:

默认优先 unique_ptr

只有真正存在:

多个所有者

的时候,再考虑:

shared_ptr

不要因为:

“shared_ptr比较方便”

就到处使用。

因为 shared_ptr 会带来:

引用计数 所有权关系复杂 生命周期不容易看清 循环引用风险

十三、weak_ptr是什么?

它解决一个典型问题:

我想观察一个对象,但我不拥有它。

例如:

Manager ↓ shared_ptr Robot ↑ Monitor

Monitor只是想:

“看看Robot还在不在。”

不应该因此增加一个拥有关系。

所以可以:

std::weak_ptr<Robot>

理解成:

shared_ptr = 我拥有你 weak_ptr = 我只是看看你

十四、一个非常重要的生命周期问题

假设:

A shared_ptr → B B shared_ptr → A

变成:

A ←→ B

即使外部已经没有:

shared_ptr

A和B互相引用:

引用计数不会变成0

于是:

A没释放 B没释放

这就是:

循环引用。

所以:

shared_ptr + weak_ptr

经常一起出现。


十五、现在进入接口 / 抽象类

这与你之前的:

MotionInput XsensUdpSource NoitomMocapApiSource

直接对应。

你之前的设计可以理解为:

MotionInput ↑ ┌─────────┴─────────┐ │ │ XsensUdpSource NoitomMocapApiSource

MotionInput是接口。

例如:

class MotionInput { public: virtual NoitomFrame receive() = 0; virtual ~MotionInput() = default; };

这里:

= 0

表示:

纯虚函数。

于是:

MotionInput

成为抽象类。

不能直接:

MotionInput input;

十六、为什么要接口?

因为你的上层程序根本不应该关心:

到底是Xsens? 还是Noitom? 还是以后换成别的设备?

上层只关心:

MotionInput ↓ receive() ↓ NoitomFrame

于是:

MotionInput ↑ ┌─────────────┼─────────────┐ ↓ ↓ ↓ Xsens Noitom Future

这就是:

解耦。


十七、这就是你简历里的Adapter思想

假设 Xsens 原始数据:

XsensData

Noitom:

NoitomData

但 GMR 只接受:

NoitomFrame

怎么办?

做 Adapter:

Xsens原始数据 ↓ XsensUdpSource ↓ Adapter ↓ NoitomFrame

另一边:

Noitom API ↓ NoitomMocapApiSource ↓ NoitomFrame

于是 GMR 完全不用知道输入到底是谁。


十八、Strategy又是什么?

Strategy主要解决:

同一个任务,有不同算法/实现方式。

例如:

MotionInput

可以看成策略。

┌─────────────────────┐ │ MotionInput Strategy│ └──────────┬──────────┘ │ ┌─────┴─────┐ ↓ ↓ Xsens Noitom

上层:

std::unique_ptr<MotionInput> input;

运行时:

选择Xsens

或者:

选择Noitom

上层代码不需要修改。

这就是 Strategy 的核心价值:

把“变化的东西”独立出来。


十九、Adapter和Strategy千万别混

你可以这样区分:

Adapter

解决:

接口不一样,怎么接起来?

A接口 ↓ Adapter ↓ B接口

Strategy

解决:

同一个功能有多种实现,运行时选择哪一个?

统一接口 ↓ 策略A 策略B 策略C

你的动捕系统里两者可能同时存在。


二十、现在把C++核心思想串起来

你现在看到:

C++17 │ ├── OOP │ ├── 封装 │ ├── 继承 │ └── 多态 │ ├── 生命周期 │ ├── RAII │ ├── 智能指针 │ ├── unique_ptr │ ├── shared_ptr │ └── weak_ptr │ ├── 接口 │ ├── Strategy │ └── Adapter

实际上不是一堆零散知识。

它们最终都是在解决:

大型机器人程序怎么做到模块化、安全、可替换、生命周期清晰。


二十一、拿你的项目再看一次

你的程序可以抽象成:

Controller │ unique_ptr ↓ MotionInput ↑ ┌───────────┴───────────┐ │ │ XsensAdapter NoitomSource │ │ └───────────┬───────────┘ ↓ NoitomFrame ↓ GMR ↓ MocapFrame ↓ Protobuf ↓ ZMQ ↓ MuJoCo

这时候你就应该理解:

为什么要:

接口

为什么要:

unique_ptr

为什么要:

Adapter

为什么要:

Strategy

而不是把所有代码写进一个:

main.cpp

二十二、这一节先记住6句话

① 生命周期

对象什么时候创建、什么时候销毁。

② RAII

资源跟着对象生命周期走。

③ unique_ptr

一个明确的所有者。

④ shared_ptr

多个所有者共同管理。

⑤ 接口

上层只依赖能力,不依赖具体实现。

⑥ Strategy / Adapter

Strategy解决“实现可替换”,Adapter解决“接口不兼容”。


下一节我们继续C++多线程:

线程 ↓ std::thread ↓ 生产者/消费者 ↓ mutex ↓ lock_guard ↓ unique_lock ↓ condition_variable ↓ 线程安全队列 ↓ 线程退出 ↓ 生命周期

这一块会直接把你之前项目中的:

UDP接收线程 ↓ 队列 ↓ GMR线程 ↓ ZMQ线程

彻底串起来。

好,继续。现在进入C++ 多线程 + 锁 + 条件变量 + 生产者消费者。

这部分和你之前做的机器人实时数据链路直接对应,所以我们重点理解为什么这么设计,不陷入特别底层的线程实现。


一、先看你的机器人程序为什么需要多个线程

假设你的系统只有一个线程:

main ↓ 接收UDP ↓ 解析动捕 ↓ GMR ↓ Protobuf ↓ ZMQ ↓ MuJoCo ↓ 再接收UDP

问题是:

如果 GMR 计算用了:

20ms

那么这20ms里:

UDP接收

可能就没办法及时处理。

所以通常会拆:

UDP线程 ↓ Queue ↓ GMR线程 ↓ Queue ↓ ZMQ线程

这就是多线程最基本的意义:

让不同任务可以同时进行。


二、std::thread

C++创建线程最基本的方式:

#include <thread> void receive() { // 接收数据 } int main() { std::thread t(receive); t.join(); }

这里:

std::thread t(receive);

就是:

创建一个线程执行receive()。


三、join是什么?

这是线程生命周期里非常重要的一个概念。

t.join();

意思:

当前线程等待 t 线程执行结束。

例如:

主线程 │ ├── 创建线程 │ ↓ │ t │ ├── 等待 │ ←──── t执行结束 │ 继续执行

所以:

join = 等它结束

四、detach是什么?

还有:

t.detach();

意思:

让线程独立运行,不再由当前线程等待。

但是对于工程程序,尤其机器人控制程序:

不要随便 detach。

因为你很容易遇到:

主对象已经销毁 ↓ 后台线程还在运行 ↓ 线程访问已经不存在的对象 ↓ 崩溃

所以实际工程里通常更关注:

线程怎么启动 线程怎么退出 线程怎么等待 对象什么时候销毁

而不是随便开后台线程。


五、真正的问题来了:共享数据

假设:

UDP线程 ↓ shared_data ↑ GMR线程

两个线程同时访问:

NoitomFrame frame;

比如 UDP线程:

frame = new_frame;

GMR线程:

use(frame);

这时候就出现:

数据竞争(data race)。


六、为什么数据竞争危险?

例如:

线程A:正在修改 frame 线程B:同时读取 frame

可能出现:

A修改到一半 ↓ B读取 ↓ 拿到一个“不完整”的数据

对于机器人:

关节1 关节2 关节3 ...

如果只更新了一半,就可能产生非常奇怪的问题。

所以需要:

锁。


七、mutex是什么?

std::mutex mutex;

它可以理解成:

一把钥匙。

某个线程拿到钥匙:

线程A ↓ lock ↓ 拿到钥匙 ↓ 访问共享数据 ↓ unlock

此时线程B:

想访问 ↓ 发现钥匙没了 ↓ 等待

八、最基本的写法

std::mutex mtx; mtx.lock(); shared_data = new_data; mtx.unlock();

但实际工程中不推荐这样裸写。

因为:

mtx.lock(); do_something(); mtx.unlock();

如果:

do_something();

中间发生异常:

throw

那么:

unlock()

可能根本执行不到。

结果:

死锁。

这就是 RAII 再次发挥作用的地方。


九、lock_guard

推荐:

std::lock_guard<std::mutex> lock(mtx); shared_data = new_data;

执行:

lock_guard创建 ↓ 自动lock ↓ 执行临界区 ↓ 离开作用域 ↓ lock_guard析构 ↓ 自动unlock

这就是:

RAII + 多线程。

所以你前面学的 RAII 并不是孤立的。


十、什么叫临界区?

例如:

{ std::lock_guard<std::mutex> lock(mtx); shared_data = new_data; }

这里:

shared_data = new_data;

就是临界区。

意思:

同一时间只允许一个线程操作这部分共享数据。

原则:

锁的范围不要太大。

不要:

lock(); 做大量计算 ↓ GMR ↓ 网络 ↓ 文件IO ↓ unlock();

这样其他线程会一直等。

应该:

加锁 ↓ 快速读写共享数据 ↓ 解锁 ↓ 耗时计算

十一、unique_lock是什么?

你还会看到:

std::unique_lock<std::mutex> lock(mtx);

和lock_guard类似,也是 RAII。

区别先简单记:

lock_guard → 简单加锁 unique_lock → 更灵活

比如条件变量:

std::unique_lock<std::mutex> lock(mtx); cv.wait(lock);

所以你以后看到:

condition_variable + unique_lock

不要觉得奇怪。

这是标准组合。


十二、为什么需要条件变量?

现在来到机器人程序非常核心的一个问题。

假设:

UDP线程

不停往队列放数据:

Frame 1 Frame 2 Frame 3 ...

而:

GMR线程

需要从队列拿。

如果队列为空:

GMR线程怎么办?

最笨的方法:

while (queue.empty()) { // 什么也不干 }

这叫:

忙等(busy waiting)。

CPU会一直转。


十三、条件变量解决什么?

条件变量:

std::condition_variable cv;

它可以让线程:

没数据的时候睡觉,有数据的时候叫醒。

生产者:

UDP线程 ↓ push(frame) ↓ notify_one()

消费者:

GMR线程 ↓ 没有数据 ↓ wait() ↓ 睡眠

收到通知:

UDP线程 ↓ notify_one() ↓ GMR线程被唤醒 ↓ 取数据

十四、标准生产者消费者模型

这就是你项目非常典型的结构:

Producer UDP接收线程 │ ↓ ┌──────────────┐ │ Queue │ └──────┬───────┘ ↓ Consumer GMR线程

代码结构大概:

std::queue<Frame> queue; std::mutex mtx; std::condition_variable cv;

生产者:

{ std::lock_guard<std::mutex> lock(mtx); queue.push(frame); } cv.notify_one();

消费者:

std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [&] { return !queue.empty(); }); auto frame = queue.front(); queue.pop();

这里最重要的是理解流程,不是现在背代码。


十五、为什么wait里面要写条件?

你经常会看到:

cv.wait(lock, [&] { return !queue.empty(); });

意思:

只要队列为空,就继续等;队列有数据了才能继续。

实际上:

wait ↓ 睡眠 ↓ 被唤醒 ↓ 检查条件 ↓ 如果还是没有数据 ↓ 继续等待

这就是为什么不能简单理解成:

notify = 一定有数据

更准确的是:

notify只是通知你“可能有变化了”,醒来后还要重新检查条件。


十六、这和你的50Hz动捕系统怎么对应?

你的动捕数据:

50 Hz

也就是大约:

20 ms

来一帧。

可以设计:

Xsens ↓ UDP Receive Thread ↓ Frame Queue ↓ GMR Thread ↓ MocapFrame ↓ ZMQ Thread ↓ MuJoCo

每20ms:

Frame ↓ 进入队列

GMR线程:

等待 ↓ 收到Frame ↓ 处理 ↓ 发送 ↓ 继续等待

十七、为什么还需要“有界队列”?

这是实时系统很重要的一点。

假设:

生产者:50帧/s 消费者:30帧/s

如果:

queue无限大

那么:

1秒 → 20帧积压 10秒 → 200帧 1分钟 → 1200帧

最后:

延迟越来越大。

虽然没有丢数据,但机器人已经在处理“过去的数据”。

这对实时控制反而更糟。


十八、所以实时机器人经常需要有界队列

例如:

Queue capacity = 10

最多:

10帧

满了以后怎么办?

根据业务可以:

丢最旧

或者:

丢最新

或者:

阻塞生产者

对于实时机器人:

“保留最新状态”通常比“保证每一帧都处理”更重要。

但具体策略要看业务,不能一概而论。


十九、为什么你以前的“丢帧测试”非常重要?

你之前分析:

7746 7747 7748

连续缺失。

现在把它放回整个系统:

UDP ↓ Frame ID ↓ Queue ↓ GMR ↓ ZMQ ↓ MuJoCo

出现:

7746 7747 7748

你就应该问:

是UDP没收到? ↓ 还是Queue丢了? ↓ 还是GMR没处理? ↓ 还是ZMQ没收到? ↓ 还是MuJoCo没消费?

这就是:

多线程 + 队列 + Frame ID + 日志

结合起来之后真正的工程排障能力。


二十、线程退出问题

还有一个非常容易被忽略的问题:

while (true) { ... }

这种线程怎么办?

程序退出时:

主线程退出

但:

UDP线程 GMR线程 ZMQ线程

还在运行。

所以实际工程需要:

线程生命周期管理。

常见思路:

std::atomic<bool> running{true};

线程:

while (running) { ... }

退出:

running = false;

然后:

thread.join();

最终:

停止信号 ↓ 线程退出循环 ↓ join ↓ 线程结束 ↓ 对象销毁

这就是一个完整的生命周期。


二十一、把你简历上的多线程全部串起来

现在你应该能看到:

C++ RAII ↓ 锁的自动释放 ↓ 多线程安全 std::thread ↓ 并行执行 mutex ↓ 保护共享数据 condition_variable ↓ 线程等待/唤醒 queue ↓ 线程之间传递数据 bounded queue ↓ 防止无限堆积 atomic<bool> ↓ 线程停止信号 join ↓ 等待线程安全退出

这其实就是一个完整的:

生产者—消费者架构。


二十二、最终形成你的机器人线程模型

你以后看到类似工程,可以先在脑子里画:

┌─────────────────┐ │ UDP Receive │ │ Thread │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ Bounded Queue │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ GMR / IK Thread │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ Protobuf │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ ZMQ Thread │ └────────┬────────┘ │ ↓ MuJoCo

旁边还有:

mutex condition_variable atomic<bool> join

负责保证整个系统能够:

安全运行 ↓ 高效等待 ↓ 控制队列 ↓ 正常停止

这一节你只需要真正记住 8 个东西

std::thread ↓ 创建线程 mutex ↓ 保护共享数据 lock_guard ↓ 简单、安全地加锁 unique_lock ↓ 更灵活的锁,常配合条件变量 condition_variable ↓ 没数据就睡,有数据再醒 queue ↓ 线程之间传递数据 atomic ↓ 线程停止标志等简单共享状态 join ↓ 等待线程结束

多线程这一块的核心框架已经建立起来了。

下一节我们把最后几个 C++ 工程必备点补完:

异常处理 ↓ try / catch ↓ 异常安全 ↓ 对象生命周期 ↓ 构造/析构顺序 ↓ 接口析构函数 ↓ RAII + 智能指针 + 线程

然后C++核心部分就基本完成,再进入你的Python 全栈部分。

继续。根据你简历原文,这一轮我们把C++ 最后几个工程核心点补完,然后就进入 Python。你简历明确写了 C++17、RAII、智能指针、多线程同步、生产者-消费者、有界队列,以及“排空退出与异常联动停止”。

C++ 最后一部分:异常、生命周期、线程安全退出

一、异常到底解决什么问题?

先看一个最简单的例子:

void run() { connect_device(); if (连接失败) { // 怎么办? } start_motor(); }

如果设备连接失败,程序需要把错误向上层传递。

C++可以使用:

throw

例如:

if (!connected) { throw std::runtime_error("device connect failed"); }

然后上层:

try { run(); } catch (const std::exception& e) { std::cout << e.what() << std::endl; }

整体:

底层 ↓ 发现错误 ↓ throw ↓ 向上层传播 ↓ catch ↓ 统一处理

二、为什么机器人程序需要异常?

你的项目里面有很多可能失败的地方:

UDP连接 ZMQ Protobuf EtherCAT 设备初始化 配置文件 线程 文件 测试任务 SSH

例如:

EtherCAT初始化失败 ↓ 抛出异常 ↓ 控制线程停止 ↓ 电机进入安全状态 ↓ 上层报告错误

这就是所谓:

异常联动停止。

而你的简历确实明确写到了这一点。


三、异常不是“程序崩溃”

这是初学者特别容易误解的地方。

throw std::runtime_error("error");

并不意味着:

程序一定马上崩溃。

如果有人接住:

try { ... } catch (const std::exception& e) { ... }

那么程序可以继续按照设计处理。

例如:

EtherCAT线程 ↓ 发生异常 ↓ 通知控制器 ↓ 停止相关线程 ↓ 记录日志 ↓ 关闭设备 ↓ 输出测试失败

这反而是比较正规的工程处理方式。


四、异常和RAII为什么经常一起出现?

这是 C++ 很重要的一组组合。

例如:

void test() { std::lock_guard<std::mutex> lock(mutex); do_something(); }

假设:

do_something();

突然:

throw ...

那么虽然函数提前退出:

throw ↓ 离开作用域 ↓ lock_guard析构 ↓ 自动unlock

所以不会因为异常导致锁永远不释放。

这就是:

RAII + 异常 = 异常安全

五、智能指针也是同样的道理

例如:

void test() { auto robot = std::make_unique<Robot>(); do_something(); }

如果:

do_something()

发生异常:

throw ↓ 离开test ↓ robot析构 ↓ Robot析构 ↓ 资源释放

所以现代 C++ 很重要的一种思想就是:

尽量让资源管理依赖对象生命周期,而不是依赖人工记忆。


六、这就是为什么你简历里的RAII非常重要

你的简历不是简单写了:

“会C++”。

而是明确写:

C++17 RAII 智能指针 多线程同步 生产者-消费者 有界队列

这些实际上是一个完整的现代 C++ 工程体系。

可以串成:

C++对象 ↓ 生命周期 ↓ RAII ↓ 智能指针 ↓ 资源自动释放 ↓ 异常安全 ↓ 多线程安全

七、再看线程退出

机器人程序最怕这种代码:

void worker() { while (true) { process(); } }

因为:

while(true)

意味着:

永远不退出。

如果主程序要关闭怎么办?

所以应该有:

std::atomic<bool> running{true};

线程:

void worker() { while (running) { process(); } }

停止:

running = false;

然后:

worker_thread.join();

八、但是你的项目还有一个问题

如果线程正在:

condition_variable.wait(...)

那么:

running = false;

可能还不够。

因为线程正在睡觉。

所以停止的时候:

running = false; cv.notify_all();

然后:

线程被唤醒 ↓ 检查running ↓ 发现false ↓ 退出 ↓ join

这就是正规的线程退出机制。


九、为什么你的“排空退出”很重要?

你的简历明确提到了:

排空退出。

这个词非常值得理解。

假设:

接收线程 ↓ Queue ↓ 处理线程

现在系统收到:

停止命令

不一定意味着:

马上把Queue全部丢掉

可能需要:

停止接收新数据 ↓ 允许已有数据继续处理 ↓ Queue逐渐变空 ↓ 处理线程退出 ↓ 系统关闭

就是:

停止生产 ↓ 消费剩余数据 ↓ Queue = empty ↓ 退出

这叫:

排空退出(drain shutdown)。


十、为什么机器人系统可能需要排空?

例如:

Frame 100 Frame 101 Frame 102 Frame 103

已经进入队列。

如果突然:

停止

直接:

clear(queue)

那么这些数据全部消失。

但有些场景需要:

停止接收新的Frame ↓ 把已有任务处理完 ↓ 再关闭

这取决于业务。


十一、有界队列再回来看

你简历还明确写了:

有界队列。

为什么不是普通:

std::queue

一直往里面塞?

因为:

生产速度 > 消费速度

就会:

Queue ↓ 越来越大 ↓ 延迟越来越高 ↓ 内存越来越多

实时机器人最危险的不是单纯“少处理一帧”。

而是:

机器人一直在处理很久以前的数据。

所以需要:

最大容量

例如:

Queue capacity = 10

超过以后按照系统策略:

丢旧

或者:

丢新

或者:

阻塞生产者

十二、背压是什么?

你简历里还有一个很重要的工程词:

背压。

假设:

Producer 50 frame/s

而:

Consumer 30 frame/s

如果没有限制:

Queue ↑ ↑ ↑ 越来越多

背压的意思就是:

下游处理不过来了,上游必须感知这个压力。

形成:

生产者 ↓ Queue ↓ 消费者 消费者处理不过来 ↓ Queue接近上限 ↓ 通知生产者 ↓ 限制/丢弃/阻塞

这就是实时系统里的一个核心思想。


十三、把你简历里的整套并发架构串起来

现在终于可以把你的简历原话完整理解了:

使用 C++17 拆分动捕接收、GMR 计算与动作发送线程,通过 RAII、智能指针、互斥锁、条件变量及有界队列实现背压、排空退出与异常联动停止。

翻译成人话就是:

动捕UDP ↓ 接收线程 Producer ↓ ┌─────────────┐ │ 有界Queue │ └──────┬──────┘ ↓ GMR计算线程 ↓ 动作数据 ↓ 发送线程 ↓ ZMQ/机器人

旁边还有:

mutex ↓ 保护Queue condition_variable ↓ 没数据就睡 RAII ↓ 资源自动释放 unique_ptr ↓ 明确所有权 atomic ↓ 停止控制 exception ↓ 异常传播 join ↓ 安全退出 bounded queue ↓ 限制积压 drain ↓ 处理剩余任务 backpressure ↓ 控制生产速度

这才是你简历这段话真正代表的技术能力。


十四、现在做一个小型“面试式检查”

你不用写代码,直接想答案。

问题1

为什么不推荐到处:

new delete

而推荐:

std::make_unique std::make_shared

问题2

为什么:

std::lock_guard

比:

mutex.lock(); ... mutex.unlock();

更安全?


问题3

为什么消费者线程不能:

while(queue.empty()) {}

一直检查?


问题4

为什么实时机器人不能让队列无限增长?


问题5

为什么线程停止时经常需要:

running = false; cv.notify_all(); thread.join();

问题6

为什么:

Adapter

和:

Strategy

不是一个东西?


如果你能用自己的话回答这 6 个问题,C++ 这一大块的核心工程思想就基本真正建立起来了。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询