欢迎关注本公众号,专注面试题拆解
分享一套视频课程《C++百万并发服务器开发》,有需要的加我微信获取:fb964919126
面试题:乐观锁和悲观锁
在并发编程中,锁机制是保证线程安全的重要手段。根据锁的实现策略,我们通常将其分为乐观锁和悲观锁。
一、基本概念对比
悲观锁的核心思想
悲观锁采用"先锁后访问"的策略,它假设最坏的情况,认为每次访问共享资源时都会发生冲突。在访问共享资源前,必须先对资源进行加锁,确保在访问期间资源被锁定。这种方式类似于图书馆借书时,一本书被借走后,其他人就无法借阅该书,必须等待书被归还。
乐观锁的核心思想
乐观锁采用"先访问后校验"的策略,它假设最好的情况,认为并发访问共享资源时不会发生冲突。在访问共享资源时不加锁,而是在更新资源时检查是否有其他线程修改了该资源。这种方式类似于在线文档协同编辑,多人可以同时查看和编辑文档,但在保存时会检查是否存在冲突。
二、悲观锁的典型实现:pthread_mutex_t
// pthread_mutex 的核心数据结构
struct pthread_mutex_t {
// 互斥锁的类型
int type;
// 递归锁的递归计数
unsigned int count;
// 持有锁的线程ID
pthread_t owner;
// 原子变量,用于自旋
atomic_int lock;
// 条件变量,用于线程阻塞和唤醒
pthread_cond_t cond;
};
// mutex 加锁的核心实现
int pthread_mutex_lock(pthread_mutex_t *mutex) {
// 快速路径:尝试直接获取锁
if (atomic_exchange(&mutex->lock, 1) == 0) {
mutex->owner = pthread_self();
return 0;
}
// 慢路径:需要等待
while (1) {
// 自旋一段时间
for (int i = 0; i < SPIN_COUNT; ++i) {
if (atomic_load(&mutex->lock) == 0 &&
atomic_exchange(&mutex->lock, 1) == 0) {
mutex->owner = pthread_self();
return 0;
}
// CPU让出时间片
cpu_relax();
}
// 自旋失败,进入睡眠等待
futex_wait(&mutex->lock, 1);
}
return 0;
}
从 Linux 2.6.25 开始,内核引入了 "自适应自旋" 机制:在进入完全阻塞前,先进行一定次数的自旋,以处理短期的锁竞争。
悲观锁的实现主要包含以下几个关键点:
原子操作保护:使用atomic_exchange确保锁的获取是原子的,避免竞态条件。
自旋等待:在进入完全阻塞前,先进行一定次数的自旋,以处理短期的锁竞争。
线程阻塞:当自旋等待失败后,通过futex系统调用使线程进入睡眠状态,避免消耗CPU资源。
三、乐观锁的典型实现:std::atomic
template<typename T>
class atomic {
private:
T value;
public:
bool compare_exchange_strong(T& expected, T desired) noexcept {
// 使用CPU的原子指令实现CAS操作
return __atomic_compare_exchange_n(
&value, // 原子变量的地址
&expected, // 期望值
desired, // 新值
false, // weak版本?
__ATOMIC_SEQ_CST, // 成功时的内存序
__ATOMIC_SEQ_CST // 失败时的内存序
);
}
bool compare_exchange_weak(T& expected, T desired) noexcept {
return __atomic_compare_exchange_n(
&value,
&expected,
desired,
true, // weak版本
__ATOMIC_SEQ_CST,
__ATOMIC_SEQ_CST
);
}
};
// 使用示例:实现一个乐观锁保护的计数器
class OptimisticCounter {
private:
std::atomic<int> value{0};
public:
void increment() {
int expected, desired;
do {
expected = value.load();
desired = expected + 1;
} while (!value.compare_exchange_strong(expected, desired));
}
};
乐观锁实现的关键点:
CAS操作:Compare-And-Swap是乐观锁的核心,它能够原子地比较并交换值。
无阻塞:在竞争失败时不会阻塞线程,而是重试操作。
版本控制:通过比较预期值来确保数据一致性。
四、性能对比
悲观锁的主要开销来自:
系统调用:加锁/解锁操作通常需要进入内核态
线程调度:当锁被占用时,线程会被挂起
上下文切换:线程挂起和恢复时的上下文切换
乐观锁的主要开销来自:
CAS操作:虽然是原子的,但频繁失败会导致重试
CPU缓存一致性:CAS操作会触发缓存一致性协议
五、应用场景
适合使用悲观锁的场景
1、写操作频繁的场景
class Database {
std::mutex mtx_;
std::map<int, std::string> data_;
public:
void update(int key, const std::string& value) {
std::lock_guard<std::mutex> lock(mtx_);
data_[key] = value;
}
};
2、临界区执行时间较长的场景
class ComplexTask {
std::mutex mtx_;
void process_data() {
std::lock_guard<std::mutex> lock(mtx_);
// 复杂的数据处理,可能需要较长时间
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
};
适合使用乐观锁的场景
1、读多写少的场景
class ReadMostlyCache {
std::atomic<uint64_t> version_{0};
std::array<int, 1024> data_;
public:
void update(size_t index, int value) {
data_[index] = value;
version_.fetch_add(1, std::memory_order_release);
}
std::pair<int, uint64_t> read(size_t index) {
uint64_t ver = version_.load(std::memory_order_acquire);
return {data_[index], ver};
}
};
2、简单的计数器场景
class Statistics {
std::atomic<uint64_t> total_requests_{0};
std::atomic<uint64_t> failed_requests_{0};
public:
void record_request(bool success) {
total_requests_.fetch_add(1, std::memory_order_relaxed);
if (!success) {
failed_requests_.fetch_add(1, std::memory_order_relaxed);
}
}
};
6. 总结
悲观锁通过操作系统提供的同步原语实现,保证了强一致性,但带来了系统调用和上下文切换的开销。
乐观锁通过硬件提供的原子指令实现,在无竞争情况下性能极高,但在竞争激烈时可能会因为频繁重试而性能下降。
并发访问的模式(读多写少还是写多)
临界区的执行时间
冲突的概率
性能要求
end
CppPlayer
关于AI编程工具:Cursor,现在官方有漏洞可以无限续期。需要方法的加我vx: fb964919126
文章推荐