C++多线程与零拷贝实战:从多媒体管线架构到锁的底层原理
C++多线程与零拷贝实战:从多媒体管线架构到锁的底层原理
🎯 本文适合:嵌入式 Linux 开发者、C++ 中高级学习者、准备嵌入式/C++ 方向秋招的同学。全文约 8000 字,建议收藏后慢慢消化。
一、前言:一帧数据,两个买家
想象一个经典场景:你正在做一台基于 RK3588 的智能摄像头。
摄像头每秒产生 30 帧 YUV 图像,这些图像需要同时做两件事:
- 送显:通过 DRM 接口把画面实时渲染到 HDMI 屏幕上;
- 编码:通过 MPP(Media Process Platform)硬件编码器压缩成 H.264,推流到网络。
如果用最笨的办法——memcpy 两份,那每帧 1080P YUV 图像就是 3MB,30 帧就是每秒 180MB 的纯内存拷贝。在嵌入式 ARM 平台上,这简直是 CPU 的噩梦。
有没有办法让一帧内存只存在一份,但 DRM 和 MPP 都能同时访问它?
答案是:零拷贝(Zero-Copy),靠 V4L2 导出 DMA-BUF fd 实现。
而当多个消费者"抢"同一帧数据时,多线程同步就成了绕不开的核心问题。
本文就从这个实战场景出发,带你从系统架构一路讲到 C++ 锁的底层指令。
二、多媒体管线零拷贝架构设计
2.1 整体架构图
┌──────────────┐
│ V4L2 Camera │ (用户态)
│ /dev/videoX│
└──────┬───────┘
│ VIDIOC_EXPBUF → DMA-BUF fd
▼
┌──────────────────────────────────────────┐
│ DMA-BUF Buffer Pool │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ buf[0] │ │ buf[1] │ │ buf[2] │ ... │
│ └───┬────┘ └───┬────┘ └───┬────┘ │
└──────┼──────────┼──────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌─────────────┐
│ DRM/KMS │ │ MPP Encoder│
│ (送显渲染) │ │ (H.264编码) │
│ fd → fb │ │ fd → input │
└────────────┘ └─────────────┘
核心思想:同一块物理内存,只通过 fd(文件描述符)传递,不做任何数据拷贝。
2.2 V4L2 导出 DMA-BUF 的关键代码
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <fcntl.h>
struct BufferInfo {
int fd; // DMA-BUF 文件描述符
void* start; // mmap 映射地址(可选)
size_t length; // 缓冲区大小
};
/**
* @brief 从 V4L2 设备导出 DMA-BUF fd
* @param v4l2_fd V4L2 设备文件描述符
* @param index 缓冲区索引
* @return BufferInfo 包含 fd 和长度
*/
BufferInfo export_dmabuf(int v4l2_fd, uint32_t index) {
// 关键结构体:v4l2_exportbuffer
struct v4l2_exportbuffer expbuf = {};
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
expbuf.index = index;
expbuf.flags = O_CLOEXEC | O_RDWR; // CLOEXEC 防止子进程继承
expbuf.plane = 0;
// VIDIOC_EXPBUF:让内核把这块 buffer 的 DMA-BUF fd 导出到用户态
if (ioctl(v4l2_fd, VIDIOC_EXPBUF, &expbuf) < 0) {
perror("VIDIOC_EXPBUF failed");
return {-1, nullptr, 0};
}
printf("[V4L2] 成功导出 DMA-BUF fd = %d\n", expbuf.fd);
return {expbuf.fd, nullptr, 0};
}
💡 什么是 DMA-BUF?
DMA-BUF 是 Linux 内核提供的跨设备共享内存框架。它本质上是一块由某个设备(如 ISP、GPU)分配的物理内存,但可以被多个设备通过 fd 引用。内核保证所有设备看到的是同一块物理页面,这就是零拷贝的底层基础。
2.3 DRM 和 MPP 同时消费同一帧
#include <xf86drm.h>
#include <rockchip/mpp.h>
/**
* @brief 将同一个 DMA-BUF fd 分发给 DRM 和 MPP
* @param dmabuf_fd V4L2 导出的 DMA-BUF fd
*/
void dispatch_to_consumers(int dmabuf_fd) {
// ====== 消费者 1:DRM 送显 ======
// 将 DMA-BUF fd 导入为 DRM framebuffer
// DRM_PRIME_HANDLE_TO_FD / DRM_IOCTL_MODE_GETFB2 等 ioctl 完成
struct drm_mode_map_dumb map_req = {};
// 实际项目中用 drmModeAddFB2() 把 dmabuf_fd 传给 DRM 子系统
// DRM 会直接引用这块物理内存作为显示缓冲区,不做拷贝
printf("[DRM] 将 fd=%d 绑定到 framebuffer\n", dmabuf_fd);
// ====== 消费者 2:MPP 编码 ======
// 将 DMA-BUF fd 设置为 MPP 编码器的输入
// MppBuffer 包装 fd,编码器直接从这块内存读取 YUV 数据
// 实际项目中:
// MppBuffer mpp_buf = nullptr;
// mpp_buffer_import(&mpp_buf, dmabuf_fd);
// mpp_frame_set_buffer(frame, mpp_buf);
printf("[MPP] 将 fd=%d 绑定到编码器输入\n", dmabuf_fd);
}
关键点:fd 可以被多个消费者同时持有,底层物理内存只有一份。 DRM 通过 KMS 扫描它送显,MPP 通过硬件 DMA 读它编码,互不干扰。
🔥 面试考点:为什么是 fd 而不是指针?
因为 DMA-BUF 是内核对象,用户态不能直接操作物理地址。fd 是内核对象在用户态的"代表",通过 ioctl 传给不同子系统(DRM、MPP、GPU),每个子系统在内核态把 fd 转换成自己的 buffer handle。这是 Linux “一切皆文件” 哲学的经典体现。
三、多消费者并发控制:生产者-消费者模型
3.1 为什么需要多线程同步?
上面的架构看起来很美好,但实际运行时会出现一个经典问题:
- V4L2 产生帧的速度(生产者)是固定的,比如 30fps;
- DRM 送显和 MPP 编码的速度可能不同——编码器处理慢帧时会卡住;
- 如果用单线程串行处理,编码器慢了就会拖慢整个流水线。
所以我们需要一个多消费者异步模型:
V4L2 (生产者)
│
▼
┌─────────────────────┐
│ 共享 Buffer 队列 │ ← 需要锁保护!
│ std::queue<Buffer> │
└──┬──────────┬────────┘
│ │
▼ ▼
DRM Thread MPP Thread ← 各自独立消费,互不阻塞
3.2 完整的生产者-消费者实现
下面给出一份可以直接用于项目中的现代 C++ 实现:
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <thread>
#include <vector>
#include <cstdio>
#include <functional>
// ============================================================
// 共享 Buffer 队列(线程安全)
// ============================================================
template<typename T>
class SharedBufferQueue {
public:
explicit SharedBufferQueue(size_t max_size)
: max_size_(max_size), stopped_(false) {}
/**
* @brief 生产者:向队列投递一帧 Buffer
* @param item 要投递的 Buffer(使用移动语义,避免拷贝)
* @return true 投递成功,false 队列已停止
*
* 【面试考点】为什么用 std::unique_lock 而不是 std::lock_guard?
* 因为 wait() 需要在等待时**临时释放锁**,lock_guard 不支持中途 unlock。
*/
bool push(T&& item) {
std::unique_lock<std::mutex> lock(mutex_);
// 【面试考点:虚假唤醒】
// wait 的第二个参数(Lambda)是"谓词",用来防止虚假唤醒。
// 内核可能在没有 notify 的情况下唤醒线程(调度抖动、信号中断等),
// 如果只用 if 判断,虚假唤醒后会直接往下走,导致在队列满时继续 push。
// 用 while 或 Lambda 谓词,唤醒后会**再次检查条件**,保证安全。
not_full_.wait(lock, [this]() {
return queue_.size() < max_size_ || stopped_;
});
if (stopped_) return false;
queue_.push(std::move(item));
// 通知一个等待中的消费者:"有新数据了"
not_empty_.notify_one();
return true;
}
/**
* @brief 消费者:从队列取走一帧 Buffer
* @param item 输出参数,取到的 Buffer
* @return true 取出成功,false 队列已停止且为空
*/
bool pop(T& item) {
std::unique_lock<std::mutex> lock(mutex_);
// 同样用 Lambda 谓词防止虚假唤醒
not_empty_.wait(lock, [this]() {
return !queue_.empty() || stopped_;
});
if (queue_.empty()) return false; // stopped_ 且空
item = std::move(queue_.front());
queue_.pop();
// 通知生产者:"有空位了"
not_full_.notify_one();
return true;
}
/**
* @brief 优雅停止:唤醒所有阻塞的线程
*/
void stop() {
{
std::lock_guard<std::mutex> lock(mutex_); // 短临界区,用 lock_guard 足够
stopped_ = true;
}
not_full_.notify_all(); // 唤醒所有等待生产者
not_empty_.notify_all(); // 唤醒所有等待消费者
}
private:
std::queue<T> queue_;
size_t max_size_;
std::mutex mutex_;
std::condition_variable not_full_; // 队列未满,生产者可写
std::condition_variable not_empty_; // 队列非空,消费者可读
bool stopped_;
};
// ============================================================
// 业务层:一帧的 DMA-BUF 信息
// ============================================================
struct FrameBuffer {
int dmabuf_fd; // DMA-BUF 文件描述符
size_t size; // 数据大小
uint64_t pts; // 时间戳
};
// ============================================================
// 生产者线程:V4L2 采集
// ============================================================
void v4l2_producer(SharedBufferQueue<FrameBuffer>& queue, int v4l2_fd) {
printf("[Producer] V4L2 采集线程启动\n");
uint64_t pts = 0;
while (true) {
// 模拟 V4L2 DQBUF:从内核取一帧
// int dmabuf_fd = v4l2_dequeue_buffer(v4l2_fd);
int dmabuf_fd = pts; // 模拟
if (dmabuf_fd < 0) break;
FrameBuffer fb{dmabuf_fd, 1920 * 1080 * 3 / 2, pts++};
// push 进队列,DRM 和 MPP 的消费者线程会异步取走
if (!queue.push(std::move(fb))) {
printf("[Producer] 队列已停止,退出\n");
break;
}
}
}
// ============================================================
// 消费者线程:DRM 送显 / MPP 编码
// ============================================================
void consumer_thread(SharedBufferQueue<FrameBuffer>& queue,
const char* name,
std::function<void(const FrameBuffer&)> process_fn) {
printf("[%s] 消费者线程启动\n", name);
FrameBuffer fb;
while (queue.pop(fb)) {
// 调用具体的处理函数(DRM 送显 或 MPP 编码)
process_fn(fb);
printf("[%s] 处理帧 pts=%lu, fd=%d\n", name, fb.pts, fb.dmabuf_fd);
}
printf("[%s] 消费者线程退出\n", name);
}
// ============================================================
// 主函数:组装流水线
// ============================================================
int main() {
const size_t QUEUE_CAPACITY = 5;
SharedBufferQueue<FrameBuffer> queue(QUEUE_CAPACITY);
// 启动生产者
std::thread producer(v4l2_producer, std::ref(queue), /*v4l2_fd=*/3);
// 启动两个消费者:DRM 和 MPP
std::thread drm_consumer(consumer_thread, std::ref(queue), "DRM",
[](const FrameBuffer& fb) {
// 实际调用:drmModeSetCrtc(fd_to_fb(fb.dmabuf_fd));
printf(" → [DRM] 送显 fd=%d\n", fb.dmabuf_fd);
});
std::thread mpp_consumer(consumer_thread, std::ref(queue), "MPP",
[](const FrameBuffer& fb) {
// 实际调用:mpp_encode_put_frame(fb);
printf(" → [MPP] 编码 fd=%d\n", fb.dmabuf_fd);
});
// 模拟采集 20 帧后停止
std::this_thread::sleep_for(std::chrono::seconds(2));
queue.stop();
producer.join();
drm_consumer.join();
mpp_consumer.join();
printf("[Main] 流水线优雅退出\n");
return 0;
}
四、锁的深度对比:lock_guard vs unique_lock
4.1 std::lock_guard:轻量级"铁锁"
{
std::lock_guard<std::mutex> lock(mutex_);
// 临界区操作
do_something();
} // 出作用域自动 unlock,RAII 保证
特点:
- 构造时
lock(),析构时unlock(),不能中途手动操作; - 开销极小(几乎没有额外开销);
- 适用于临界区短、不需要 wait 的场景。
4.2 std::unique_lock:灵活的"智能锁"
{
std::unique_lock<std::mutex> lock(mutex_);
do_part1();
lock.unlock(); // 可以中途手动 unlock!
do_something_slow(); // 这段不在锁保护内
lock.lock(); // 再加锁
do_part2();
} // 析构时自动 unlock(如果还持有锁的话)
特点:
- 支持
lock()/unlock()/try_lock()/release()等灵活操作; - 可以配合
condition_variable::wait()使用(这是唯一选择); - 开销比
lock_guard略大(内部要维护一个"是否持有锁"的状态标志)。
4.3 为什么 condition_variable::wait() 必须用 unique_lock?
// condition_variable::wait 的内部实现伪代码:
void wait(unique_lock<mutex>& lock) {
// 1. 原子操作:释放锁 + 让当前线程进入等待队列
lock.unlock(); // ← 需要调用 unlock()!
// 2. 线程挂起(futex / pthread_cond_wait)
suspend_thread();
// 3. 被唤醒后,重新获取锁
lock.lock(); // ← 需要调用 lock()!
}
lock_guard 没有 unlock() 和 lock() 方法,所以根本编译不过!
🔥 面试考点:一句话总结
lock_guard是"买了就用、用完就扔"的一次性打火机;unique_lock是"可以反复开关"的 Zippo 打火机。需要配合条件变量(wait/notify)时,只能用unique_lock。
五、八股硬核:虚假唤醒与锁的底层原理
5.1 虚假唤醒(Spurious Wakeup)到底是什么?
现象: 一个线程调用了 cond_var.wait(),明明没有人在 notify,它却醒了。
原因(来自内核层面):
- 信号中断:线程在
futex_wait中被信号打断,内核返回EINTR,glibc/pthread 重新获取锁后返回; - 实现自由度:POSIX 标准明确说了——允许实现在线程没有被
notify的情况下唤醒它,这是为了简化某些内核实现(如某些 RTOS); - 多消费者竞争:有多个消费者都在
wait,notify_one唤醒了一个,但那个消费者发现条件不满足(被别人抢先了),于是放弃了——对其他消费者来说就是"白醒了一次"。
错误写法:
// ❌ 危险!虚假唤醒会导致在队列为空时执行 pop,直接 crash
not_empty_.wait(lock);
T item = queue.front(); // 可能在虚假唤醒后 queue 为空,UB!
queue.pop();
正确写法:
// ✅ 用 Lambda 谓词,唤醒后必须再次检查条件
not_empty_.wait(lock, [&]() { return !queue_.empty(); });
// 只有 queue_ 确实非空时,wait 才会真正返回
T item = std::move(queue_.front());
queue_.pop();
🔥 面试高频题:为什么条件变量必须配合
while循环(或 Lambda 谓词)?三个原因:
- 防止虚假唤醒:内核可能在没有 notify 的情况下唤醒线程;
- 防止多个消费者竞争:
notify_one唤醒了一个消费者,但它醒来发现条件被别人抢先满足了,需要重新等待;- 防御性编程:即使你的代码里只有一个消费者,未来维护者可能加入新的消费者,
while循环保证代码永远正确。
5.2 互斥锁 vs 自旋锁:上厕所排队的比喻
这是面试中出现频率最高的锁相关问题,我用一个"上厕所"的比喻来帮你彻底理解。
场景设定
公司只有一个厕所(共享资源),10 个员工(线程)要上厕所。
互斥锁(Mutex)—— “回工位等”
员工 A 到厕所门口 → 发现有人 → 回工位坐下 → 等通知再来
↑
【上下文切换:CPU 让给别的线程】
实现原理:
- 线程发现锁被占用,调用
futex(FUTEX_WAIT)陷入内核态; - 内核将线程状态改为
TASK_INTERRUPTIBLE,挂到等待队列; - CPU 被调度给其他线程(发生了上下文切换,代价约 1~10μs);
- 持有锁的线程释放时,调用
futex(FUTEX_WAKE)唤醒等待者。
特点: 适合临界区执行时间长(> 几微秒)的场景。
自旋锁(Spinlock)—— “在门口死等”
员工 B 到厕所门口 → 发现有人 → 就站在门口不停地问"好了吗好了吗好了吗"
↑
【不切换 CPU,原地循环】
实现原理:
// 自旋锁的底层实现(简化版)
void spin_lock(std::atomic<int>& lock) {
while (lock.exchange(1, std::memory_order_acquire) != 0) {
// CPU 空转,不断尝试读取锁变量
// 在 ARM 上就是 LDREX/STREX 指令对(后面会讲)
__asm__ volatile("yield"); // ARM 提示 CPU 让出资源
}
}
特点: 适合临界区非常短(< 几微秒)的场景,避免上下文切换的开销。
对比表格
| 特性 | 互斥锁 (Mutex) | 自旋锁 (Spinlock) |
|---|---|---|
| 等待方式 | 线程挂起,CPU 调度给他人 | CPU 空转,不断重试 |
| 是否陷入内核 | ✅ 是(futex syscall) |
❌ 否(用户态循环) |
| 上下文切换 | ✅ 有(代价 1~10μs) | ❌ 无 |
| 适用临界区 | 长(> 几微秒) | 短(< 几微秒) |
| 实时性 | 较低(唤醒有延迟) | 高(立即可用) |
| 嵌入式典型场景 | 多媒体 Buffer 队列操作 | 中断处理(spin_lock_irqsave) |
🔥 面试追问:为什么内核中断上下文不能用互斥锁?
因为中断上下文不能睡眠!中断处理函数运行在中断上下文中,如果调用
mutex_lock(),锁被占用时线程会挂起睡眠,而中断上下文是不允许睡眠的(会导致内核 panic)。所以内核中断里只能用自旋锁(spin_lock_irqsave)。
5.3 std::atomic 无锁编程与 ARM 底层指令
std::atomic 是 C++11 提供的原子操作类型,它不依赖互斥锁,而是利用硬件指令保证操作的原子性。
在 x86 上:LOCK 前缀指令
; x86 原子自增
lock xadd eax, [counter] ; LOCK 前缀锁住总线/缓存行
在 ARM(如 RK3588)上:LDREX/STREX 指令对
; ARM 原子自增(LL/SC 模式:Load-Linked / Store-Conditional)
retry:
LDREX r1, [r0] ; 读取 counter 到 r1,并标记为"独占"
ADD r1, r1, #1 ; r1 = r1 + 1
STREX r2, r1, [r0] ; 尝试写回,r2=0 表示成功,r2=1 表示失败
CMP r2, #0
BNE retry ; 失败则重试(可能被其他核心抢占了)
核心思想:
LDREX(Load Exclusive):读取内存并标记这块地址为"独占访问";STREX(Store Exclusive):尝试写入,但如果其他核心在这期间修改了这块内存,写入会失败(返回 1),需要重试;- 这就是所谓的 CAS(Compare-And-Swap) 思想在 ARM 上的实现。
#include <atomic>
#include <thread>
#include <cstdio>
std::atomic<int> frame_counter{0};
// 两个消费者线程同时递增,不需要锁
void count_frames(const char* name, int count) {
for (int i = 0; i < count; i++) {
// fetch_add 底层就是 LDREX + ADD + STREX 循环
int old_val = frame_counter.fetch_add(1, std::memory_order_relaxed);
(void)old_val;
}
}
int main() {
std::thread t1(count_frames, "Consumer-A", 100000);
std::thread t2(count_frames, "Consumer-B", 100000);
t1.join();
t2.join();
// 结果一定是 200000,不会出现数据竞争
printf("Total frames: %d\n", frame_counter.load());
return 0;
}
🔥 面试追问:
memory_order_relaxed/acquire/release有什么区别?
relaxed:只保证原子性,不保证顺序,性能最高(适合计数器);acquire:本线程中,读操作不会被重排到这个原子操作之前(适合"获取锁"语义);release:本线程中,写操作不会被重排到这个原子操作之后(适合"释放锁"语义);- 大多数场景用默认的
seq_cst(顺序一致性)就够了,不要过早优化。
六、Lambda 表达式的降维打击
6.1 破除误区:Lambda 不只是"省了写函数名"
很多初学者觉得 Lambda 只是语法糖:
// "Lambda 不就是省了写函数名嘛"
auto add = [](int a, int b) { return a + b; };
// 等价于
int add(int a, int b) { return a + b; }
大错特错。 Lambda 的真正威力在于闭包(Closure)——它能捕获外部作用域的变量,把"上下文"打包进函数对象里。
6.2 闭包的本质:一个"记住环境"的函数对象
// 普通函数:无状态,无法访问外部变量
int normal_func(int x) { return x * 2; }
// 闭包:有状态,捕获了外部变量 factor
int factor = 10;
auto closure = [factor](int x) { return x * factor; };
// 编译器生成的等价代码(伪码):
struct __Lambda_factor_10 {
int factor; // 捕获的变量被"存储"在对象内部
int operator()(int x) const { return x * factor; }
};
auto closure = __Lambda_factor_10{factor};
闭包 = 函数 + 环境。 它"记住"了创建时的上下文。
6.3 捕获列表的"黑魔法"
int a = 1;
std::string name = "camera";
// [a] 值捕获:拷贝一份 a 的值,闭包内修改不影响外部
auto by_val = [a]() { printf("a = %d\n", a); };
// [&] 引用捕获:直接引用外部变量,闭包内修改会影响外部
auto by_ref = [&]() {
a = 42; // 外部的 a 也会变成 42
name = "encoder"; // 外部的 name 也会改变
};
// [this] 捕获当前对象的 this 指针(在类成员函数中使用 Lambda 时常用)
// [=] 值捕获所有外部变量
// [&] 引用捕获所有外部变量
捕获列表实战技巧
// ✅ 推荐:显式捕获,明确依赖关系
std::thread t([this, &queue, encoder_id = id_]() {
// 捕获 this(成员函数访问)、queue(引用)、encoder_id(移动语义值)
this->encode_loop(queue, encoder_id);
});
// ❌ 不推荐:隐式捕获 [&] 或 [=],容易出现悬垂引用
std::function<void()> create_callback() {
int local_var = 42;
return [&]() { printf("%d\n", local_var); }; // ⚠️ local_var 已经销毁!
}
🔥 面试考点:Lambda 在多线程中的价值
- 局部性:逻辑写在使用处,不用跳到文件末尾找函数定义;
- 自动类型推导:不需要写
std::function<void(int, int)>这种冗长类型;- 捕获上下文:可以直接访问当前作用域的变量,省去大量参数传递;
- 配合 STL 算法和线程:
std::thread、std::async、std::for_each的最佳搭档。
6.4 Lambda 在多媒体管线中的实战
// 为每个消费者线程创建独立的处理函数,闭包捕获各自的配置
auto create_consumer(SharedBufferQueue<FrameBuffer>& queue,
const std::string& name,
int output_width,
int output_height) {
// Lambda 捕获了 queue(引用)、name(值)、宽高(值)
return [&queue, name, output_width, output_height]() {
FrameBuffer fb;
while (queue.pop(fb)) {
printf("[%s] 处理 %dx%d, pts=%lu\n",
name.c_str(), output_width, output_height, fb.pts);
}
printf("[%s] 退出\n", name.c_str());
};
}
// 使用
int main() {
SharedBufferQueue<FrameBuffer> queue(5);
// 每个消费者有不同的输出分辨率,Lambda 帮我们"记住"了这些配置
std::thread t1(create_consumer(queue, "DRM-1080P", 1920, 1080));
std::thread t2(create_consumer(queue, "MPP-720P", 1280, 720));
// ...
}
七、技术总结
本文从一个真实的嵌入式多媒体场景出发,串联了以下核心知识点:
| 知识点 | 核心要点 |
|---|---|
| DMA-BUF 零拷贝 | V4L2 导出 fd → DRM/MPP 共享同一物理内存,避免 memcpy |
| 生产者-消费者模型 | std::queue + std::mutex + std::condition_variable 三件套 |
lock_guard vs unique_lock |
需要 wait 时必须用 unique_lock,短临界区用 lock_guard |
| 虚假唤醒 | 条件变量 wait 必须用 while/Lambda 谓词,不能裸 wait |
| Mutex vs Spinlock | Mutex 挂起等(适合长临界区),Spinlock 空转等(适合短临界区) |
std::atomic 底层 |
ARM 用 LDREX/STREX,x86 用 LOCK XADD,硬件保证原子性 |
| Lambda 闭包 | 不是语法糖,是"函数+环境"的打包,多线程传参的利器 |
一句话总结: 在嵌入式 Linux 多媒体开发中,零拷贝解决性能问题,多线程解决并发问题,锁解决同步问题,Lambda 解决代码组织问题。这四个知识点环环相扣,缺一不可。
📖 如果觉得本文对你有帮助,请点赞、收藏、关注三连!后续会持续更新嵌入式 Linux + 现代 C++ 系列文章。
📧 有问题欢迎评论区讨论,我会逐一回复。
更多推荐
所有评论(0)