C++ 并发编程系列,关于锁的使用总结。
Created 2021.02.15 by William Yu; Last modified: 2022.09.16-v1.1.0
Contact: windmillyucong@163.com
Copyleft! 2022 William Yu. Some rights reserved.
锁
References
1. Concepts
- 互斥锁 mutex 1.1 互斥锁 mutex
- 自旋锁 Spinlock 1.2 自旋锁 Spinlock
- 读写锁 shared_mutex 1.3 读写锁 shared_mutex
- 递归锁 recursive_mutex 1.4 递归锁 recursive_mutex
锁类型 | 操作 |
---|---|
互斥锁(mutex) | - .lock() - .unlock() - .try_lock() - std::lock_guard<> - std::unique_lock<adopt_lock/defer_lock> |
自旋锁(spinlock) | - std::lock_guard<> - std::unique_lock<adopt_lock/defer_lock> |
读写锁(shared_mutex) | - std::unique_lock<> (写锁)- std::shared_lock<> (读锁) |
递归锁(recursive_mutex) | - std::lock_guard<> - std::unique_lock<> |
1.1 互斥锁 mutex
互斥锁
- 在某一时刻,只有一个线程可以获取互斥锁
- 在释放互斥锁之前其他线程都不能获取该互斥锁
- 如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待
1
2
3
#include <mutex>
std::mutex some_mutex;
1.2 自旋锁 Spinlock
原理
- 互斥锁是一种sleep-waiting 的锁
- 流程为:假设线程T1获取互斥锁并且正在处理器core1上运行时,此时线程T2也想要获取互斥锁,但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它会去处理其他事务去。
- 自旋锁是一种busy-waiting 的锁
- 流程为:假设线程T1获取互斥锁并且正在处理器core1上运行时,此时线程T2也想要获取互斥锁,但是由于T1正在使用互斥锁使得T2被阻塞。此时运行T2的处理器core2会一直不断地循环检查锁是否可用,直到获取到锁为止。
- 当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道”自旋锁”是比较耗费CPU的。
如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该使用自旋锁,否则使用互斥锁。 Q: 实际项目使用中,差别大吗?
实现一个自旋锁
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
/**
* @brief A spinlock implemented using an atomic_flag.
* @remark Reference: https://en.cppreference.com/w/cpp/atomic/atomic_flag
*/
class Spinlock {
public:
Spinlock() = default;
~Spinlock() = default;
void lock() {
while (lock_.test_and_set(std::memory_order_acquire));
}
bool try_lock() {
return !lock_.test_and_set(std::memory_order_acquire);
}
void unlock() {
lock_.clear(std::memory_order_release);
}
private:
std::atomic_flag lock_ = ATOMIC_FLAG_INIT;
};
使用方法
1
2
3
4
5
6
7
Spinlock user_data_lock_;
UserData user_data_;
int main() {
std::lock_guard<Spinlock> lock(user_data_lock_);
user_data_ //balabala
}
1.3 读写锁 shared_mutex
-
“读者-写者”问题。
-
计算机中某些数据被多个进程共享,对数据的操作有两种:
-
一种是读操作,就是从数据库中读取数据不会修改数据库中内容;
-
另一种就是写操作,写操作会修改数据库中存放的数据。
-
-
我们允许对数据同时执行多个”读”操作
-
但是某一时刻只能在数据库上有一个”写”操作来更新数据。
-
这就是一个简单的读者-写者模型。
-
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
36
37
38
39
40
41
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
std::shared_mutex rwMutex;
std::vector<int> sharedData;
// 读操作函数
void readData(int id) {
std::shared_lock<std::shared_mutex> readLock(rwMutex);
std::cout << "Thread " << id << " is reading data: ";
for (int val : sharedData) {
std::cout << val << " ";
}
std::cout << std::endl;
}
// 写操作函数
void writeData(int value) {
std::unique_lock<std::shared_mutex> writeLock(rwMutex);
std::cout << "Writing data: " << value << std::endl;
sharedData.push_back(value);
}
int main() {
// 创建写线程
std::thread writer(writeData, 42);
// 创建多个读线程
std::vector<std::thread> readers;
for (int i = 0; i < 3; ++i) {
readers.emplace_back(readData, i);
}
// 等待写线程完成
writer.join();
// 等待所有读线程完成
for (auto& reader : readers) {
reader.join();
}
return 0;
}
1.4 递归锁 recursive_mutex
常用于递归函数中上锁
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
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <mutex>
#include <thread>
class RecursiveExample {
private:
std::recursive_mutex mutex_;
int value_ = 0;
public:
// 递归函数示例
void recursiveFunction(int depth) {
std::lock_guard<std::recursive_mutex> lock(mutex_);
if (depth <= 0) {
return;
}
value_ += depth;
std::cout << "Depth " << depth << ", value: " << value_ << std::endl;
// 递归调用,会再次获取同一个锁
recursiveFunction(depth - 1);
}
// 普通函数示例
void normalFunction() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
value_ += 1;
std::cout << "Normal function, value: " << value_ << std::endl;
}
};
int main() {
RecursiveExample example;
// 测试递归函数
std::cout << "Testing recursive function:" << std::endl;
example.recursiveFunction(3);
// 测试普通函数
std::cout << "\nTesting normal function:" << std::endl;
example.normalFunction();
return 0;
}
使用递归锁的注意事项:
- 性能考虑:
- 递归锁比普通互斥锁有更多的开销
- 如果不需要递归获取锁,应该使用普通的
std::mutex
- 使用场景:
- 递归函数中需要保护共享资源
- 同一个线程需要多次获取同一个锁
- 复杂的类层次结构中,基类和派生类都需要获取同一个锁
- 替代方案:
- 如果可能,考虑重构代码以避免递归获取锁
- 使用其他同步机制,如条件变量或信号量
- 最佳实践:
- 尽量限制递归深度
- 确保每次获取锁都有对应的释放
- 使用 RAII 风格的锁管理(如
std::lock_guard
)
2. 上锁操作
2.1 mutex.lock() unlock()
对于互斥量,可以使用互斥量的 lock() 和 unlock() 方法上锁和解锁。
- 需要手动调用
1
2
3
4
5
6
7
std::vector<long> some_list; // 共享的数据
std::mutex some_mutex; // 互斥量
void some_thread_function(){
some_mutex.lock();
// 一些操作
some_mutex.unlock();
}
2.2 std::lock_guard<>
自动
- 自动上锁,自动解锁
- 而对互斥锁的lock()和unlock()需要手动调用
- 构造时自动上锁
- 离开局部作用域,析构函数自动解锁
1
2
3
4
5
6
std::vector<long> some_list; // 共享的数据
std::mutex some_mutex; // 互斥量
void some_thread_function(){
std::lock_guard<std::mutex> lock(some_mutex); // 修改数据之前上锁
// 一些操作
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <mutex> // 头文件
std::vector<long> some_list; // 共享的数据
std::mutex some_mutex; // 互斥量
void add_to_list(int new_value) {
std::lock_guard<std::mutex> lock(some_mutex); // 修改数据之前上锁
some_list.push_back(new_value);
}
bool list_contains(int value_to_find) {
std::lock_guard<std::mutex> lock(some_mutex); // 访问数据之前上锁
return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
}
对自旋锁也是一样的上锁方法。
1
2
3
4
5
6
7
Spinlock user_data_lock_;
UserData user_data_;
int main() {
std::lock_guard<Spinlock> lock(user_data_lock_);
user_data_ //balabala
}
2.3 std::unique_lock<>
轻度灵活锁
- 可以提供第二参数:
- std::adopt_lock 假定当前线程已经获得锁,不再请求
- std::defer_lock 表示在构造锁的时候并不上锁,使互斥量保持在解锁状态
- std::try_to_lock 尝试请求锁,但不阻塞线程,锁不可用时也会立即返回
-
如果不提供第二参数,表示构造时同时也上锁,需要手动解锁
1 2 3
std::unique_lock<std::mutex> some_lock(some_mutex); // do something some_lock.unlock();
特点
std::unique_lock
比std::lock_guard
体积大,所以后者如果够用,建议优先使用后者
std::adopt_lock
-
假定当前线程已经获得锁,不再请求
1
std::unique_lock<std::mutex> some_lock(some_mutex, std::adopt_lock);
std::defer_lock
- 表示在构造锁的时候并不上锁,使互斥量保持在解锁状态
-
std::unique_lock
锁对象可以传给lock()
对象作为参数1 2 3 4 5
std::unique_lock<std::mutex> some_lock(some_mutex, std::defer_lock); std::lock(some_lock); some_lock.lock(); some_lock.unlock(); some_lock.try_lock();
2.4 std::shared_lock<>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 读操作函数
void readData(int id) {
std::shared_lock<std::shared_mutex> readLock(rwMutex);
std::cout << "Thread " << id << " is reading data: ";
for (int val : sharedData) {
std::cout << val << " ";
}
std::cout << std::endl;
}
// 写操作函数
void writeData(int value) {
std::unique_lock<std::shared_mutex> writeLock(rwMutex);
std::cout << "Writing data: " << value << std::endl;
sharedData.push_back(value);
}
Contact
Feel free to contact me windmillyucong@163.com anytime for anything.