C++多线程(二)---- mutex互斥量、lock_guard<mutex>、unique_lock<mutex>的使用和各种方式加锁的区别(详细解析~~~)

1、资源竞争

在多线程编程中,资源竞争是一种常见的问题,指多个线程同时访问或修改共享资源,而没有适当的同步机制。资源竞争可能导致不确定的行为、数据损坏和程序崩溃。

死锁:发生死锁时,多个线程相互等待对方释放资源,导致程序无法继续执行。

2、std::mutex

互斥访问共享资源:多个线程同时读写共享变量可能导致数据不一致。使用互斥量(std::mutex)来保护共享资源,确保同一时间只有一个线程访问共享资源。

  • 2.1 第一种加锁方式
/*
	No.1 第一种加锁方式
	mutex类(互斥量) 创建mutex类的对象
	1.1 通过调用lock函数进行加锁
	1.2 通过调用unlock进行解锁
	注意点: lock必须与unlock成对出现
*/
class SeaKing
{
public:
	void makeFriend()
	{
		for (int i = 0; i < 10000; i++)
		{
			m_mutex.lock();        //加锁
			printf("增加一个女朋友:%d", i);
			mm.push_back(i);
			m_mutex.unlock();      //解锁
		}
	}
	void breakUp()
	{
		for (int i = 0; i < 10000; i++)
		{
			if (!mm.empty())
			{
				m_mutex.lock();        //加锁
				printf("分手一个女朋友:%d\n", mm.back());
				mm.pop_back();
				m_mutex.unlock();      //解锁
			}
			else
			{
				printf("海王变单身狗!\n");
			}
		}
	}
protected:
	list<int> mm;        //共享数据
	mutex m_mutex;				//首先第一步构建一个互斥量对象
};

int main()
{
	SeaKing man;
	thread t1(&SeaKing::makeFriend, &man);  //创建两个线程
	thread t2(&SeaKing::breakUp, &man);
	t1.join();
	t2.join();
	return 0;
}

不加锁的情况下,代码运行结果,程序运行不稳定
在这里插入图片描述
加入互斥锁后,代码运行结果,程序运行稳定
在这里插入图片描述

  • 2.1 第二种加锁方式 std::lock_guard()
/*
	No.2 第二种加锁方式
	lock_guard 对象实现加锁    
		lock_guard类的构造函数中调用lock函数
		lock_gurad类的析构函数调用了unlock函
		lock_guard本质上还是调用mutex的lock()和un_lock()函数
*/
class SeaKing
{
public:
	void makeFriend()
	{
		lock_guard<mutex> lgObject(m_mutex);
		for (int i = 0; i < 10000; i++)
		{
			//m_mutex.lock();        //加锁
			printf("增加一个女朋友:%d", i);
			mm.push_back(i);
			//m_mutex.unlock();      //解锁
		}
	}
	void breakUp()
	{
		for (int i = 0; i < 10000; i++)
		{
			if (!mm.empty())
			{
				lock_guard<mutex> lgObject(m_mutex);
				//m_mutex.lock();        //加锁
				printf("分手一个女朋友:%d\n", mm.back());
				mm.pop_back();
				//m_mutex.unlock();      //解锁
			}
			else
			{
				printf("海王变单身狗!\n");
			}
		}
	}
protected:
	list<int> mm;        //共享数据
	mutex m_mutex;				//首先第一步构建一个互斥量对象
};

int main()
{
	SeaKing man;
	thread t1(&SeaKing::makeFriend, &man);
	thread t2(&SeaKing::breakUp, &man);
	t1.join();
	t2.join();
	return 0;
}

3、std::unique_lock()

