互斥锁、读写锁、ref()函数使用
目录
- 1. 互斥锁(Mutex)
- 1.1 基本概念
- 1.2 使用方法
- 1.3 代码示例
- 2. 读写锁(Read-Write Lock)
- 2.1 基本概念
- 2.2 使用方法
- 2.3 代码示例
- 3. 适用场景对比
- 4. 多线程中的 std::ref() 函数
- 4.1 为什么需要 std::ref()
- 4.2 使用规则
- 4.3 实际示例
- 4.4 常见错误
- 5. 最佳实践
- 6. 常见问题
1. 互斥锁(Mutex)
1.1 基本概念
互斥锁(Mutual Exclusion Lock)是最基本的同步机制,用于保护共享资源。它确保在同一时刻只有一个线程能够访问被保护的资源。
核心特性:
- 独占访问:无论读还是写,同一时刻只允许一个线程持有锁
- 阻塞等待:其他线程在锁被占用时会被阻塞,直到锁被释放
- 简单可靠:实现简单,适合大多数场景
1.2 使用方法
C++11提供了std::mutex类,配合RAII机制的std::lock_guard或std::unique_lock使用。
基本步骤:
- 定义互斥锁对象
- 在需要保护的代码段前加锁
- 执行临界区代码
- 自动或手动解锁
1.3 代码示例
示例1:基本使用 - 保护共享计数器
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>std::mutex mtx; // 全局互斥锁
int shared_counter = 0; // 共享资源void increment(int id, int times) {for (int i = 0; i < times; ++i) {// 使用 lock_guard 自动管理锁的生命周期std::lock_guard<std::mutex> lock(mtx);++shared_counter;std::cout << "线程 " << id << " 将计数器增加到: " << shared_counter << std::endl;}// lock_guard 析构时自动释放锁
}int main() {const int num_threads = 5;const int increments_per_thread = 10;std::vector<std::thread> threads;// 创建多个线程for (int i = 0; i < num_threads; ++i) {threads.emplace_back(increment, i, increments_per_thread);}// 等待所有线程完成for (auto& t : threads) {t.join();}std::cout << "最终计数器值: " << shared_counter << std::endl;std::cout << "预期值: " << num_threads * increments_per_thread << std::endl;return 0;
}
示例2:使用 unique_lock 实现更灵活的锁控制
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::mutex mtx;void flexible_locking(int id) {// unique_lock 允许手动控制锁的获取和释放std::unique_lock<std::mutex> lock(mtx);std::cout << "线程 " << id << " 获得锁" << std::endl;// 可以手动解锁lock.unlock();// 执行一些不需要锁保护的操作std::this_thread::sleep_for(std::chrono::milliseconds(100));// 重新获取锁lock.lock();std::cout << "线程 " << id << " 重新获得锁" << std::endl;// 析构时自动释放锁(如果还持有的话)
}int main() {std::thread t1(flexible_locking, 1);std::thread t2(flexible_locking, 2);t1.join();t2.join();return 0;
}
示例3:避免死锁 - 同时锁定多个互斥锁
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx1, mtx2;void safe_transfer(int from, int to, int amount) {// 使用 std::lock 同时锁定多个互斥锁,避免死锁std::lock(mtx1, mtx2);// 将已锁定的互斥锁交给 lock_guard 管理std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);std::cout << "从账户 " << from << " 转账 " << amount << " 到账户 " << to << std::endl;
}int main() {std::thread t1(safe_transfer, 1, 2, 100);std::thread t2(safe_transfer, 2, 1, 50);t1.join();t2.join();return 0;
}
2. 读写锁(Read-Write Lock)
2.1 基本概念
读写锁是一种更精细的锁机制,它区分读操作和写操作,允许:
- 多个读线程同时访问:读操作不会修改数据,可以并发执行
- 写线程独占访问:写操作需要独占锁,不能与其他读或写操作并发
核心特性:
- 读共享:多个线程可以同时持有读锁
- 写独占:写锁与任何其他锁互斥
- 提高并发:在读多写少的场景下性能更优
2.2 使用方法
C++17引入了std::shared_mutex,配合std::shared_lock(读锁)和std::unique_lock(写锁)使用。
基本步骤:
- 读操作:使用
std::shared_lock获取共享锁 - 写操作:使用
std::unique_lock或std::lock_guard获取独占锁
2.3 代码示例
示例1:基本使用 - 保护共享数据结构
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
#include <chrono>
#include <map>class ThreadSafeMap {
private:std::map<int, std::string> data_;mutable std::shared_mutex mutex_; // mutable 允许在 const 方法中使用public:// 写操作:使用独占锁void insert(int key, const std::string& value) {std::unique_lock<std::shared_mutex> lock(mutex_);data_[key] = value;std::cout << "写入: [" << key << "] = " << value << std::endl;}// 读操作:使用共享锁bool find(int key, std::string& value) const {std::shared_lock<std::shared_mutex> lock(mutex_);auto it = data_.find(key);if (it != data_.end()) {value = it->second;return true;}return false;}// 读操作:获取大小size_t size() const {std::shared_lock<std::shared_mutex> lock(mutex_);return data_.size();}
};void reader(const ThreadSafeMap& map, int id) {for (int i = 0; i < 5; ++i) {std::string value;if (map.find(i, value)) {std::cout << "读线程 " << id << " 读取到: [" << i << "] = " << value << std::endl;}std::this_thread::sleep_for(std::chrono::milliseconds(10));}
}void writer(ThreadSafeMap& map, int id) {for (int i = 0; i < 3; ++i) {map.insert(i, "value_from_thread_" + std::to_string(id));std::this_thread::sleep_for(std::chrono::milliseconds(50));}
}int main() {ThreadSafeMap map;std::vector<std::thread> threads;// 创建写线程 - 注意使用 std::ref()threads.emplace_back(writer, std::ref(map), 1);threads.emplace_back(writer, std::ref(map), 2);// 创建多个读线程(可以并发执行)- 注意使用 std::cref()for (int i = 0; i < 5; ++i) {threads.emplace_back(reader, std::cref(map), i);}// 等待所有线程完成for (auto& t : threads) {t.join();}std::cout << "最终 map 大小: " << map.size() << std::endl;return 0;
}
4. 多线程中的 std::ref() 函数
4.1 为什么需要 std::ref()
在C++中创建线程时,std::thread 的构造函数会按值复制所有参数到新线程的上下文中。这意味着:
问题: 即使线程函数的参数是引用类型,直接传递变量也会被复制,而不是真正的引用传递。
void thread_func(int& value) { // 参数是引用++value;
}int main() {int num = 0;// ❌ 错误:即使函数参数是引用,num 仍会被复制// 这会导致编译错误或未定义行为std::thread t(thread_func, num); t.join();// num 的值不会改变!
}
解决方案: 使用 std::ref() 创建引用包装器,确保按引用传递,线程使用了&,传参的时候就要用ref()或cref()。
int main() {int num = 0;// ✅ 正确:使用 std::ref() 按引用传递std::thread t(thread_func, std::ref(num));t.join();std::cout << num << std::endl; // 输出: 1
}
4.2 使用规则
| 线程函数参数类型 | 传参方式 | 说明 |
|---|---|---|
void func(int value) | std::thread t(func, num); | 按值传递,直接传 |
void func(int& value) | std::thread t(func, std::ref(num)); | 非const引用,用 std::ref() |
void func(const int& value) | std::thread t(func, std::cref(num)); | const引用,用 std::cref() |
void func(int* ptr) | std::thread t(func, &num); | 指针,直接传地址 |
记忆口诀:
函数参数带 &,
传参就要用 ref()!
4.4 常见错误
错误1:忘记使用 std::ref()
void modify(int& value) {++value;
}int main() {int num = 0;// ❌ 编译错误!无法将右值绑定到非const引用std::thread t(modify, num);// ✅ 正确写法std::thread t(modify, std::ref(num));
}
错误2:对象生命周期问题
// ❌ 危险:引用的对象会先被销毁
std::thread create_thread() {int local_var = 0;return std::thread(func, std::ref(local_var)); // 未定义行为!
} // local_var 在这里被销毁,但线程还在运行// ✅ 正确:确保对象生命周期足够长
class MyClass {int member_var_ = 0;std::thread thread_;public:void start() {// member_var_ 的生命周期与 MyClass 对象相同thread_ = std::thread(func, std::ref(member_var_));}~MyClass() {if (thread_.joinable()) {thread_.join();}}
};
错误3:const 引用使用 ref 而不是 cref
void read_only(const Data& data) {// 只读操作
}int main() {Data data;// ⚠️ 可以工作,但不推荐std::thread t(read_only, std::ref(data));// ✅ 推荐:const引用使用 crefstd::thread t(read_only, std::cref(data));
}
5. 最佳实践
5.1 使用RAII管理锁
推荐做法:
// ✅ 使用 lock_guard 或 unique_lock
{std::lock_guard<std::mutex> lock(mtx);// 操作共享资源
} // 自动解锁// ✅ 读写锁也使用RAII
{std::shared_lock<std::shared_mutex> lock(rwlock); // 读锁// 读取操作
}
避免手动加解锁:
// ❌ 容易忘记解锁或异常时无法解锁
mtx.lock();
// 操作共享资源
mtx.unlock(); // 如果上面抛出异常,这行不会执行!
5.2 最小化锁的持有时间
// ✅ 只在必要时持有锁
void goodExample() {// 准备数据(不需要锁)std::string data = prepareData();{std::lock_guard<std::mutex> lock(mtx);// 只在这里持有锁shared_resource = data;}// 其他操作(不需要锁)processResult();
}// ❌ 锁持有时间过长
void badExample() {std::lock_guard<std::mutex> lock(mtx);std::string data = prepareData(); // 不需要锁保护shared_resource = data;processResult(); // 不需要锁保护
}
5.3 避免死锁
规则1:始终以相同顺序获取多个锁
// 所有线程都按 mtx1 -> mtx2 的顺序加锁
void thread1() {std::lock_guard<std::mutex> lock1(mtx1);std::lock_guard<std::mutex> lock2(mtx2);// ...
}void thread2() {std::lock_guard<std::mutex> lock1(mtx1); // 相同顺序std::lock_guard<std::mutex> lock2(mtx2);// ...
}
规则2:使用 std::lock 同时锁定多个锁
std::lock(mtx1, mtx2); // 原子地锁定两个锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
规则3:使用 std::scoped_lock(C++17)
// 最推荐的方式
std::scoped_lock lock(mtx1, mtx2); // 自动处理多个锁
5.4 正确使用 std::ref()
规则:
- 线程函数参数是
T&时,使用std::ref(obj) - 线程函数参数是
const T&时,使用std::cref(obj) - 确保被引用对象的生命周期覆盖整个线程执行期
// ✅ 正确示例
void process(Data& data) { /* ... */ }
void read(const Data& data) { /* ... */ }Data data;
std::thread t1(process, std::ref(data)); // 非const引用
std::thread t2(read, std::cref(data)); // const引用
5.5 使用 const 成员函数表明读意图
class ThreadSafeContainer {
private:std::vector<int> data_;mutable std::shared_mutex mutex_;public:// const 函数表明这是读操作size_t size() const {std::shared_lock<std::shared_mutex> lock(mutex_);return data_.size();}// 非 const 函数表明这是写操作void push_back(int value) {std::unique_lock<std::shared_mutex> lock(mutex_);data_.push_back(value);}
};
6. 常见问题
Q1: std::ref() 和直接传指针有什么区别?
答:
void func_ref(int& val) { ++val; }
void func_ptr(int* val) { ++(*val); }int num = 0;// 使用引用(需要 std::ref)
std::thread t1(func_ref, std::ref(num));// 使用指针(直接传)
std::thread t2(func_ptr, &num);
区别:
- 引用更安全(不能为空)
- 引用语法更清晰(不需要解引用)
- 指针更灵活(可以重新指向、可以为nullptr)
- 现代C++推荐优先使用引用
