C++:mutex库

一、C++的mutex库
互斥量
C++11中提供了std::mutex互斥量,共包含四种类型:
std::mutex:最基本的mutex类。
std::recursive_mutex:递归mutex类,能多次锁定而不死锁。
std::time_mutex:定时mutex类,可以锁定一定的时间。
std::recursive_timed_mutex:定时递归mutex类。

1、mutex
在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题。因此就需要引入锁的机制,来保证任意时候只有一个线程在访问公共资源。
首先我们来看这样一段代码:

#include<thread>
#include<mutex>
#include<iostream>

void Add(int* px, int n)
{
	for (int i = 0; i < n; i++)
	{
		(*px)++;
	}
}

void Fun()
{
	int x = 0;
	std::cout << "Before Add:x=" << x << std::endl;
	std::thread t1(Add, &x, 10000000);
	std::thread t2(Add, &x, 10000000);
	t1.join();
	t2.join();
	std::cout << "After Add:x=" << x << std::endl;
	std::cout << "----------------------------"<<std :: endl;
}
int main()
{
	for (int i = 0; i < 5; i++){
		std::cout << "--------AddNum =" << i <<"-----------"<< std::endl;
		Fun();
	}
	system("pause");
	return 0;
}

// 执行结果
--------AddNum =0-----------
Before Add:x=0
After Add:x=15703953
----------------------------
--------AddNum =1-----------
Before Add:x=0
After Add:x=12994172
----------------------------
--------AddNum =2-----------
Before Add:x=0
After Add:x=14373455
----------------------------
--------AddNum =3-----------
Before Add:x=0
After Add:x=12553528
----------------------------
--------AddNum =4-----------
Before Add:x=0
After Add:x=13493504
----------------------------

在上面的代码中,我么起两个线程,对变量x加加,在数据量在小的时候,是没啥问题的,但是数据量比较大的时候,在我的机器d到达千万次时,就会出现问题,而且多次执行,每次的结果都是不同的。

那这个时候我们就想到了加锁来解决,但是加锁有产生了两种不同的加锁方式
方式一:

void Add1(int* px, int n,std::mutex* pmt)
{
	(*pmt).lock();
	for (int i = 0; i < n; i++)
	{
		(*px)++;
	}
	(*pmt).unlock();
}

方式二

void Add2(int* px, int n, std::mutex* pmt)
{
	for (int i = 0; i < n; i++)
	{
		(*pmt).lock();
		(*px)++;
		(*pmt).unlock();
	}
}

然后我们执行下面的代码,对这两种不同的加锁方式进行计时;

#include<thread>
#include<mutex>
#include<iostream>
#include<ctime>

void Add1(int* px, int n,std::mutex* pmt)
{
	(*pmt).lock();
	for (int i = 0; i < n; i++)
	{
		(*px)++;
	}
	(*pmt).unlock();
}

void Add2(int* px, int n, std::mutex* pmt)
{
	for (int i = 0; i < n; i++)
	{
		(*pmt).lock();
		(*px)++;
		(*pmt).unlock();
	}
}

void Fun1()
{
	int x = 0;
	std::cout << "Before Add:x=" << x << std::endl;
	std::mutex mt;
	std::thread t1(Add1, &x, 10000000,&mt);
	std::thread t2(Add1, &x, 10000000,&mt);
	t1.join();
	t2.join();
	std::cout << "After Add:x=" << x << std::endl;
}
void Fun2()
{
	int y = 0;
	std::cout << "Before Add:y=" << y << std::endl;
	std::mutex mt;
	std::thread t1(Add2, &y, 10000000, &mt);
	std::thread t2(Add2, &y, 10000000, &mt);
	t1.join();
	t2.join();
	std::cout << "After Add:y=" << y << std::endl;
}
int main()
{
	int start1 = clock();
	Fun1();
	int end1 = clock();
	std::cout << "Fun1 Time Spend:" << end1 - start1 << std::endl;

	int start2 = clock();
	Fun2();
	int end2 = clock();
	std::cout << "Fun2 Time Spend:" << end2 - start2 << std::endl;
	system("pause");
	return 0;
}
// 执行结果
Before Add:x=0
After Add:x=20000000
Fun1 Time Spend:89
Before Add:y=0
After Add:y=20000000
Fun2 Time Spend:7795

