文章目录
1.普通互斥量(也叫 互斥锁)(std:: mutex)
-
注意,线程函数调用 lock() 时,可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
-
线程函数调用 try_lock() 时,可能会发生以下三种情况:
-
如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
-
如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
-
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
-
2.递归互斥量(std:: recursive_mutex)
-
递归互斥锁 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权。释放互斥量时需要调用相同次数的 unlock()。除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同
-
这对于递归函数 可能需要在同一线程中多次获取锁的情况很有用:
#include <iostream>
#include <mutex>
#include <thread>
std::recursive_mutex myRecursiveMutex;
void recursiveAccess(int depth) {
std::unique_lock<std::recursive_mutex> lock(myRecursiveMutex);
if (depth > 0) {
recursiveAccess(depth - 1);
}
// 访问共享资源的代码
std::cout << "Accessing shared resource at depth " << depth << "...\n";
}
int main() {
std::thread t1(recursiveAccess, 3);
t1.join();
return 0;
}
3.定时互斥量(std::timed_mutex)
-
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() :
- try_lock_for():函数参数表示一个时间范围,在这一段时间范围之内线程如果没有获得锁 则保持阻塞;如果在此期间其他线程释放了锁,则该线程可获得该互斥锁;如果超时(指定时间范围内没有获得锁),则函数调用返回false。
timed_mutex myMutex;
chrono::milliseconds timeout(100); //100毫秒
if (myMutex.try_lock_for(timeout))
{
//在100毫秒内获取了锁
//业务代码
myMutex.unlock(); //释放锁
}
else
{
//在100毫秒内没有获取锁
//业务代码
}
- try_lock_until():函数参数表示一个时刻,在这一时刻之前线程如果没有获得锁则保持阻塞;如果在此时刻前其他线程释放了锁,则该线程可获得该互斥锁;如果超过指定时刻没有获得锁,则函数调用返回false。
4.定时递归互斥量(std::recursive_timed_mutex)
- 允许同一线程多次获取锁,并提供了超时功能。与std::timed_mutex一样,std::recursive_timed_mutex也提供了try_lock_for()和try_lock_until()方法
5.读写锁(std::shared_mutex) C++17开始才有
-
读写锁主要用于区分对共享资源的 读操作 和 写操作。它有两种获取模式:共享模式(读模式)和独占模式(写模式)。
-
读写锁机制:多个线程可以共同读取一共共享资源,也就是共享读;但只有一个线程可以写入也就是修改这个共享资源,其他线程都会阻塞,既不能写,也不能读。总之,当一个线程获取 读锁 的时候,其他线程可以获取读锁,但不能获取写锁;当一个线程获取 写锁 的时候,其他线程既不能获取读锁,也不能获取写锁。
-
有 大量读操作 和 少量写操作 时,可以提高程序的并发性能。例如,在一个缓存系统中,多个线程可能经常读取缓存中的数据,只有在缓存数据需要更新时才会进行写操作,使用读写锁可以很好地处理这种情况。
//shared_mutex 支持共享锁和独占锁,
//std::shared_lock<std::shared_mutex> lock(sharedMutex); //共享锁,即读锁
//std::unique_lock<std::shared_mutex> lock(sharedMutex); //独占锁,即写锁
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
std::shared_mutex smtx;
int shared_data = 0;
void read_data()
{
std::shared_lock<std::shared_mutex> lock(smtx);
std::cout << "Read data: " << shared_data << std::endl;
}
void write_data(int new_value)
{
std::unique_lock<std::shared_mutex> lock(smtx);
shared_data = new_value;
std::cout << "Wrote data: " << shared_data << std::endl;
}
int main()
{
std::vector<std::thread> read_threads;
for (int i = 0; i < 5; i++)
{
read_threads.push_back(std::thread(read_data));
}
std::thread write_thread(write_data, 10);
for (auto& t : read_threads)
{
t.join();
}
write_thread.join();
return 0;
}
6.自旋锁(通常用 std::atomic_flag 实现) C++11
-
与传统的互斥锁(Mutex)不同,当线程没有获得锁时 并不会阻塞,而是在一个循环中不断检查锁的状态,直到成功获取锁。这种行为被称为“自旋”。
-
优缺点:
- 自旋锁获取锁时 不需要进行上下文切换,节省开销。
- 但是,如果循环等待的时间过长,线程会一直占用 CPU 资源(进行检查),从而导致 CPU 资源的浪费。所以自旋锁并不适合长时间等待
//std::atomic_flag 可以实现无锁编程,它只能进行两种操作:获取锁(test_and_set())和释放锁(clear())。
//test_and_set(): 设置标志为true,但返回之前的值,即获取锁
//clear(): 将标志设置为false,即释放锁
/*
bool test_and_set(bool *target)
{
bool rv = *target;
// 通过地址寻址,真实地修改target的值
*target = true;
// 返回的是传入时target的值
return rv;
}
*/
#include <atomic>
#include <thread>
#include <iostream>
class SpinLock {
private:
std::atomic_flag flag = ATOMIC_FLAG_INIT; // 初始化为清除状态即false(无人获取锁)
public:
void lock() {
//只有memory_order_acquire为false,表示还没有线程获取锁,所以当前线程可以跳出循环,也就是获取锁。
//当memory_order_acquire为true时,就相当于有线程已经获取锁了,当前线程要阻塞(即自旋)
while (flag.test_and_set(std::memory_order_acquire)) {
// 忙等待,直到获得锁
}
}
void unlock() {
flag.clear(std::memory_order_release);//释放锁
}
};
SpinLock spinlock;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
spinlock.lock();
counter++;
spinlock.unlock();
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl;
return 0;
}
7.悲观锁与乐观锁
悲观锁:
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。
-
悲观锁做事比较悲观,也就是未雨绸缪。它认为 多线程同时访问共享资源的概率 比较高,很容易出现冲突,所以要先上锁,才能 访问共享资源
-
那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。
乐观锁:
乐观锁做事比较乐观,它假定 冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突。如果 没有其他线程 在修改资源,则修改完成;如果发现有其他线程同时修改这个资源,就放弃本次操作。
-
可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。比如在线文档 就是 乐观锁
-
乐观锁虽然全程并没有加锁,但是一旦发生冲突,重试的成本非常高。所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。