C++并发与多线程(二)

创建多个线程、数据共享问题分析、案例代码

创建和等待多个线程

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
using namespace std;
//线程入口函数
void myprint(int inum)
{
	cout << "myprint线程开始执行,线程编号 = " << inum << endl;
	//...........
	cout << "myprint线程执行结束,线程编号 = " << inum << endl;
	return;

}


int main()
{
	vector<thread> mythreads;
	//创建十个线程,线程入口函数统一使用myprint
	for (int i = 0; i < 10; i++)
	{
		mythreads.push_back(thread(myprint, i));//创建并开始执行线程
	}
	for (auto iter = mythreads.begin();iter != mythreads.end();iter++)
	{
		iter->join(); //等待十个线程都返回
	}
	
	cout << "主线程执行完毕" << endl;

	return 0;
}

运行结果:

myprint线程开始执行,线程编号 = myprint线程开始执行,线程编号 = myprint线程开始执行,线程编号 = 4
myprint线程执行结束,线程编号 = 4
myprint线程开始执行,线程编号 = 5
myprint线程执行结束,线程编号 = 5
myprint线程开始执行,线程编号 = 9
myprint线程执行结束,线程编号 = 9
myprint线程开始执行,线程编号 = 8
myprint线程执行结束,线程编号 = 8
1
myprint线程执行结束,线程编号 = 1
myprint线程开始执行,线程编号 = 7
myprint线程执行结束,线程编号 = 7
myprint线程开始执行,线程编号 = 0
myprint线程执行结束,线程编号 = 0
2
myprint线程执行结束,线程编号 = 2
myprint线程开始执行,线程编号 = 3
myprint线程执行结束,线程编号 = 3
myprint线程开始执行,线程编号 = 6
myprint线程执行结束,线程编号 = 6
主线程执行完毕

可以看见,线程执行的顺序是混乱的,这与计算机内部的线程运行调度机制有关.

将线程放到容器中管理,对大量线程的管理很方便。

数据共享问题分析

只读数据
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
using namespace std;

vector<int> g_v = { 1,2,3 }; //共享数据 只读


//线程入口函数
void myprint(int inum)
{
	//cout << "myprint线程开始执行,线程编号 = " << inum << endl;
	...........
	//cout << "myprint线程执行结束,线程编号 = " << inum << endl;
	cout << "id 为 " << std::this_thread::get_id() << "的线程 打印g_v值" << g_v[0] << g_v[1] << g_v[2] << endl;
	return;

}


int main()
{
	vector<thread> mythreads;
	//创建十个线程,线程入口函数统一使用myprint
	for (int i = 0; i < 10; i++)
	{
		mythreads.push_back(thread(myprint, i));//创建并开始执行线程
	}
	for (auto iter = mythreads.begin();iter != mythreads.end();iter++)
	{
		iter->join(); //等待十个线程都返回
	}
	
	cout << "主线程执行完毕" << endl;

	return 0;
}

执行结果:

id 为 20924的线程 打印g_v值12id 为 28192的线程 打印g_v值123
id 为 14616的线程 打印g_v值123
3
id 为 26844的线程 打印g_v值123
id 为 27624的线程 打印g_v值123
id 为 25476的线程 打印g_v值123
id 为 24024的线程 打印g_v值123
id 为 26920的线程 打印g_v值123
id 为 17628的线程 打印g_v值123
id 为 25048的线程 打印g_v值123
主线程执行完毕

可以看见,只读的数据是安全稳定的,不管怎么读,读出来的数据都是固定的,对于这种数据直接读取就可以了。

可读可写的数据

两个线程写数据,八个线程读数据,如果代码没有特别处理,程序肯定崩溃。

最简单的处理:读的时候不能写,写的时候不能读,两个线程不能同时写,八个线程不能同时读。(互斥锁?排他锁?)

共享数据的保护案例代码

开发一个简单的网络游戏服务器,有两个自己创建的线程,一个线程收集玩家命令,并把命令数据写到一个队列中。

另外一个线程,从队列中取出玩家发出来的命令,解析,然后执行玩家需要的动作。

假定每次发出的命令为一个数字。用list容器。list:频繁的按顺序插入和删除数据时效率高。vector对于随机插入和删除数据效率高。

互斥量概念、用法、死锁演示及解决

互斥量(mutex)的基本概念

互斥量是一个类对象。可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能够锁定成功(成功的标志是lock()返回,如果没有锁成功,那就会一直卡住等待枷锁成功)。

互斥量使用要小心,保护数据少了达不到保护效果,多了会影响效率。

互斥量的用法

lock(), unlock()

步骤:先lock()操作共享数据。然后再unlock()共享数据。两个函数必须匹配。

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;