可以看出相同的代码,由于加锁的粒度不同的,其执行的时间的差异是非常大的。

这是因为,第一种加锁方式中,第一个线程进去后,另一个线程阻塞休眠,在第一个线程完成循环后,在唤醒另外一个线程。而第二个线程则是在循环中加加一次后,有可能就会让另外一个线程执行。而我们知道CPU的运算速度是非常快的,频繁的加锁解锁,进行线程的切换(cpu进程调度:需要保存当前进程的上下文)都需要耗费时间,因此第二种加锁方式相比于第一种会慢很多。

而在上面的程序中我么使用的是互斥锁,而互斥锁是适用于锁的粒度比较大的场景使用的,而对于锁的粒度比较小的场景,我们可以使用自旋锁。

那互斥所和自旋锁有什么区别呢?其各自的使用场景是怎样的?
互斥锁:
lock() :第一个线程进去后,后面的线程会进入阻塞休眠状态,被放入到阻塞队列中。
unlock():加锁的线程执行完后,会解锁,然后去阻塞队列中唤醒阻塞线程
适用于锁的粒度大的场景

自旋锁:
lock():第一个线程进去,后面的线程在循环空转检查。
unlock():第一个加锁的线程解锁,后面的线程检测到就可以被cpu调度。

举个例子就很好理解:快递。
互斥锁就好比,你去取快递的地方问了一下,你的快递还有1天才能到,你就可以先把电话留在取快递的地方,等你的快递到了,快递公司的工作人员会打电话通知你,而这种场景如果使用自旋锁,就好比每隔几分钟就去询问快递工作人员,我觉得快递人员只能说:what’s your problem?(宏颜祸水)。

自旋锁就像,你去问的时候,你的快递还有大概20分钟就到了,那这20分钟也干不了啥,还不如就在快递点等着,玩玩手机,每隔5分钟去问一下,快递到了没。而这种场景如果使用互斥锁,那你就会觉的还有20分钟,不如回家等着快递人员给我打电话吧,结果你刚走到家门口,把钥匙插入锁,快递公司打电话告知:你的快递到了。

而类比到计算机:使用互斥锁就好比让其他线程阻塞,进入阻塞队列,就类似你回家等通知。
使用自旋锁就让其它线程空转(比如可以给该线程设置在一定时间段内让出cpu使用权),每隔10毫秒检查一次,就类似你在快递点的门口等待,每隔5分钟去问一下,快递到了没。

2、recursive_mutex
先来看一段代码:

std::mutex mt;  

int n = 10;

void Fun()
{
	if (n == 0)
		return;
	mt.lock();
	n -= 1;
	Fun();
	mt.unlock();
}
int main()
{
	Fun();
	std::cout << n << std::endl;
	system("pause");
	return 0;
}
// 运行结构
0

在上面的程序中,Fun函数中有调用Fun()函数,会对mt多次加锁。而std::mutex是个非递归锁,这个程序会立刻死锁。而这时候使用mutex库中的recursive_mutex就可以避免程序死锁。

而递归锁的实现,也比较简单,只需要在非递归锁的基础上,引入引用计数就可以解决了。下面模拟实现一个简单的递归锁。

#include<iostream>
#include<chrono>
#include<thread>
#include<mutex>
#include<atomic>

int g_num = 0;
std::recursive_mutex rmt;

class RecursiveMutex
{
public:
	RecursiveMutex()
		:lock_nums(0)
	{
	}
	~RecursiveMutex() {}
	void lock(){
		if (lock_nums == 0){
			my_mutex.lock();
			owner_thread_id = std::this_thread::get_id();
			lock_nums++;
		}
		else if (std::this_thread::get_id() == owner_thread_id)
			lock_nums++;
		else{
			my_mutex.lock();
			owner_thread_id = std::this_thread::get_id();
			lock_nums++;
		}
	}
	void unlock()
	{
		if (lock_nums > 0)
			lock_nums--;
		if (lock_nums == 0)
			my_mutex.unlock();
	}

private:
	int lock_nums = 0;
	std::mutex my_mutex;
	std::thread::id owner_thread_id;
};

