一、锁的基本概念
在多线程程序中,多个线程可能同时访问和操作同一份数据,若不加以控制,将导致数据竞争(Data Race)和数据不一致。锁(Lock)是一种同步原语,能够确保同一时刻只有一个或部分线程访问共享资源,从而保证并发安全。
为什么需要锁?
并发≠并行。当多个线程访问 共享可变状态 时,必须通过同步原语保证 数据竞争(data race) 不会发生。C++ 标准库把“锁”抽象为 互斥量(mutex) 与 锁管理器(lock guard)。锁的本质是对临界区(Critical Section)的保护。合理使用锁是实现线程安全的基础。
二、C++锁的分类
C++标准库及POSIX线程库为开发者提供了多种锁,常见分类如下:
互斥锁(Mutex)
读写锁(Shared/Read-Write Mutex)
自旋锁(Spinlock)
三、互斥锁(Mutex)
3.1 基本互斥锁 std::mutex
std::mutex 是 C++11 标准库中最基础的互斥锁类型。它的作用是保证同一时刻只有一个线程能够访问被保护的临界区资源,从而防止数据竞争和数据不一致。
原理:线程调用 lock() 发现被占用时,主动让出 CPU(阻塞),由内核唤醒。
适用:临界区较大或线程数远大于 CPU 核心数,避免空转。
代码示例:
#include <mutex>
class Counter {
public:
void Increment() {
mutex_.lock(); // 手动加锁
++count_;
mutex_.unlock(); // 手动解锁
}
int Get() const {
mutex_.lock();
int value = count_;
mutex_.unlock();
return value;
}
private:
mutable std::mutex mutex_; // 互斥锁用于保护count_
int count_ = 0;
};
代码解释:
mutex_.lock()和mutex_.unlock()分别用于加锁和解锁。如果在加锁后因异常或提前 return 忘记解锁,容易导致死锁或资源泄漏。
注意事项:
手动加解锁容易出错,推荐配合智能锁(RAII锁)使用。
3.2 带超时机制的互斥锁 std::timed_mutex
原理:std::timed_mutex 在 std::mutex 的基础上增加了“限时等待”接口:try_lock_for(rel_time) 和 try_lock_until(abs_time) 。成功拿到锁 → 返回 true;超时仍未拿到 → 返回 false,线程不会被永久阻塞。
适用:需避免长时间等待锁的场合,如实时系统。
代码示例:
#include <mutex>
#include <chrono>
class TimedLockExample {
public:
bool TryLockForWork() {
// 尝试在100ms内获取锁,超时则放弃
if (mutex_.try_lock_for(std::chrono::milliseconds(100))) {
DoWork();
mutex_.unlock();
return true;
}
return false;
}
private:
void DoWork() {
// ...实际工作内容
}
std::timed_mutex mutex_;
};
3.3 递归互斥锁 std::recursive_mutex
原理:允许同一线程多次加锁,需相同次数解锁。
适用:递归函数、嵌套加锁。注意:过度使用可能导致死锁或逻辑混乱。
代码示例:
#include <mutex>
class RecursiveExample {
public:
void RecursiveFunc(int depth) {
std::lock_guard<std::recursive_mutex> lock(mutex_);
if (depth > 0) {
RecursiveFunc(depth - 1); // 递归调用可多次加锁
}
}
private:
std::recursive_mutex mutex_;
};
3.4 带超时机制的递归互斥锁 std::recursive_timed_mutex
原理:结合了std::recursive_mutex和std::timed_mutex的特性,支持递归锁定和超时机制。
适用:需要递归锁定资源,并且希望能够设置尝试获取锁的超时时间的场景。这在需要防止线程在等待锁时无限阻塞的复杂递归调用中特别有用。
代码示例:
#include <mutex>
#include <chrono>
class RecursiveTimedExample {
public:
// 尝试递归加锁depth次,每次加锁超时时间为50ms
bool TryRecursiveLock(int depth) {
// 尝试在50毫秒内获得递归互斥量的锁
if (mutex_.try_lock_for(std::chrono::milliseconds(50))) {
if (depth > 0) {
// 递归调用自身,继续加锁
bool success = TryRecursiveLock(depth - 1);
// 解锁当前层次的锁
mutex_.unlock();
return success;
}
// 已达到递归基准,解锁并返回true
mutex_.unlock();
return true;
}
// 未能在指定时间内获得锁,返回false
return false;
}
private:
// 递归定时互斥量,支持同一线程多次加锁
std::recursive_timed_mutex mutex_;
};
3.5 pthread_mutex_t(POSIX)
原理:POSIX线程库的原生互斥锁。
适用:适合系统底层开发,适合C/C++混合项目。
代码示例:
#include <pthread.h>
// 使用pthread库实现线程安全计数
class PthreadMutexExample {
public:
// 构造函数:初始化互斥锁
PthreadMutexExample() {
pthread_mutex_init(&mutex_, nullptr);
}
// 析构函数:销毁互斥锁,避免资源泄漏
~PthreadMutexExample() {
pthread_mutex_destroy(&mutex_);
}
// 线程安全地递增计数器
void SafeIncrement() {
pthread_mutex_lock(&mutex_); // 加锁,保护counter_的访问
++counter_; // 递增计数器
pthread_mutex_unlock(&mutex_); // 解锁
}
private:
pthread_mutex_t mutex_; // POSIX 互斥锁
int counter_ = 0; // 计数器
};
四、读写锁(Shared/Read-Write Mutex)
4.1 共享锁 std::shared_mutex (C++17)
共享锁也叫读写锁。
原理:读操作和写操作使用不同的锁定机制,支持多线程并发读,写操作时独占。
适用:适合读多写少场景,高并发读、低频写,如缓存、配置等。
代码示例:
#include <shared_mutex>
#include <string>
// 线程安全的数据读写类,支持多线程并发读和独占写
class SharedData {
public:
// 写操作,使用独占锁,防止其他线程读写
void Write(const std::string& data) {
std::unique_lock<std::shared_mutex> lock(mutex_); // 独占锁,阻塞其他读写
data_ = data; // 写入数据
}
// 读操作,使用共享锁,允许多个线程同时读取
std::string Read() const {
std::shared_lock<std::shared_mutex> lock(mutex_); // 共享锁,允许并发读取
return data_; // 返回数据
}
private:
mutable std::shared_mutex mutex_; // 读写锁,mutable允许const方法加锁
std::string data_; // 受保护的数据成员
};
std::unique_lock用于写操作(独占),std::shared_lock用于读操作(共享)。
Read()是一个const方法。
Write()不是const方法。const 关键字修饰成员函数时,表示该函数在逻辑上不会修改类的成员变量(除了被 mutable 修饰的成员)。
在 SharedData 类中,Read() 方法被声明为 const,意味着它不会修改类的成员变量(除了 mutable 成员)。
这也是为什么 mutex_ 被声明为 mutable std::shared_mutex mutex_;,这样即使在 const 方法中也可以对 mutex_ 加锁。
4.2 带超时机制的共享锁 std::shared_timed_mutex(C++14)
原理:允许多个线程并发读取,但写操作必须独占。try_lock_for 方法可以在指定的超时时间内尝试获取写锁。如果在时间内获得锁,则执行写操作,否则立即返回失败。
适用:
高并发读、低并发写:如缓存、配置中心等场景,读操作频繁,写操作较少。
需要避免死锁或长时间阻塞:如实时系统或对响应时间有要求的系统,写操作不能无限等待。
需要尝试性写入:如在某些情况下,写线程希望在限定时间内写入数据,如果无法获取锁则放弃本次写入。
代码示例:
#include <shared_mutex>
#include <chrono>
// 线程安全的共享数据写入类,支持定时尝试获取写锁
class SharedTimedExample {
public:
// 尝试在指定时间内获取写锁并写入数据
bool TryWrite(const std::string& data) {
// 尝试在100毫秒内获取独占写锁
if (mutex_.try_lock_for(std::chrono::milliseconds(100))) {
data_ = data; // 获得锁后安全地写入数据
mutex_.unlock(); // 写入完成后立即释放锁
return true; // 写入成功
}
return false; // 超时未获取到锁,写入失败
}
private:
std::shared_timed_mutex mutex_; // 支持定时加锁的读写锁
std::string data_; // 受保护的数据成员
};
4.3 POSIX线程库的读写锁 pthread_rwlock_t
原理:POSIX线程库的读写锁,支持多线程读、单线程写。
适用:C/C++跨平台高并发读写。
代码示例:
#include <pthread.h>
// 使用 pthread 读写锁实现线程安全读写的类
class PthreadRWLockExample {
public:
// 构造函数:初始化读写锁
PthreadRWLockExample() {
pthread_rwlock_init(&rwlock_, nullptr);
}
// 析构函数:销毁读写锁,释放资源
~PthreadRWLockExample() {
pthread_rwlock_destroy(&rwlock_);
}
// 写操作,获取写锁,独占访问
void Write(int value) {
pthread_rwlock_wrlock(&rwlock_); // 加写锁,阻塞其他读写
data_ = value; // 写入数据
pthread_rwlock_unlock(&rwlock_); // 解锁
}
// 读操作,获取读锁,可以并发读取
int Read() const {
pthread_rwlock_rdlock(&rwlock_); // 加读锁,允许多个线程并发读取
int value = data_; // 读取数据
pthread_rwlock_unlock(&rwlock_); // 解锁
return value;
}
private:
mutable pthread_rwlock_t rwlock_; // 读写锁,mutable保证const方法可加锁
int data_ = 0; // 受保护的数据
};
五、自旋锁(Spinlock)
5.1 原子标志实现的自旋锁 std::atomic_flag
原理:自旋锁(SpinLock)是一种用户态锁,线程在加锁时如果没有获得锁,会不断循环检查锁状态(自旋),而不是阻塞挂起。
std::atomic_flag是最轻量级的原子类型,test_and_set操作会原子性地设置标志,并返回旧值,适合实现自旋锁。memory_order_acquire和memory_order_release保证内存操作的可见性和顺序性。
适用:
临界区极短:锁保护的代码执行时间非常短,否则自旋会浪费 CPU 资源。
高并发、低延迟要求:如多核 CPU 上的小粒度同步操作。
不可用于临界区较长或可能阻塞的场景,否则会导致 CPU 资源浪费和性能下降。
代码示例:
#include <atomic>
// 自旋锁实现类,适用于临界区很短的高并发场景
class SpinLock {
public:
// 加锁操作
void Lock() {
// test_and_set() 返回旧值,如果为 true 表示锁已被其他线程持有
// 如果锁被占用,则循环自旋等待,直到获取到锁为止
while (flag_.test_and_set(std::memory_order_acquire)) {
// busy-wait(自旋等待),不让出 CPU
}
}
// 解锁操作
void Unlock() {
// clear() 将标志位复位,释放锁
flag_.clear(std::memory_order_release);
}
private:
std::atomic_flag flag_ = ATOMIC_FLAG_INIT; // 原子标志,用于实现自旋锁
};
5.2 POSIX线程库的自旋锁 pthread_spinlock_t
原理:pthread_spinlock_t 是 POSIX 提供的自旋锁实现。自旋锁在加锁时如果未获得锁,会在用户态不断尝试获取,不会主动让出CPU。
pthread_spin_init(&spinlock_, 0)初始化自旋锁,0表示锁仅在本进程的线程间使用。pthread_spin_lock尝试加锁,如果锁已被占用则自旋等待,直到获得锁。pthread_spin_unlock释放锁。
适用:
临界区非常短:适合保护执行时间极短的代码段,否则会浪费大量CPU资源。
高并发、低延迟:如多核服务器上的原子操作、队列入队/出队等。
不适合临界区较长或可能阻塞的场景,否则会导致CPU资源浪费和性能下降。
代码示例:
#include <pthread.h>
// 基于pthread库的自旋锁示例类
class PthreadSpinLockExample {
public:
// 构造函数:初始化自旋锁
PthreadSpinLockExample() {
pthread_spin_init(&spinlock_, 0); // 0表示用于当前进程内线程间同步
}
// 析构函数:销毁自旋锁,释放资源
~PthreadSpinLockExample() {
pthread_spin_destroy(&spinlock_);
}
// 受保护的临界区操作
void CriticalSection() {
pthread_spin_lock(&spinlock_); // 加锁,若未获得锁则自旋等待
// 临界区,放置需要同步保护的代码
pthread_spin_unlock(&spinlock_); // 解锁
}
private:
pthread_spinlock_t spinlock_; // pthread自旋锁对象
};
评论区