各种锁的介绍

在并发编程中,锁是用于保护共享资源、防止数据竞争和确保线程安全的机制。以下是对你提到的各种锁(mutexlock_guardshared_lockunique_lock、分布式锁、自旋锁、悲观锁、乐观锁)的详细介绍,涵盖定义、用途、实现原理、优缺点及适用场景。由于你提到自旋锁两次,我会将其合并处理,并结合 C++ 上下文(基于之前的线程池讨论)进行讲解。


1. 互斥锁(std::mutex

定义

std::mutex 是 C++11 提供的互斥锁(Mutual Exclusion Lock),用于保护共享资源,确保同一时间只有一个线程可以访问临界区。

用途

  • 防止多个线程同时访问共享资源(如线程池中的任务队列)。
  • 实现线程同步,确保数据一致性。

实现原理

  • std::mutex 提供 lock()unlock() 方法,锁定和释放互斥量。
  • 线程尝试获取锁时,若锁已被占用,线程会阻塞(进入休眠)直到锁可用。
  • 操作系统底层通常通过原子操作和内核调度实现。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
int shared_data = 0;

void increment() {
for (int i = 0; i < 1000; ++i) {
mtx.lock();
++shared_data;
mtx.unlock();
}
}

int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared data: " << shared_data << std::endl; // 应输出 2000
}

优缺点

  • 优点
    • 简单易用,适合保护临界区。
    • 提供强一致性保证。
  • 缺点
    • 手动调用 lock/unlock 易出错(忘记解锁会导致死锁)。
    • 阻塞线程可能导致性能开销(上下文切换)。

适用场景

  • 保护共享数据(如计数器、队列)。
  • 需要简单、强一致性的同步场景。

2. std::lock_guard

定义

std::lock_guard 是 C++11 提供的 RAII(资源获取即初始化)类模板,用于自动管理 std::mutex 的锁定和解锁,防止忘记解锁导致死锁。

用途

  • 简化 std::mutex 的使用,确保锁在作用域结束时自动释放。
  • 用于短生命周期的临界区保护。

实现原理

  • 构造时调用 mutex.lock(),析构时调用 mutex.unlock()
  • 使用 RAII 机制,绑定锁的生命周期到作用域。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
int shared_data = 0;

void increment() {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> guard(mtx); // 自动加锁
++shared_data;
} // 离开作用域,自动解锁
}

int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared data: " << shared_data << std::endl;
}

优缺点

  • 优点
    • 自动管理锁,防止忘记解锁。
    • 代码简洁,安全性高。
  • 缺点
    • 不支持灵活的锁管理(如中途解锁或条件变量)。
    • 仅适用于简单场景。

适用场景

  • 需要简单、自动化的互斥锁管理。
  • 保护短临界区(如单次数据操作)。

3. std::shared_mutex/shared_lock

定义

std::shared_lock 是 C++14 引入的 RAII 类模板,用于管理共享锁(std::shared_mutexstd::shared_timed_mutex),支持读写锁模式(多读单写)。

用途

  • 允许多个线程同时读取共享资源(共享锁),但写操作独占(独占锁)。
  • 用于读多写少的场景,提高并发性能。

实现原理

std::shared_mutex 支持两种锁模式:

    • 共享锁lock_shared()):多个线程可同时获取,适合读操作。
    • 独占锁lock()):仅一个线程可获取,适合写操作。
  • std::shared_lock 在构造时调用 lock_shared(),析构时调用 unlock_shared()

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <shared_mutex>
#include <thread>
#include <iostream>

std::shared_mutex smtx;
int shared_data = 0;

void read_data(int id) {
std::shared_lock<std::shared_mutex> lock(smtx); // 共享锁
std::cout << "Reader " << id << " reads: " << shared_data << std::endl;
}

void write_data() {
std::unique_lock<std::shared_mutex> lock(smtx); // 独占锁
++shared_data;
std::cout << "Writer updated data to: " << shared_data << std::endl;
}

int main() {
std::thread readers[3];
for (int i = 0; i < 3; ++i) {
readers[i] = std::thread(read_data, i);
}
std::thread writer(write_data);
for (auto& r : readers) r.join();
writer.join();
}