RecursiveMutex RMT;
std::lock_guard<RecursiveMutex> lg(RMT);

void SlowAddStl(int id)
{
	for (int i = 0; i < 3; i++){
		rmt.lock();
		++g_num;
		std::cout << id << "=>" << g_num << std::endl;
		rmt.unlock();
		std::this_thread::sleep_for(std::chrono::seconds(2));
	}
}

void SlowAdd(int id)
{
	for (int i = 0; i < 3; i++){
		RMT.lock();
		++g_num;
		std::cout << id << "=>" << g_num << std::endl;
		RMT.unlock();
		std::this_thread::sleep_for(std::chrono::seconds(2));
	}
}

int main()
{
	std::cout << "使用stl中的recursive_mutex" << std::endl;
	std::thread t1(SlowAddStl, 0);
	std::thread t2(SlowAddStl, 1);
	t1.join();
	t2.join();

	std::cout << "使用自己实现的RecursiceMutex" << std::endl;
	std::thread t3(SlowAdd, 0);
	std::thread t4(SlowAdd, 1);
	t3.join();
	t4.join();
	system("pause");
	return 0;
}

程序运行结果:
在这里插入图片描述
此外还有timed_mutex和recursive_timed_mutex分别是可以设置时间的非递归互斥量和递归互斥量,有时间在演示吧。

C++11中提供了std::mute锁,共包含两种类型:
std::lock_guard:方便线程对互斥量上锁。
std::unique_lock:方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。
以及相关的函数:

1、lock_guard
先来看一段代码:

#include<iostream>
#include<thread>
#include<mutex>
#include<string>

std::mutex mt;

void PrintEven(int x)
{
	if (x % 2 == 0)
		std::cout << x << "is even\n";
	else
		throw ("not even");
}
void PrintThreadId(int id)
{
	try{
		mt.lock();
		PrintEven(id);
		mt.lock();
	}
	catch (std::string& e)
	{
		std::cout << "[" << e << "]\n";
	}
}
int main()
{
	std::thread threads[5];
	for (int i = 0; i < 5; i++)
		threads[i] = std::thread(PrintThreadId, i + 1);
	for (auto& th : threads)
		th.join();
	return 0;
}

上面的程序中会崩溃,因为线程在执行PrintThreadId函数时,先加锁,然后在调用PrintEven函数,有可能会抛出异常,而在PrintThreadID函数中捕获异常,导致该线程并没有解锁,导致死锁问题。

那这个时候就可以使用lock_guard解决上面的问题。lock_guard是基于RAII思想的一种产物,即把锁的生命周期交给lcok_guard实例化的对象去管理。而且在lock_guard类只有两个成员函数,构造和析构函数。
在这里插入图片描述
只需要对代码做出如下的修改就好了。

void PrintThreadId(int id)
{
	try{
		std::lock_guard<std::mutex> lg(mt);
		PrintEven(id);
	}
	catch (...)
	{
		std::cout << "[exception caught]\n";
	}
}

2、unique_lock
unique_lock相比于lock_guard,都是基于RAII思想的,也支持std::lock_guard的功能,但是区别在于它提供更多的成员函数,比如:lock(),unlock()使用更加灵活,并且可以和condiction_variable一起使用控制线程同步。

#include <iostream>
#include <thread>    
#include <mutex>      

std::mutex mtx;  

void print_block (int n, char c) {
  std::unique_lock<std::mutex> lck (mtx);
  for (int i=0; i<n; ++i) { std::cout << c; }
  std::cout << '\n';
}

int main ()
{
  std::thread th1 (print_block,50,'*');
  std::thread th2 (print_block,50,'$');

  th1.join();
  th2.join();

  return 0;

还提供了这些函数:
std::try_lock:尝试同时对多个互斥量上锁
std::lock:可以同时对多个互斥量上锁
std::call_one:如果多个线程需要同时调用某个函数,call_one可以保证多个线程对该函数只调用一次.

  • 36
    点赞
  • 171
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值