C++并发与多线程 创建多个线程、数据问题共享分析、mutex、lock_guard、死锁、std::lock()、std::adopt_loc

创建多个线程和等待多个线程

#include<iostream>
#include <thread>
#include<vector>
using namespace std;

void print(int num)
{
	cout << "print执行,线程编号:" << num << endl;
	cout << "print结束,线程编号:" << num << endl;
	return;
}
int main()
{
	vector<thread>v;
	for (int i = 0; i < 10; ++i)
	{
		v.push_back (thread(print, i));//创建10个线程并开始执行线程
	}
	for (auto iter = v.begin(); iter != v.end(); ++iter)
	{
		iter->join();
	}
	cout << "hello!" << endl;
	return 0;

}

在这里插入图片描述

可以得出结论:

1.多个线程执行的顺序是乱的,跟操作系统内部调度机制有关。

2.主线程等待所有子线程运行结束以后,最后主线程结束,推荐这种写法,更容易写出稳定的程序。


数据共享问题分析

1.只读数据不修改

void print(int num)
{
	cout << "线程id: " << this_thread::get_id() << ", " << vals[0] << vals[1] << vals[2] << endl;
	return;
}
int main()
{
	vector<thread>v;
	for (int i = 0; i < 10; ++i)
	{
		v.push_back (thread(print, i));//创建10个线程并开始执行线程
	}
	for (auto iter = v.begin(); iter != v.end(); ++iter)
	{
		iter->join();
	}
	cout << "hello!" << endl;
	return 0;

}

在这里插入图片描述

可以看到虽然顺序是不稳定的,但是每次都成功打印了,只读数据是安全稳定的,不需要什么处理手段.

实际案例

class A
{
private:
	list<int>msgqueue;
public:
	void MsgEnqueue()
	{
		for (int i = 0; i < 10000; ++i)
		{
			cout << "MsgEnqueue()执行,插入一个元素 " << i << endl;
			msgqueue.push_back(i);
		}
	}
	void MsgDequeue()
	{
		for (int i = 0; i < 10000; ++i)
		{
			if (!msgqueue.empty())
			{
				int command = msgqueue.front();
				msgqueue.pop_front();
				
			}
			else
			{
				cout << "MsgEnqueue()执行,但队列为空" << endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

在这里插入图片描述

可以看到由于没有保护,一个线程疯狂入队,另一个线程疯狂出队。程序运行时很快就崩溃了。

解决办法:保护共享数据,操作时,操作时,某个线程用代码把共享数据锁住,其他想操作共享数据的线程必须等待解锁。

互斥量

互斥量是一个类对象,理解成一把锁,多个线程尝试使用Lock()成员函数来加锁这把锁头,只有一个线程能锁定成功,如果没锁成功,流程会卡在lock()这里不断的尝试去加锁。互斥量使用要小心,保护数据少了,达不到保护的效果,多了影响效率

头文件<mutex>

lock()和unlock()

使用方法:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex;
public:
	void MsgEnqueue()
	{
		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()执行,插入一个元素 " << i << endl;
			mymutex.lock();
			msgqueue.push_back(i);
			mymutex.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		mymutex.lock();
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			mymutex.unlock();
			return true;
		}
		mymutex.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper执行,取出一个元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()执行,但队列为空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

在这里插入图片描述

这个时候运行就不会出错了。

使用总结:
1.先lock(),在unlock().
2. lock()和unlock()要成对执行,如果是有多个分支比如if else这种,每个分支都要unlock,因为是多个出口.

lock_guard使用方法

为了防止忘记unlock,提供了lock_guard更方便的使用方法

工作原理:类似智能指针,构造的时候调用锁的lock()函数,析构的时候调用unlock()函数,作用域结束的时候也就自动销毁调用unlock().

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()执行,插入一个元素 " << i << endl;
			mymutex.lock();
			msgqueue.push_back(i);
			mymutex.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		lock_guard<mutex>a(mymutex);
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			//mymutex.unlock();
			return true;
		}
		//mymutex.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper执行,取出一个元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()执行,但队列为空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

可以加个花括号,缩小作用域从而提前析构。

好处:使用简单,不怕忘记unlock()。

缺点:不够灵活,不能随时unlock(),只有析构的时候才能解锁,不能更精确的控制加锁和解锁。



死锁

两个或两个以上锁(互斥量)会有可能造成死锁问题

产生的原因,举个例子:

假设有两把锁1和锁2,有两个线程A和B:
1.线程A执行,先加锁锁1,锁1lock()成功,正打算lock()锁2.
然后上下文切换
2.线程B执行,先加锁锁2,锁2lock()成功,正打算lock()锁1.
此时此刻,死锁就产生了。
线程A因为加锁不了锁2,流程走不下去。
线程B因为加锁不了锁1,流程也走不下去。
就这样僵持住了,导致死锁。

死锁演示:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex1;
	mutex mymutex2;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()执行,插入一个元素 " << i << endl;
			mymutex1.lock();
			mymutex2.lock();
			msgqueue.push_back(i);
			mymutex2.unlock();
			mymutex1.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		mymutex2.lock();
		mymutex1.lock();
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			mymutex2.unlock();
			mymutex1.unlock();
			return true;
		}
		mymutex2.unlock();
		mymutex1.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper执行,取出一个元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()执行,但队列为空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

在这里插入图片描述
可以明确看到程序卡住不动了,死锁了。

解决死锁的办法:保证相同的上锁顺序,比如线程A先lock锁1在lock锁2,那线程B也应该先lock锁1在lock锁2.

std::lock()函数模板

能力:一次锁住两个或两个以上的互斥量(至少两个,多了也不行),不存在在多线程中,因为锁的顺序而造成死锁的风险。如果互斥量中有一个没锁住,就等待所以互斥量锁住,要么多个锁都锁住了,要么都不锁,如果其他中一个锁锁住了,另外一个上锁失败,那就会把其他的锁也解锁。

使用例子:

class A
{
private:
	list<int>msgqueue;
	mutex mymutex1;
	mutex mymutex2;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()执行,插入一个元素 " << i << endl;
			lock(mymutex1, mymutex2);
			msgqueue.push_back(i);
			mymutex2.unlock();
			mymutex1.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		lock(mymutex1, mymutex2);
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			mymutex2.unlock();
			mymutex1.unlock();
			return true;
		}
		mymutex2.unlock();
		mymutex1.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper执行,取出一个元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()执行,但队列为空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;
}

在这里插入图片描述
缺点:还是需要手动unlock很有可能会忘记。
解决办法:使用lock_guard配合std::adopt_lock,自动调用unlock
使用例子:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex1;
	mutex mymutex2;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()执行,插入一个元素 " << i << endl;
			lock(mymutex1, mymutex2);
			lock_guard<mutex>(mymutex1, std::adopt_lock);
			lock_guard<mutex>(mymutex2, std::adopt_lock);
			msgqueue.push_back(i);
		}
		return;
	}
	bool helper(int & command)
	{
		lock(mymutex1, mymutex2);
		lock_guard<mutex>(mymutex1, std::adopt_lock);
		lock_guard<mutex>(mymutex2, std::adopt_lock);
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			return true;
		}
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper执行,取出一个元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()执行,但队列为空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;
}

std::adopt_loc是一个结构体对象,起到标记的作用,有了这个标记lock_guard就不会调用lock函数了

猜你喜欢

转载自blog.csdn.net/qq_44800780/article/details/104774713
今日推荐