优缺点

  • 优点
    • 支持读写分离,提高读操作并发性。
    • RAII 机制,自动管理锁。
  • 缺点
    • 实现复杂度高于 std::mutex
    • 写操作可能饥饿(读操作过多时)。

适用场景

  • 读多写少的场景(如数据库查询、缓存访问)。
  • 需要高并发读操作的系统。

4. std::unique_lock

定义

std::unique_lock 是 C++11 提供的 RAII 类模板,用于管理 std::mutex(或类似互斥量),比 std::lock_guard 更灵活,支持延迟加锁、条件变量等。

用途

  • 提供灵活的锁管理(如手动加锁/解锁、与条件变量配合)。
  • 用于复杂同步场景(如线程池中的任务等待)。

实现原理

  • 类似 std::lock_guard,但支持:
    • 延迟加锁(std::unique_lock(mutex, std::defer_lock))。
    • 手动解锁(unlock())。
    • 转移锁所有权(std::move)。
  • std::condition_variable 配合,wait 时自动释放锁,唤醒时重新加锁。

代码示例

结合线程池中的条件变量(参考之前的线程池代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::mutex mutex_;
std::condition_variable condition_;
std::queue<std::function<void()>> tasks;
bool stop;

void worker() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
} // 锁自动释放
task();
}
}

优缺点

  • 优点
    • 灵活性高,支持条件变量、延迟加锁等。
    • RAII 机制,自动管理锁。
  • 缺点
    • std::lock_guard 略高开销(因功能更复杂)。
    • 使用复杂,需小心管理锁状态。

适用场景

  • 需要条件变量的场景(如线程池的任务等待)。
  • 复杂同步逻辑(如动态加锁/解锁)。

5. 分布式锁

定义

分布式锁是跨多个进程或节点(通常在分布式系统中)的锁机制,用于协调不同机器上的共享资源访问。

用途

  • 在分布式系统中保护共享资源(如数据库、文件系统)。
  • 实现分布式任务调度、分布式事务等。

实现原理

  • 通常基于分布式协调服务(如 ZooKeeper、Redis、etcd)实现。
  • Redis 实现
    • 使用 SETNX(set if not exists)命令创建锁。
    • 设置锁的过期时间(防止死锁)。
    • 释放锁通过删除键。
  • ZooKeeper 实现
    • 使用临时节点和顺序节点创建锁。
    • 节点删除或断开连接自动释放锁。

代码示例(Redis 分布式锁)

使用 C++ Redis 客户端(如 cpp_redis):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <cpp_redis/cpp_redis>
#include <thread>
#include <chrono>

cpp_redis::client redis_client;

bool acquire_lock(const std::string& lock_key, int timeout_ms) {
auto expire = std::chrono::milliseconds(timeout_ms).count();
return redis_client.setnx(lock_key, "locked") && redis_client.pexpire(lock_key, expire);
}

void release_lock(const std::string& lock_key) {
redis_client.del({lock_key});
}

优缺点

  • 优点
    • 支持跨节点同步,适用于分布式系统。
    • 可实现高可用性(如 Redis 集群)。
  • 缺点
    • 依赖外部服务,增加系统复杂性。
    • 网络延迟可能影响性能。
    • 实现复杂,需处理锁超时、脑裂等问题。

适用场景

  • 分布式系统中的资源协调(如分布式任务调度)。
  • 跨节点的共享资源访问(如分布式数据库)。

6. 自旋锁(Spinlock)

定义

自旋锁是一种非阻塞锁,线程在获取锁失败时不会休眠,而是通过循环(“自旋”)不断尝试获取锁,直到成功。

用途

  • 适合临界区非常短的场景,减少上下文切换开销。
  • 高性能场景(如实时系统、内核编程)。

实现原理

  • 使用原子操作(如 std::atomic_flag)检查锁状态。
  • 如果锁被占用,线程循环检查,直到锁可用。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <atomic>
#include <thread>
#include <iostream>

class Spinlock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// 自旋等待
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};

Spinlock spinlock;
int shared_data = 0;

void increment() {
for (int i = 0; i < 1000; ++i) {
spinlock.lock();
++shared_data;
spinlock.unlock();
}
}

int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared data: " << shared_data << std::endl;
}

优缺点

  • 优点
    • 避免上下文切换,适合短临界区。
    • 实现简单,适用于低延迟场景。
  • 缺点
    • 自旋浪费 CPU 资源,长时间等待效率低。
    • 不适合长临界区或高争用场景。