class A
{
public:
	//把收到的消息放入队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
			my_mutex.lock();
			msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
			my_mutex.unlock();
		}

	}
	bool outMsgLULProc(int& command)
	{
		my_mutex.lock();
		if (!msgRecvQueue.empty())
		{
			//不为空
			int command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			my_mutex.unlock();
			return true;
			//这里处理数据........
			//..............

		}
			//消息队列为空
		my_mutex.unlock();
			return false;
			//cout << "outMsgRecvQueue()执行,但是目前消息队列为空 " << i << endl;
	}
	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLULProc(i);
			if (result)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;

			}
			else
			{
				cout << "消息序列为空" << endl;
			}
		}
		cout << "end" << endl;
		return;
	}

private:

	list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
	std::mutex my_mutex;

};



int main()
{
	A myobja;
	thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
	thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
	myOutnMsgobj.join();
	myInMsgobj.join();


	return 0;
}

加入锁之后程序不会出现异常。

std::lock_guard类模板

为了防止忘记unlock(),引入了std::lock_guard类模板,可以自动unlock();

有点类似于只能指针(unique_ptr<>)。直接取代lock()和unlock();也就是说,用过std::lock_guard类模板之后,不能使用lock()和unlock();

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;


class A
{
public:
	//把收到的消息放入队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
			my_mutex.lock();
			msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
			my_mutex.unlock();
		}

	}
	bool outMsgLULProc(int& command)
	{
		std::lock_guard<std::mutex> sbguard(my_mutex);
		//my_mutex.lock();
		if (!msgRecvQueue.empty())
		{
			//不为空
			int command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			//my_mutex.unlock();
			return true;
			//这里处理数据........
			//..............

		}
			
		
			return false;
			
	}
	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLULProc(i);
			if (result)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;

			}
			else
			{
				cout << "消息序列为空" << endl;
			}
		}
		cout << "end" << endl;
		return;
	}

private:

	list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
	std::mutex my_mutex;

};



int main()
{
	A myobja;
	thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
	thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
	myOutnMsgobj.join();
	myInMsgobj.join();


	return 0;
}

lock_guard构造函数里面执行了mutex::lock();

lock_guard析构函数里执行了mutex::unlock();

根据原理想要在任何地方unlock(),就可以在程序中想析构的地方加入花括号{};他的作用域就在花括号中。

死锁

比如有两把锁(死锁产生的前提条件是 由至少两个互斥量也就是两把锁才能产生),金锁(jinlock),银锁(yinlock);

假设线程A,B;

(1)线程A执行的时候将金锁锁住,lock()成功,然后它去lock银锁时;

(2)线程B执行正好已经锁住银锁,然后它去lock金锁。

这时,线程A一直尝试锁住银锁,金锁一直没有解锁,线程B一直尝试金锁,银锁一直没有解锁,两边线程一直等待,就造成了死锁。

死锁演示
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;


class A
{
public:
	//把收到的消息放入队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
			my_mutex1.lock(); //实际工程中这两个lock时机可能不一定挨着,可能他们需要保护不同的数据共享块
			my_mutex2.lock();
			msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
			my_mutex2.unlock();
			my_mutex1.unlock();
		}

	}
	bool outMsgLULProc(int& command)
	{
		//std::lock_guard<std::mutex> sbguard(my_mutex1);
		my_mutex2.lock();
		my_mutex1.lock();
		if (!msgRecvQueue.empty())
		{
			//不为空
			int command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			//my_mutex.unlock();
			return true;
			//这里处理数据........
			//..............

		}
			//消息队列为空
		my_mutex1.unlock();
		my_mutex2.unlock();
			return false;
			//cout << "outMsgRecvQueue()执行,但是目前消息队列为空 " << i << endl;
	}
	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLULProc(i);
			if (result)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;

			}
			else
			{
				cout << "消息序列为空" << endl;
			}
		}
		cout << "end" << endl;
		return;
	}

private:

	list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
	std::mutex my_mutex1;//一个互斥量
	std::mutex my_mutex2;//另一个互斥量

};



int main()
{
	A myobja;
	thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
	thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
	myOutnMsgobj.join();
	myInMsgobj.join();


	return 0;
}

因为一边是先锁1再锁2,一边是先锁2再锁1,就发生了死锁。

死锁的一般解决方案

只要保证两个互斥量上锁的顺序一致,就不会死锁。

std::lock()函数模板

一次锁住两个或者两个以上的互斥量(至少两个);

它不存在因为在多个线程中因为锁的顺序问题导致死锁的风险问题。

std::lock():如果互斥量中有一个没有锁住,就等待所有互斥量锁住才往下走(返回)。

std::mutex my_mutex1;//一个互斥量
std::mutex my_mutex2;//另一个互斥量

用这个函数的话会同时锁住两个互斥量。不会出现只锁住一个的情况。如果只锁定一个另一个没有成功,就立即解锁已经锁住的锁。

std::lock(my_mutex1, my_mutex2);代替两个锁的lock()即可,但是后面还是要记得unlock();

std::lock_guard的std::adopt_lock参数

在std::lock_guard构造的时候加入std::adopt_lock 就相当于这个互斥量已经lock()过了。在使用std::lock_guard中不加锁,在释放对象的时候直接解锁!

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;


