多线程————lock_guard和unique_lock

1.前言

mutex 头文件除之前讲的 mutex (互斥锁)和 timed_mutex (定时互斥锁),还有 lock_guard 和 unique_lock 模板类,用于管理互斥锁的生命周期(互斥锁何时被创建,何时被销毁)。他们符合RAII规则。

RAII 是 Resource Acquisition Is Initialization 的缩写,即“资源获取即初始化”。它是C++语言的一种管理资源、避免资源泄漏的惯用法,利用栈对象自动销毁的特点来实现。

可以理解为他会在你忘记解锁互斥锁或者出现异常时,在他的作用域结束后自动解锁。当你在编写一个庞大的代码时,自动解锁可以帮你规避一些忘记解锁产生的问题(如死锁等)。

以下是代码会用到的头文件

//使用c++14规则
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

2.lock_guard的使用

2.1lock_guard基础

lock_guard 是不可移动,不可复制的。他无法手动解锁(没有用于解锁的函数),所有的加锁和解锁都被 lock_guard 所接管。

他还有个构造函数

lock_guard(_Mutex& _Mtx, adopt_lock_t) noexcept // noexcept:保证构造函数不抛出异常,确保资源安全。
    : _MyMutex(_Mtx) {} // 构造但不加锁

用于接管已经上锁的互斥锁。注意!!!若未先加锁直接使用此构造函数,会导致未定义行为。

2.2lock_guard的实例

下面来看一段代码

int count1 = 0, count2 = 0;	//共享变量
std::mutex mtx;	//互斥锁
void thread_func(int times) {
	{
		std::lock_guard<std::mutex> lock(mtx);	//传入了mutex类型锁mtx
		for (int i = 0; i < times; i++) {
			count1++;
		}
	}//lock_guard的作用域
	for (int i = 0; i < times; i++) {
		count2++;
	}
}
  • 可以看到我创建了两个共享变量 count1 和 count2,count1 的自增操作是 lock_guard 的作用域中,而 count2 不在作用域中。({ }代表 lock_guard 的作用域)。

  • 在 lock_guard 的< >(尖括号)中,我传入了 mutex 类型。由源码来看,lock_guard 使用了传入的互斥锁类型的 lock() 函数。也就是说我传入了 mutex 类型的锁,他就会调用了 mutex 类型的 lock() 函数。也就是说 lock_guard 并不支持更复杂的 try_lock_for() 等函数。

  • 其中的 lock(mtx) 这段我将 mutex 类型的锁 mtx 传入,构造一个名为 lock 的 lock_guard 对象.以 对象名(实参)的方式构造了 lock_guard 对象。

现在我加入如下主函数

int main() {
	int times = 100000;
	std::thread t1(thread_func, times);
	std::thread t2(thread_func, times);
	t1.join();
	t2.join();
	std::cout << "count1:" << count1 << std::endl;
	std::cout << "count2:" << count2 << std::endl;
	return 0;
}

可以看到类似以下的结果,因为 count2 不在 lock_guard 作用域中,所以得出的数据出现问题,并非200000.

count1:200000
count2:164513

3.unique_lock的使用

3.1unique_lock基础

unique_lock 的出现其实是为了解决使用 lock_guard 会产生的一些问题。
  1. 锁的粒度过大,上锁的代码越少(粒度越小)。因为 lock_guard 无法手动解锁,所以可能导致 lock_guard 作用域过大,影响性能(失去了多线程并发的优势)。

  2. 无法指定尝试获得锁,也就是非阻塞的尝试获取锁,lock_guard 没获得锁会阻塞。

  3. 没有超时机制,也就是 try_lock_for() ,try_lock_until() 函数获取锁

  4. 不支持条件变量和递归锁

3.2unique_lock参数

unique_lock在构建时更灵活。并且支持移动(可转移所有权),它提供了更多的操作,通过向unique_lock传递第二个参数来调用不同的构造函数。以下是几个传入的参数

  1. std::adopt_lock,表示要求 unique_lock 接管一个已被当前线程锁定的互斥量。

  2. std::defer_lock,表示要求 unique_lock 暂不锁定互斥量,允许后续手动控制加锁和解锁时机。

  3. std::try_to_lock,表示要求 unique_lock 尝试非阻塞地锁定互斥量。若成功则持有锁,否则不阻塞线程。也就是说程序不会在获取锁这一动作时停止,而是会继续执行下去

  4. 在传入 std::timed_mutex 类型的情况下,第二个参数可以传入一个时间范围,相当于调用 try_lock_for() 函数。

  5. 在传入 std::timed_mutex 类型的情况下,第二个参数可以传入一个时间点,相当于调用 try_lock_until() 函数。

以下演示几个参数的使用方法