在 C++ 中,std::unique_lock 是一个线程安全的互斥量封装类,用于管理互斥量的加锁和解锁操作。它提供了更灵活的锁定机制,并允许在不同的作用域内锁定和解锁互斥量。

  • 包含头文件 ,声明一个 std::mutex 作为互斥量,并创建一个 std::unique_lock 对象来管理互斥量的加锁和解锁操作。
  • 使用 std::unique_lock 对象来锁定互斥量。可以通过构造函数锁定互斥量,或者使用 lock() 成员函数手动锁定 std::unique_lock 会在其作用域结束时自动释放互斥量的锁定。如果需要提前解锁,可以使用 unlock() 成员函数。通过使用 std::unique_lock,可以更灵活地控制互斥量的加锁和解锁操作,并在需要时进行手动解锁或重新锁定。这对于实现更复杂的锁定逻辑和避免死锁非常有用。
  • 需要注意的是,std::unique_lock 对象是非拷贝构造的,但可以通过移动构造。因此,通常会使用引用或指针传递 std::unique_lock 对象,以便在函数间传递和共享互斥量的锁定状态
  • 除了使用默认参数之外,std::unique_lock 还可以接受其他参数来调整其行为。以下是 std::unique_lock 的一些常见参数:
  • 下面看一下unique_lock类的源码
    在这里插入图片描述

3.1 std::adopt_lock

使用 std::adopt_lock 参数时,你需要确保在创建 std::unique_lockstd::lock_guard 对象之前,已经手动锁定了相应的互斥量。
这通常用于在同一线程中的不同作用域内对互斥量进行多次加锁,并确保在作用域结束时正确解锁

std::mutex mtx;
mtx.lock();  // 手动锁定互斥量,使用std::adopt_lock参数,互斥量必须先进行lock()
{
	std::unique_lock<std::mutex> lock(mtx, std::adopt_lock);   //使用std::adopt_lock参数
	// 互斥量已经被其他方式锁定,无需再次锁定
	// 在此作用域内对互斥量进行操作
	// lock 在作用域结束时会自动解锁互斥量  析构函数会解锁互斥量
}
// 在此继续使用互斥量
mtx.unlock();  // 手动解锁互斥量

3.2 std::defer_lock