class A
{
public:
	//把收到的消息放入队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
			std::lock(my_mutex1, my_mutex2);
			std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
			std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);
			msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列

		}

	}
	bool outMsgLULProc(int& command)
	{
		//std::lock_guard<std::mutex> sbguard(my_mutex1);
		std::lock(my_mutex1, my_mutex2);
		std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
		std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);
		if (!msgRecvQueue.empty())
		{
			//不为空
			int command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			
			//这里处理数据........
			//..............

			return true;

		}
			//消息队列为空

			return false;
	}
	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLULProc(i);
			if (result)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;

			}
			else
			{
				cout << "消息序列为空" << endl;
			}
		}
		cout << "end" << endl;
		return;
	}

private:

	list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
	std::mutex my_mutex1;//一个互斥量
	std::mutex my_mutex2;//另一个互斥量

};



int main()
{
	A myobja;
	thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
	thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
	myOutnMsgobj.join();
	myInMsgobj.join();


	return 0;
}

unique_lock详解

unique_lock是一个类模板,工作中,一般lock_guard(推荐使用);

lock_guard取代了mutex的lock()和unlock();unique_lock比起lock_guard灵活了很多,但是效率要低一些。

unique_lock取代lock_guard

直接替换没有任何区别。

unique_lock的第二个参数

lock_guard可以带第二个参数: std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);,第二个参数起标记作用。

unique_lock第二个参数一样起标记作用。

std::adopt_lock

表示这个互斥量已经被lock(必须要把互斥量提前lock)。

std::adopt_lock标记的效果是“假设调用方线程已经拥有了互斥的所有权”,通知unique_lock不在 构造函数中lock这个互斥量。

std::try_to_lock

我们尝试用mutex的lock()去锁定这个mutex,如果没有锁定成功,也会立即返回,不会阻塞到那里。

使用try_to_lock不能自己先lock,相当于锁了两次。

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;


class A
{
public:
	//把收到的消息放入队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;

			std::unique_lock<std::mutex> sbguard1(my_mutex1, std::try_to_lock);//尝试加锁
			if (sbguard1.owns_lock())//如果拿到了锁
			{
				msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
			}
			else
			{
				//没拿到锁
				cout << "inMsgRecvQueue()执行但未能拿到锁。" << endl;
			}
		}

	}
	bool outMsgLULProc(int& command)
	{
		
		std::unique_lock<std::mutex> sbguard1(my_mutex1);

		std::chrono::milliseconds dura(20000);
		std::this_thread::sleep_for(dura);//休息20s
		if (!msgRecvQueue.empty())
		{
			//不为空
			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			
			//这里处理数据........
			//..............

			return true;

		}
			//消息队列为空

			return false;
	}
	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLULProc(i);
			if (result)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;

			}
			else
			{
				cout << "消息序列为空" << endl;
			}
		}
		cout << "end" << endl;
		return;
	}

private:

	list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
	std::mutex my_mutex1;//一个互斥量
	std::mutex my_mutex2;//另一个互斥量

};



int main()
{
	A myobja;
	thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
	thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
	myOutnMsgobj.join();
	myInMsgobj.join();


	return 0;
}

运行结果:

inMsgRecvQueue()执行但未能拿到锁。
inMsgRecvQueue()执行,插入一个元素 1924
inMsgRecvQueue()执行但未能拿到锁。
inMsgRecvQueue()执行,插入一个元素 1925
inMsgRecvQueue()执行但未能拿到锁。
inMsgRecvQueue()执行,插入一个元素 1926
inMsgRecvQueue()执行但未能拿到锁。

20s之后就会拿到锁开始执行。

std::defer_lock

用defer_lock的前提是,你自己不能先lock,否则会报异常。

defer_lock的意思是没有给mutex加锁:初始化了一个没有加锁的mutex。

			std::unique_lock<std::mutex> sbguard1(my_mutex1,std::defer_lock);//没加锁的mutex1
			sbguard1.lock();//不用自己unlock

unique_lock的成员函数

lock()

使用unique_lock的时候,实例化的对象lock()后不用unlock();但是可以手动unlock。

unlock()

可以提前解锁,之后还是可以加锁。

try_lock()

尝试给互斥量加锁,如果拿不到锁,就返回false,如果拿到了,就返回true。

if (sbguard1.try_lock() == true)
			{
				msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
			}

			else
			{
				//没拿到锁
				cout << "inMsgRecvQueue()执行但未能拿到锁。" << endl;
			}
		}
release()

返回它所管理的mutex指针,并释放所有权,也就是说,这个unique_lock和mutex没有任何联系。

std::unique_lock<std::mutex> sbguard1(my_mutex1);
std::mutex* ptx = sbguard1.release();//释放这个对象关联的互斥量对象后,用ptx指针接管。然后需要手动解锁。
ptx->unlock();

unique_lock所有权的传递

所有权不能复制,需要用std::move函数:

	std::unique_lock<std::mutex> sbguard1(my_mutex1);
	std::unique_lock<std::mutex> sbguard2(std::move(sbguard1));

还可以return unique_lock类的对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

word_no_bug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值