文章目录
-
- 一、原子操作的内部屏障
- 二、虚函数表实现原理
-
- [2.1 为什么需要虚函数?](#2.1 为什么需要虚函数?)
- [2.2 虚函数表是什么?](#2.2 虚函数表是什么?)
- [2.3 虚函数表的开销?](#2.3 虚函数表的开销?)
- [2.4 析构函数为什么经常写virtual?](#2.4 析构函数为什么经常写virtual?)
- 三、智能指针使用场景和内存管理
- 四、多线程同步机制
-
- [4.1 mutex互斥锁](#4.1 mutex互斥锁)
- [4.2 atomic原子操作](#4.2 atomic原子操作)
- [4.3 生产者、消费者模式](#4.3 生产者、消费者模式)
- [4.4 semaphore信号量](#4.4 semaphore信号量)
一、原子操作的内部屏障
1、原子操作的功能
(1)保证这个变量的读写不可被打断;
(2)保证操作前后的内存访问顺序不会被编译器或CPU重排序;
2、原子内部屏障
(1)CAS(无锁同步机制,通过比较内存中的值是否等于期望值,如果相等则更新为新值,否则失败并重试)
(2)memory-barrier(内存屏障 )防止CPU和编译器为了优化,把代码执行顺序乱掉
release/acquire通常通过memory barrier实现
acquire和release
| Acquire | Release |
|---|---|
| 保证当前线程读到的数据是最新的 | 保证本线程对数据的修改是其他线程可见的 |
cpp
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> ready{false};
int data = 0;
void producer() {
data = 42; // 步骤 1: 准备数据
// Release: 保证 data = 42 绝对不会被重排到这句代码的【后面】
ready.store(true, std::memory_order_release);
}
void consumer() {
// Acquire: 保证读到 true 之后,后续的代码绝对不会被重排到这句代码的【前面】
while (!ready.load(std::memory_order_acquire)) {
// 等待
}
// 步骤 3: 读取数据
// 绝对安全!因为 Acquire 和 Release 配对,
// producer 中在 release 之前的所有修改(data=42),
// 在 consumer 中 acquire 之后必然可见!
std::cout << data << std::endl; // 永远输出 42
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
二、虚函数表实现原理
2.1 为什么需要虚函数?
C++默认函数调用都是静态绑定:
cpp
class Base {
public:
void func(){
cout<<"Base";
}
};
class Derived: public Base {
public:
void func(){
cout<<"Derived";
}
};
Base* ptr = new Derived();
ptr->func();
输出:Base
因为在编译阶段就已经确定调用了Base::func
如果希望:利用基类Base类型指针调用派生类func,则需要虚函数!
cpp
class Base{
public:
virtual void func(){
cout<<"Base";
}
};
这就是多态!
2.2 虚函数表是什么?
当一个类包含虚函数时,编译器会给这个类增加一个隐藏成员:虚函数表;
调用过程:
cpp
Base* p = new Derived();
p->func();
- 根据p找到对象
- 找到对象里的vptr
- 根据vptr找到vtable
- 找到对应函数地址
- 调用派生类Derived::func()
通过虚函数表记录,实现动态绑定;
2.3 虚函数表的开销?
2个:
(1)对象增加一个指针:vptr(8字节)
(2)多一次间接寻址:加载 vptr -> 加载 func address -> 调用
2.4 析构函数为什么经常写virtual?
普通的析构函数,在通过基类ptr执行析构函数时,不会将其派生类对象析构掉,会造成:内存泄露。
回答总结:
- C++虚函数通过vptr和vtable实现运行时多态。包含虚函数的对象内部维护一个虚函数表指针,指向存储虚函数地址的表。
- 当通过基类指针调用虚函数时,会根据vptr查找到其真实函数的地址,从而调用派生类实现;
三、智能指针使用场景和内存管理
普通指针会存在忘记delete的问题,所以利用智能指针方便管理(直接自动管理)
| 类型 | 特点 | 场景 |
|---|---|---|
| unique_ptr | 唯一所有权 | 资源管理 :谁创建谁释放 |
| shared_ptr | 共享所有权 | 多个模块共享,存在问题:多线程同步 |
| weak_ptr | 观察,不拥有 | 解决循环引用 |
四、多线程同步机制
多个线程同时访问共享资源导致数据竞争
重量级方式:
4.1 mutex互斥锁
cpp
std::mutex m;
m.lock();
count++;
m.unlock();
4.2 atomic原子操作
针对简单变量:
cpp
std::atomic<int> count;
count++;
内部支持CAS
读取旧值
比较是否改变
如果没有改变
更新
否则重试
4.3 生产者、消费者模式
消费者:没有数据就wait()
生产者:开始生产就notify()
4.4 semaphore信号量
控制资源数量
例如:GPU任务队列
acquire()获取
执行任务
release()释放