在C++多线程编程中,锁是保证共享资源访问安全的基础工具,但锁的粒度选择直接决定了多线程程序的性能表现。锁粒度指的是锁所保护的代码范围或者共享资源的范围,粒度过大或过小都会带来问题,需要结合具体场景合理调整。

锁粒度的基本概念
锁粒度可以分为粗粒度和细粒度两种类型。粗粒度锁是指用一个锁保护大范围的代码或者多个共享资源,实现简单但并发度低;细粒度锁是指用多个锁分别保护不同的小范围代码或者单个共享资源,并发度高但实现复杂度更高,还可能增加死锁风险。
粗粒度锁的问题
当使用粗粒度锁时,多个线程即使访问的是不同的共享资源,也需要竞争同一个锁,导致线程阻塞等待,无法发挥多核CPU的优势。比如下面的代码用一个全局锁保护两个独立的计数器:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex global_mutex;
int counter_a = 0;
int counter_b = 0;
// 粗粒度锁:用一个锁保护两个独立计数器
void increment_a() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(global_mutex);
++counter_a;
}
}
void increment_b() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(global_mutex);
++counter_b;
}
}
int main() {
std::vector<std::thread> threads;
threads.emplace_back(increment_a);
threads.emplace_back(increment_b);
for (auto& t : threads) {
t.join();
}
std::cout << "counter_a: " << counter_a << ", counter_b: " << counter_b << std::endl;
return 0;
}
上述代码中,increment_a和increment_b操作的是两个完全独立的计数器,却需要竞争同一个global_mutex,两个线程无法并行执行,执行效率会明显下降。
细粒度锁的优势
如果将锁拆分为两个,分别保护两个计数器,两个线程就可以并行执行,提升整体性能:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mutex_a;
std::mutex mutex_b;
int counter_a = 0;
int counter_b = 0;
// 细粒度锁:两个独立锁分别保护两个计数器
void increment_a() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mutex_a);
++counter_a;
}
}
void increment_b() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mutex_b);
++counter_b;
}
}
int main() {
std::vector<std::thread> threads;
threads.emplace_back(increment_a);
threads.emplace_back(increment_b);
for (auto& t : threads) {
t.join();
}
std::cout << "counter_a: " << counter_a << ", counter_b: " << counter_b << std::endl;
return 0;
}
锁粒度控制的常用方法
缩小锁的保护范围
很多时候开发者会把不必要的代码放到锁内部,导致锁持有时间过长。正确的做法是只把访问共享资源的代码放在锁内部,其他无关操作移到锁外部。比如下面的代码:
#include <mutex>
#include <string>
std::mutex data_mutex;
std::string shared_data;
// 优化前:锁内部包含无关操作
void process_data_before() {
std::lock_guard<std::mutex> lock(data_mutex);
// 模拟耗时的非共享资源操作
std::string temp = "processed_" + shared_data;
shared_data = temp;
}
// 优化后:只保护共享资源访问
void process_data_after() {
std::string temp;
{
std::lock_guard<std::mutex> lock(data_mutex);
temp = shared_data;
}
// 非共享资源操作移到锁外部
temp = "processed_" + temp;
{
std::lock_guard<std::mutex> lock(data_mutex);
shared_data = temp;
}
}
优化后的代码减少了锁的持有时间,其他线程可以更早获取到锁,提升并发效率。
使用读写锁替代互斥锁
如果共享资源的读操作远多于写操作,可以使用std::shared_mutex读写锁,读操作可以共享访问,写操作独占访问,进一步提升并发度:
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
std::shared_mutex rw_mutex;
int shared_value = 0;
void read_value(int thread_id) {
for (int i = 0; i < 1000; ++i) {
std::shared_lock<std::shared_mutex> lock(rw_mutex);
// 读操作,多个线程可以同时执行
int val = shared_value;
}
}
void write_value(int thread_id) {
for (int i = 0; i < 100; ++i) {
std::unique_lock<std::shared_mutex> lock(rw_mutex);
// 写操作,独占访问
++shared_value;
}
}
int main() {
std::vector<std::thread> threads;
// 启动8个读线程
for (int i = 0; i < 8; ++i) {
threads.emplace_back(read_value, i);
}
// 启动2个写线程
for (int i = 0; i < 2; ++i) {
threads.emplace_back(write_value, i);
}
for (auto& t : threads) {
t.join();
}
std::cout << "final value: " << shared_value << std::endl;
return 0;
}
锁粒度控制的注意事项
细粒度锁虽然能提升性能,但也会带来死锁风险。如果多个线程需要同时获取多个锁,需要保证获取锁的顺序一致,或者使用std::scoped_lock来避免死锁:
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
// 错误示例:可能死锁
void unsafe_func() {
std::lock_guard<std::mutex> lock1(mutex1);
std::lock_guard<std::mutex> lock2(mutex2);
// 操作共享资源
}
// 正确示例:使用scoped_lock避免死锁
void safe_func() {
std::scoped_lock lock(mutex1, mutex2);
// 操作共享资源
}
另外,锁粒度过细也会增加代码维护成本,需要根据实际性能测试结果调整,不要盲目追求细粒度。如果共享资源访问冲突很少,粗粒度锁反而更简单可靠。