8、介绍一下几种典型的锁【中高频】

文章目录

1.普通互斥量(也叫 互斥锁)(std:: mutex)2.递归互斥量(std:: recursive_mutex)3.定时互斥量(std::timed_mutex)4.定时递归互斥量(std::recursive_timed_mutex)5.读写锁(std::shared_mutex) C++17开始才有6.自旋锁(通常用 std::atomic_flag 实现) C++117.悲观锁与乐观锁

1.普通互斥量(也叫 互斥锁)(std:: mutex)

注意,线程函数调用 lock() 时,可能会发生以下三种情况:

如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock) 线程函数调用 try_lock() 时,可能会发生以下三种情况:

如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量

如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉

如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

2.递归互斥量(std:: recursive_mutex)

递归互斥锁 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权。释放互斥量时需要调用相同次数的 unlock()。除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同

这对于递归函数 可能需要在同一线程中多次获取锁的情况很有用:

#include

#include

#include

std::recursive_mutex myRecursiveMutex;

void recursiveAccess(int depth) {

std::unique_lock 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 lock(sharedMutex); //共享锁,即读锁

//std::unique_lock lock(sharedMutex); //独占锁,即写锁

#include

#include

#include

#include

std::shared_mutex smtx;

int shared_data = 0;

void read_data()

{

std::shared_lock lock(smtx);

std::cout << "Read data: " << shared_data << std::endl;

}

void write_data(int new_value)

{

std::unique_lock lock(smtx);

shared_data = new_value;

std::cout << "Wrote data: " << shared_data << std::endl;

}

int main()

{

std::vector 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

#include

#include

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.悲观锁与乐观锁

悲观锁:

前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。

悲观锁做事比较悲观,也就是未雨绸缪。它认为 多线程同时访问共享资源的概率 比较高,很容易出现冲突,所以要先上锁,才能 访问共享资源

那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。

乐观锁:

乐观锁做事比较乐观,它假定 冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突。如果 没有其他线程 在修改资源,则修改完成;如果发现有其他线程同时修改这个资源,就放弃本次操作。

可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。比如在线文档 就是 乐观锁

乐观锁虽然全程并没有加锁,但是一旦发生冲突,重试的成本非常高。所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。