int count = 0;	//共享变量
std::timed_mutex tmtx;	//定时互斥锁
void thread_func(int times) {
	std::unique_lock<std::timed_mutex> lock(tmtx, std::chrono::milliseconds(200));	//在200毫秒的时间间隔内上锁
	for (int j = 0; j < times; j++) {
		count++;
	}
}
int count = 0;	//共享变量
std::mutex mtx;	//互斥锁
void thread_func(int times) {
	mtx.lock();	//上锁
	std::unique_lock<std::mutex> lock(mtx,std::adopt_lock);	//接管已经上锁的互斥锁
	for (int j = 0; j < times; j++) {
		count++;
	}
}
int count = 0;	//共享变量
std::mutex mtx;	//互斥锁
void thread_func(int times) {
	std::unique_lock<std::mutex> lock(mtx,std::defer_lock);	//不会立即上锁
	for (int j = 0; j < times; j++) {
		lock.lock();	//上锁
		count++;
		lock.unlock();	//解锁
	}
}

下面演示一个比较特殊的,不阻塞线程的获取锁

void thread_func(int i) {
	std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);	//尝试上锁
	if (lock.owns_lock()) {//如果获得了锁
        std::cout << "thread_func: lock success" << std::endl;
		for (int j = 0; j < i; j++) {
			count++;
		}
	}
	else {
		std::cout << "thread_func: lock failed" << std::endl;
	}
}

使用以下主函数(类似上面的主函数)。

点击查看代码
int main() {
	int times = 100000;
	std::thread t1(thread_func, times);
	std::thread t2(thread_func, times);
	t1.join();
	t2.join();
	std::cout << "count:" << count << std::endl;
	return 0;
}

可以得到如下结果,可以看到其中一个线程在没有得到锁之后直接打印了获取锁失败的语句。

thread_func: lock success
thread_func: lock failed
count:100000

3.3unique_lock函数

unique_lock还有几个成员函数用于管理锁,以下是几个成员函数

  1. lock(),上锁。如果之前在参数中传入 std::defer_lock ,则可以在这里手动上锁。

  2. try_lock(),尝试上锁,如果拿到锁了,则返回 true,否则返回 false。

  3. try_lock_for(),传入一个时间范围,在这时间范围没有获得锁则会阻塞住。超出时间范围则会返回 false,成功返回 true。

  4. try_lock_until(),传入一个时间点,在这时间点到来之前没有获得锁则会阻塞住。超出时间点则会返回 false。

  5. unlock(), unique_lock 可以手动解锁。方便对 unique_lock 作用域的一部分代码进行上锁解锁管理,从而提高效率。

  6. swap(),交换两个 unique_lock 对象的状态(管理的互斥量及其所有权)。

  7. release(),返回它所管理的互斥锁对象指针,并释放所有权。也就是这个 unique_lock 不再和互斥锁绑定,控制这个互斥锁。

  8. owns_lock(),返回这个 unique_lock 管理的互斥锁是否上锁,如果上锁返回 true,否则返回 false。

以下演示几个函数的使用方法

int count = 0;	//共享变量
std::timed_mutex tmtx;	//定时互斥锁
void thread_func(int times) {
	std::unique_lock<std::timed_mutex> lock(tmtx, std::defer_lock);	//不会立即上锁
		if(lock.try_lock_for(std::chrono::seconds(1))){	//尝试在1秒内上锁
			for (int j = 0; j < times; j++) {
				count++;
			}
		}
}
int count = 0;	//共享变量
std::timed_mutex tmtx_1;	//定时互斥锁
std::timed_mutex tmtx_2;	//定时互斥锁
void thread_func(int times) {
	tmtx_2.lock();	//上锁
	std::unique_lock<std::timed_mutex> lock_1(tmtx_1, std::defer_lock);	//不会立即上锁
	std::unique_lock<std::timed_mutex> lock_2(tmtx_2, std::adopt_lock);	//接管已经上锁的互斥锁
	swap(lock_1, lock_2);	//交换锁
	if (lock_1.owns_lock()) {
		for (int j = 0; j < times; j++) {
			count++;
		}
	}
}

unique_lock和lock_guard使用注意

  • unique_lock 的 lock() 和 unlock(),和互斥锁本身的 lock() 和 unlock() 混合使用会导致程序异常,很有可能都会导致崩溃。

  • lock_guard 的速度比 unique_lock 快,占用空间也比 unique_lock 少。

以上为我在学习过程中整理的知识点,如有哪里说错了感谢指出
一部分代码参考【35.C++多线程:unique_lock的用法】https://www.bilibili.com/video/BV1m6421Z79g?vd_source=dccc0abff62c8559f0a5ed0bce39dec2

posted @ 2025-04-08 23:27  散尽天华  阅读(1290)  评论(0)    收藏  举报