std::defer_lockstd::unique_lock 的构造函数参数之一,用于指示在构造锁定对象(std::unique_lock)时不立即锁定互斥量,而是延迟锁定操作。使用 std::defer_lock 参数时,你需要手动调用 lock()` 成员函数来显式地锁定互斥量。这通常用于在构造锁定对象后的特定位置或条件下,根据需要选择性地锁定互斥量。

std::mutex mtx;

std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 互斥量没有被锁定

// 其他操作

lock.lock();    // 手动锁定互斥量

// 在此作用域内对互斥量进行操作
// lock 在作用域结束时会自动解锁互斥量
  • 使用了 std::defer_lock 参数,这样互斥量 mtx 并没有被锁定。然后,根据需要,在特定位置调用 lock() 成员函数手动锁定互斥量。在lock() 调用之后的作用域内,我们可以安全地对互斥量进行操作,直到在作用域结束时 lock 对象会自动解锁互斥量。
  • 使用 std::defer_lock 允许我们灵活地控制互斥量的锁定时机,从而在需要时选择性地锁定互斥量,而不是在构造锁定对象时立即锁定。这对于需要根据特定条件进行互斥操作的情况非常有用。

3.3 std::try_to_lock

  • std::try_to_lock参数作用:锁管理器在构造的时候尝试lock;如果lock上,锁管理器就拥有可锁对象(持有锁),析构的时候自动执行解锁;否则就不持可锁对象,析构的时候也就不会解锁;它的好处是当某个线程尝试获取该该锁,但是该锁已经已被其他线程持有,那么不会出现线程被阻塞挂起。
  • std::try_to_lock 是 std::unique_lock 用于指定在构造锁定对象时尝试非阻塞地获取互斥量的所有权,使用 std::try_to_lock 参数时,构造函数会尝试立即获取互斥量的所有权,但如果互斥量已经被其他线程锁定,则不会阻塞当前线程,而是立即返回
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);

if (lock.owns_lock()) {
	// 成功获取互斥量的所有权
	// 在此作用域内对互斥量进行操作
	// lock 在作用域结束时会自动解锁互斥量
} else {
	// 未能获取互斥量的所有权
	// 可以在此处理无法获取锁的情况
}
  • 在上面的示例中,我们使用 std::try_to_lock 参数来构造 std::unique_lock 对象 lock,并将其作为第二个参数传递给构造函数。构造函数会立即尝试获取互斥量的所有权,但如果互斥量已经被其他线程锁定,则不会阻塞当前线程,而是立即返回。

  • 通过调用 lock.owns_lock() 成员函数,我们可以检查是否成功获取互斥量的所有权。如果 owns_lock() 返回 true,表示成功获取了互斥量的所有权,可以在作用域内对互斥量进行操作。如果 owns_lock() 返回 false,表示无法获取互斥量的所有权,可以在此处理无法获取锁的情况。使用 std::try_to_lock 可以实现非阻塞地尝试获取互斥量的所有权,适用于需要在无法立即获取锁时执行备选操作的情况。

4、std::unique_lock 和 std::lock_guard 的区别

std::unique_lock 和 std::lock_guard 都是 C++ 中用于管理互斥量的 RAII(Resource Acquisition Is Initialization)类。它们的目的是确保在作用域结束时自动释放互斥量,以避免忘记手动解锁。

区别:

  1. 灵活性:std::unique_lock 提供了更大的灵活性。你可以在任何时候手动锁定或解锁互斥量,也可以选择延迟锁定或尝试锁定。而 std::lock_guard 则是在构造时锁定互斥量,在析构时解锁,没有手动解锁的选项。

  2. 所有权传递:std::unique_lock 是可移动的,可以通过移动构造函数传递所有权,从而可以将 std::unique_lock 对象传递给其他函数或线程。而 std::lock_guard 没有移动构造函数,只能在其作用域内使用。

  3. 条件变量支持:std::unique_lock 可以与条件变量(std::condition_variable)一起使用,它提供了更灵活的等待和通知机制。而std::lock_guard 不能直接与条件变量一起使用,因为它没有提供相应的成员函数。

  4. 多个互斥量:std::unique_lock 可以同时管理多个互斥量,通过构造函数和成员函数 lock()try_lock() 可以实现对多个互斥量的加锁和解锁操作。而 std::lock_guard 只能管理单个互斥量。

  5. 构造std::unique_lock时有3个参数(std::adopt_lock,std::defer_lock,std::try_to_lock)可以选择,std::lock_guard只有一个参数(std::adopt_lock可以选择.

一般来说,如果你只需要简单地对单个互斥量进行加锁和解锁,并且不需要手动解锁、条件变量支持或传递所有权,那么 std::lock_guard 是更简单和推荐的选择。它更加轻量且易于使用。如果你需要更多的灵活性,例如手动解锁、延迟锁定、尝试锁定、条件变量支持或传递所有权等,那么 std::unique_lock 是更适合的选择。它提供了更多的功能和控制,但相应地会增加一些开销。


Time:2023.5.21 (周日)
如果上面代码对您有帮助,欢迎点个赞!!!

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
在给定的代码示例中,`std::mutex`,`std::unique_lock`和`std::lock_guard`是C++中用于实现线程同步的标准库类。它们可以用于保护共享资源,以确保在多个线程同时访问该资源时不会出现竞争条件。 `std::mutex`是一个互斥体,用于实现互斥锁。它可以在需要时锁定共享资源,并在不再需要访问时解锁它。在给定的代码示例中,`std::mutex mt`是一个互斥体对象。 `std::unique_lock`和`std::lock_guard`都是互斥锁的RAII(资源获取即初始化)封装类。它们提供了一种便捷的方式来锁定和解锁互斥锁。不同之处在于,`std::unique_lock`提供了更多的灵活性,可以在运行时选择锁定和解锁的时机,而`std::lock_guard`在构造时锁定互斥锁,在析构时解锁互斥锁。在给定的代码示例中,`std::lock_guard`和`std::unique_lock`都用于在执行特定代码块时锁定互斥锁,并在代码块结束时解锁它们。 在混用`std::mutex`,`std::unique_lock`和`std::lock_guard`时,可以根据具体的需求选择合适的组合。例如,如果您需要在代码块中灵活地锁定和解锁互斥锁,可以使用`std::unique_lock`。如果您只需要在代码块中锁定和解锁互斥锁,并且不需要灵活性,可以使用`std::lock_guard`。这些类可以根据需要混合使用,以提供更具表达力和适应性的线程同步代码。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++11多线程编程——lock_guardunique_lock](https://blog.csdn.net/m0_47313541/article/details/130480617)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值