适用场景

  • 短临界区(如内存分配、计数器更新)。
  • 实时系统或高性能计算。

7. 悲观锁(Pessimistic Lock)

定义

悲观锁假设并发操作会发生冲突,因此在访问共享资源前总是先加锁,确保独占访问。

用途

  • 确保数据一致性,适合写多读少的场景。
  • 防止数据竞争或冲突。

实现原理

  • 使用互斥锁(如 std::mutex)、读写锁等实现。
  • 线程获取锁后独占资源,其他线程阻塞等待。

代码示例

std::mutexstd::lock_guard 就是典型的悲观锁实现(见上述示例)。

优缺点

  • 优点
    • 强一致性,适合高冲突场景。
    • 简单可靠,易于实现。
  • 缺点
    • 阻塞线程,降低并发性能。
    • 不适合读多写少的场景。

适用场景

  • 写操作频繁的场景(如数据库事务)。
  • 需要强一致性的系统(如金融系统)。

8. 乐观锁(Optimistic Lock)

定义

乐观锁假设冲突较少,允许线程先操作共享资源,提交时检查是否发生冲突,若有冲突则回滚重试。

用途

  • 适合读多写少的场景,提高并发性能。
  • 用于数据库(如 MVCC)、版本控制等。

实现原理

  • 通常基于版本号或比较交换(CAS,Compare-And-Swap)机制。
  • 线程读取数据时记录版本号,更新时检查版本号是否改变。

代码示例(基于 CAS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> shared_data(0);

void increment() {
for (int i = 0; i < 1000; ++i) {
int expected = shared_data.load();
while (!shared_data.compare_exchange_strong(expected, expected + 1)) {
// CAS 失败,重试
expected = shared_data.load();
}
}
}

int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared data: " << shared_data << std::endl;
}

优缺点

  • 优点
    • 非阻塞,高并发性能。
    • 适合读多写少的场景。
  • 缺点
    • 冲突多时,重试开销大。
    • 实现复杂,需处理版本控制或 CAS。

适用场景

  • 读多写少的场景(如数据库 MVCC)。
  • 高并发系统(如缓存更新)。

9. 总结与对比

锁类型 阻塞/非阻塞 适用场景 C++ 实现 优缺点
std::mutex 阻塞 简单共享资源保护 lock/unlock 简单但需手动管理,易死锁
std::lock_guard 阻塞 短临界区,自动管理锁 RAII 管理 mutex 安全简单,但不灵活
std::shared_lock 阻塞 读多写少 配合 std::shared_mutex 高并发读,写可能饥饿
std::unique_lock 阻塞 复杂同步(如条件变量) 配合 std::mutex 灵活但稍复杂
分布式锁 阻塞/非阻塞 分布式系统资源协调 Redis/ZooKeeper 跨节点同步,依赖外部服务
自旋锁 非阻塞 短临界区,高性能 std::atomic_flag 低延迟但浪费 CPU
悲观锁 阻塞 写多读少,强一致性 std::mutex 简单可靠但并发性低
乐观锁 非阻塞 读多写少,高并发 std::atomic CAS 高并发但冲突多时效率低

10. 结合线程池的上下文

在之前的线程池实现中:

  • **std::mutexstd::unique_lock**:用于保护任务队列,unique_lockstd::condition_variable 配合实现线程等待/通知。
  • 适用性:线程池中任务队列的访问是典型悲观锁场景,unique_lock 提供灵活性支持条件变量。
  • 扩展建议
    • 若任务队列访问时间极短,可尝试自旋锁优化。
    • 若线程池扩展到分布式系统,可引入分布式锁管理跨节点任务。

11. 注意事项

  • 选择锁类型:根据临界区长度、读写比例、并发需求选择合适的锁。
  • 死锁风险:避免多锁嵌套,使用 std::lock 或 RAII 工具。
  • 性能权衡:阻塞锁适合长临界区,非阻塞锁(如自旋锁、乐观锁)适合短临界区或高并发。

如果你对某类锁的实现细节(如分布式锁的 Redis 配置、自旋锁的优化)或在特定场景(如线程池)的应用有进一步疑问,请告诉我,我可以提供更深入的代码或分析!