音视频开发实战
附录 A:C++11 线程速成
写给 Ch2 异步播放器读者的前置知识补充。
如果你不熟悉 C++11 的多线程编程,这份附录会帮你快速掌握 Ch2 所需的基础知识。
1. 为什么需要多线程?
单线程的问题:
// 单线程:解码和渲染串行执行
while (true) {
decode_frame(); // 耗时 10ms
render_frame(); // 耗时 5ms
// 总耗时:15ms/帧,最高 66fps
}多线程的解决:
// 线程A:专门解码
void decode_thread() {
while (true) {
decode_frame();
queue.push(frame); // 放入队列
}
}
// 线程B:专门渲染
void render_thread() {
while (true) {
frame = queue.pop(); // 从队列取
render_frame();
}
}2. 创建线程
#include <thread>
#include <iostream>
void hello() {
std::cout << "Hello from thread\n";
}
int main() {
// 创建线程
std::thread t(hello);
// 等待线程结束
t.join();
std::cout << "Main thread done\n";
return 0;
}关键点: - std::thread 创建新线程 -
join() 等待线程完成(主线程会阻塞在这里) -
detach() 让线程独立运行(主线程不等待)
3. 传递参数
void print_number(int n, const std::string& label) {
std::cout << label << ": " << n << "\n";
}
int main() {
// 传递参数给线程函数
std::thread t(print_number, 42, "The answer");
t.join();
return 0;
}4. 互斥锁(Mutex)
问题:多线程访问共享数据会冲突
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
counter++; // 危险!多个线程同时修改
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
// counter 可能不是 2000!
std::cout << counter << "\n";
return 0;
}解决:使用互斥锁
#include <mutex>
int counter = 0;
std::mutex mtx; // 互斥锁
void increment() {
for (int i = 0; i < 1000; i++) {
mtx.lock(); // 加锁
counter++; // 临界区:只有一个线程能执行
mtx.unlock(); // 解锁
}
}更好的方式:lock_guard(自动解锁)
void increment() {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁
counter++; // 临界区
// 析构时自动解锁
}
}为什么用 lock_guard? - 自动管理锁的生命周期 - 避免忘记 unlock - 异常安全(抛出异常也会解锁)
5. 条件变量(Condition Variable)
场景:线程等待某个条件
#include <condition_variable>
std::queue<int> queue;
std::mutex mtx;
std::condition_variable cv;
// 生产者线程
void producer() {
for (int i = 0; i < 10; i++) {
std::unique_lock<std::mutex> lock(mtx);
queue.push(i);
std::cout << "Produced: " << i << "\n";
lock.unlock();
cv.notify_one(); // 通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// 消费者线程
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待队列非空
cv.wait(lock, [] { return !queue.empty(); });
int value = queue.front();
queue.pop();
lock.unlock();
std::cout << "Consumed: " << value << "\n";
if (value == 9) break;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}关键点: -
cv.wait(lock, predicate):自动解锁等待,条件满足时加锁返回
- cv.notify_one():唤醒一个等待的线程 -
cv.notify_all():唤醒所有等待的线程
6. Ch2 中的 FrameQueue 简化解说
class FrameQueue {
public:
void Push(AVFrame* frame) {
std::unique_lock<std::mutex> lock(mutex_);
// 等待队列不满
not_full_.wait(lock, [this] { return queue_.size() < max_size_; });
queue_.push(frame);
not_empty_.notify_one(); // 通知消费者
}
AVFrame* Pop() {
std::unique_lock<std::mutex> lock(mutex_);
// 等待队列非空
not_empty_.wait(lock, [this] { return !queue_.empty(); });
AVFrame* frame = queue_.front();
queue_.pop();
not_full_.notify_one(); // 通知生产者
return frame;
}
private:
std::queue<AVFrame*> queue_;
std::mutex mutex_;
std::condition_variable not_empty_;
std::condition_variable not_full_;
size_t max_size_ = 3;
};核心逻辑: 1.
Push:队列满时等待(not_full_),放入数据后通知消费者(not_empty_)
2.
Pop:队列空时等待(not_empty_),取出数据后通知生产者(not_full_)
这就是生产者-消费者模式。
7. 常见问题
Q1: 什么是线程安全?
线程安全:多个线程同时访问时,结果正确的代码。
例子:
// 线程不安全
void unsafe_increment() {
counter++; // 实际上是:读-修改-写,三步操作
}
// 线程安全
void safe_increment() {
std::lock_guard<std::mutex> lock(mtx);
counter++;
}Q2: 死锁是什么?
死锁:两个线程互相等待对方释放锁。
// 线程A:先锁mtx1,再锁mtx2
// 线程B:先锁mtx2,再锁mtx1
// 结果:A等B释放mtx2,B等A释放mtx1,永远等待
// 解决:始终按相同顺序加锁Q3: 为什么用 lambda?
// 传统写法:定义一个函数
bool is_not_empty() { return !queue.empty(); }
cv.wait(lock, is_not_empty);
// Lambda 写法:内联定义
cv.wait(lock, [] { return !queue.empty(); });
// ↑ lambda 表达式Lambda 让你可以在代码中直接写小函数,不用到处定义。
8. 总结
| 概念 | 作用 | Ch2 中哪里用 |
|---|---|---|
std::thread |
创建线程 | DecoderThread |
std::mutex |
保护共享数据 | FrameQueue 的锁 |
std::lock_guard |
自动管理锁 | Push/Pop
函数 |
std::condition_variable |
线程间等待/通知 | 队列满/空的等待 |
学完本节,你应该能理解: - Ch2 为什么要用多线程 -
FrameQueue 的工作原理 - 为什么需要锁和条件变量
如果还有疑问,建议先写几个简单的多线程程序练习,再回来看 Ch2。