栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++多线程

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++多线程

概述 并行与并发

并行指的是程序运行时的状态,就是同时运行的意思。

并发指的是程序的结构,这个程序程序同时执行多个独立的任务就说这个程序是并发的设计。

我们讲的代码结构就是指的并发

  • 单核CPU产生并发:

    某一个时刻只能执行一个任务,由操作系统调度,每秒钟进行多次“任务切换”,来实现并发的假象(不是真正的并发),切换任务时要保存变量的状态、执行进度等,存在时间开销。

  • 多核CPU产生并发

    双核,4核,8核,10核,能够实现真正的并行执行多个任务(硬件并发)

进程和线程

进程就是运行起来的可执行程序

可执行程序 :在Windows下扩展名为 exe的文件,Linux下为bin文件

线程:

  • 进程可以包含多个线程。
  • 主线程:从main函数开始,main函数执行完,主线程结束。
  • 其他线程:需要我们自己创建,入口可以是函数、类、lambda表达式。
  • 进程是否执行完毕的标志是主线程是否执行完。

实现并发的手段:

  1. 通过多个进程实现并发(多进程并发)
  2. 在单独的进程中,写代码创建除了主线程之外的其他线程来实现并发(多线程并发)

多线程并发由于线程间可以共享数据,开销远小于多进程并发,优先使用多线程并发。

构建子线程 thread 类成员函数

join

示例:

#include 
// 使用多线程的头文件
#include   
#include 
#include 
using namespace std;

// 子线程
void putInData()
{
	cout << "putInData 子线程 id = " << this_thread::get_id() << endl;
	for (int i = 0; i < 100; i++)
	{
		cout << "putInData 子线程:放入一个数据" << i << endl;
		//std::this_thread::sleep_for(std::chrono::milliseconds(5));
	}
}


// 主线程从main函数入口开始执行
int main()
{
	cout << "主线程 id = " << this_thread::get_id() << endl;
	//std::list dataQuene;

	//1、创建并开启线程,线程入口是 putInData 函数
	thread putThread(putInData);

	//2、join() 阻塞主线程并等待子线程执行完,当子线程执行完毕,join()就执行完毕,主线程继续往下执行
	//join意为汇合,子线程和主线程汇合
	putThread.join();


	//3、传统多线程程序中,主线程要等待子线程执行完毕,然后自己才能向下执行
	//detach:分离,主线程不再与子线程汇合,不再等待子线程。!不建议使用
	//detach后,子线程和主线程失去关联,驻留在后台,由C++运行时库接管

	//putThread.detach();

	// 4、joinable()判断是否可以成功使用join()或者detach()
	//如果返回true,证明可以调用join()或者detach()
	//如果返回false,证明调用过join()或者detach(),join()和detach()都不能再调用了
	if (putThread.joinable())
	{
		cout << "可以调用join()或者detach()" << endl;
		putThread.join();
	}

	cout << "Hello, thread!" << endl;
	return 0;  //主线程结束,则整个进程执行完毕
}

总结:

  • main函数开始后自动开启主线程

  • 进程id: this_thread::get_id()可以查看线程id

  • thread类可以创建线程:创建后自动开启线程,线程入口可以是函数、类、lambda表达式

  • join:意为汇合,子线程和主线程汇合,阻塞主线程并等待子线程执行完,当子线程执行完毕,join()就执行完毕

  • detach:分离,主线程不再与子线程汇合,不再等待子线程

  • joinable:判断是否可以成功使用join()或者detach(),可以的话返回true,否则返回false

多线程数据共享

数据共享:

顾名思义,数据共享就是同一份数据有多个线程在使用,此时,如果同一个地址数据有读有写,如果不做处理,容易冲突崩溃。

互斥量 mutex

  • 互斥量就是个类对象,可以理解为一把锁,多个线程同一时间只有一个线程能加锁或解锁

  • lock, unlock 要成对使用,必须对应,否则出错

  • 只保护需要保护的数据,控制好范围,少了达不到效果,多了影响效率。

示例:

示例分别通过函数和类成员函数创建子线程,两种方法有使用线程锁和不使用线程锁的两种模式。

#include 
#include    // 多线程的头文件
#include 
#include   // 互斥锁头文件
using namespace std;

// 设置模式
//#define THREAD_BY_FUNCTION
#define THREAD_BY_CLASS
#define USE_MUTEX



#ifdef THREAD_BY_FUNCTION
// 函数创建线程
std::list dataQuene;
mutex myMutex;
void putInData()
{
	for (int i = 0; i < 1000; i++) //数字增大可增加冲突概率
	{
#ifdef USE_MUTEX
		myMutex.lock();
		cout << "putInData 子线程:放入一个数据" << i << endl;
		dataQuene.push_back(i);
		myMutex.unlock();
#else
		cout << "putInData 子线程:放入一个数据" << i << endl;
		dataQuene.push_back(i);
#endif 
	}
}

void takeOutData()
{
	int dataNum = 1000;
	while (dataNum)
	{
		if (!dataQuene.empty())
		{
#ifdef USE_MUTEX
			myMutex.lock();
			cout << "takeOutData 子线程:取出一个数据" << dataQuene.front() << endl;
			dataQuene.pop_front();
			dataNum--;
			myMutex.unlock();
#else
			cout << "takeOutData 子线程:取出一个数据" << dataQuene.front() << endl;
			dataQuene.pop_front();
			dataNum--;
#endif 
		}
	}
}
#endif 


#ifdef THREAD_BY_CLASS
// 类成员函数创建线程
class DealData
{
public:
	void putInData()
	{
		for (int i = 0; i < 1000; i++)
		{
#ifdef USE_MUTEX
			myMutex.lock();
			cout << "putInData 子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
			myMutex.unlock();
#else
			cout << "putInData 子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
#endif 
		}
	}

	void takeOutData()
	{
		//for (int i = 0; i < 10000; i++) // 会导致不能完全取出所有数据
		while (dataNum)
		{
			if (!dataQuene.empty())
			{
#ifdef USE_MUTEX
				myMutex.lock();
				cout << "takeOutData 子线程:取出一个数据" << dataQuene.front() << endl;
				dataQuene.pop_front();
				dataNum--;
				myMutex.unlock();
#else
				cout << "takeOutData 子线程:取出一个数据" << dataQuene.front() << endl;
				dataQuene.pop_front();
				dataNum--;
#endif 
			}
		}
	}
private:
	std::list dataQuene;
	mutex myMutex;
	int dataNum = 1000;

};
#endif 

int main()
{

//1、创建并开启线程,线程入口是 putInData,takeOutData 函数
#ifdef THREAD_BY_CLASS
	DealData myData;
	thread putThread(&DealData::putInData, ref(myData));
	thread takeThread(&DealData::takeOutData, ref(myData));
#endif
#ifdef THREAD_BY_FUNCTION
	thread putThread(&putInData);
	thread takeThread(&takeOutData);
#endif
	takeThread.join();
	putThread.join();

	cout << "hello thread!" << endl;
	return 0;
}

总结:

  • 不管是使用哪种方法创建子线程,如果数据共享不使用互斥锁就会出现如下的错误信息:

互斥锁的其他实现方法

分别采用了9种方法实现互斥锁,实际使用掌握一两种即可:

  1. lock和unlock
  2. lock_guard
  3. unique_lock
  4. adopt_lock
  5. defer_lock
  6. try_to_lock
  7. try_lock
  8. release
  9. move
#include 
#include    // 多线程的头文件
#include 
#include   // 互斥锁头文件
using namespace std;

//#define USE_LOCK_UNLOCK
//#define USE_LOCKGUARD
//#define USE_UNIQUELOCK
//#define USE_ADOPTLOCK     //已经提前 lock  
//#define USE_DEFER_LOCK    //互斥量没有lock,不能提前lock,只能后面 lock  
//#define USE_TRY_TO_LOCK   //尝试去lock(不能提前lock),lock失败会返回,防止阻塞
//#define USE_RELEASE         //解除所有权
//#define USE_MOVE            //转移所有权
//#define USE_TRY_LOCK
class DealData
{
public:
	void putInData()
	{
		for (int i = 0; i < 1000; i++) //数字增大可增加冲突概率
		{
#ifdef USE_LOCK_UNLOCK
			myMutex.lock();
			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
			myMutex.unlock();
#endif // USE_LOCK_UNLOCK

#ifdef USE_LOCKGUARD
			lock_guard dataGuard(myMutex);
			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);

#endif // USE_LOCKGUARD

#ifdef USE_UNIQUELOCK
			unique_lock dataOutUnique(myMutex);
			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
#endif // USE_UNIQUELOCK

#ifdef USE_ADOPTLOCK
			myMutex.lock(); // 使用std::adopt_lock 必须先lock,且不需要手动解锁
			unique_lock dataControl(myMutex, std::adopt_lock);
			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
#endif // USE_ADOPTLOCK

#ifdef USE_DEFER_LOCK
			unique_lock dataControl(myMutex, std::defer_lock); // std::defer_lock 使得myMutex没有加锁

			myMutex.lock();
			// 处理共享代码
			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);

			myMutex.unlock();

#endif // USE_DEFER_LOCK

#ifdef USE_TRY_TO_LOCK
			// 使用try_lock 功能类似 try_to_lock
			unique_lock dataControl(myMutex, std::try_to_lock); // 使用std::try_to_lock 之前不能lock,否则会lock 两次出错
			if (dataControl.owns_lock())
			{
				// 如果拿到了锁
				cout << "putInData子线程:放入一个数据" << i << endl;
				dataQuene.push_back(i);
			}
			else
			{
				// 如果没拿到锁
				cout << "没拿到锁,没法放入数据" << i << endl;
				i--;
			}
#endif

#ifdef USE_TRY_LOCK
			// 使用try_lock 功能类似 try_to_lock
			if (myMutex.try_lock() == true)
			{
				// 如果拿到了锁
				cout << "putInData子线程:放入一个数据" << i << endl;
				dataQuene.push_back(i);
				myMutex.unlock();
			}
			else
			{
				// 如果没拿到锁
				cout << "没拿到锁,无法放入数据" << i << endl;
				i--;
			}
#endif // USE_TRY_LOCK


#ifdef USE_RELEASE
			// 把 dataControl 和 myMutex 绑定在一起,dataControl有myMutex的所有权
			unique_lock dataControl(myMutex);

			// 释放了dataControl对myMutex的所有权,由pControl接管
			std::mutex* pControl = dataControl.release();

			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);

			pControl->unlock();
			// 非共享数据
			//....

#endif // USE_RELEASE

#ifdef USE_MOVE          
			unique_lock dataControl1(myMutex);
			//unique_lock dataControl2(dataControl1);            // 不能复制所有权
			unique_lock dataControl2(std::move(dataControl1));   // 可以移动所有权

			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
#endif // USE_MOVE
		}
	}

	void takeOutData()
	{
		while (dataNum)
		{
			haveDta();
		}
	}

	// 为了演示lock()、unlock()配对的重要性
	void haveDta()
	{
#ifdef USE_LOCK_UNLOCK
		myMutex.lock();
		if (!dataQuene.empty())
		{
			cout << "takeOutData子线程:取出一个数据" << dataQuene.front() << endl;
			dataQuene.pop_front();
			dataNum--;
			myMutex.unlock();   // 不能忘记
			return;
		}
		myMutex.unlock();
		return;
#endif // USE_LOCK_UNLOCK

#ifdef USE_LOCKGUARD
		{
			lock_guard dataOutGuard(myMutex);
			if (!dataQuene.empty())
			{
				cout << "takeOutData子线程:取出一个数据" << dataQuene.front() << endl;
				dataNum--;
				dataQuene.pop_front();
				return;
			}
			return;
		}
		// xxxx
		//...
#endif // USE_LOCKGUARD

#ifdef USE_UNIQUELOCK

		if (!dataQuene.empty())
		{
			unique_lock dataOutUnique(myMutex);
			cout << "takeOutData子线程:取出一个数据" << dataQuene.front() << endl;
			dataNum--;
			dataQuene.pop_front();
			return;
		}
		return;
#endif // USE_UNIQUELOCK


		// 默认的数据取出线程
		unique_lock dataOutUnique(myMutex);
		if (!dataQuene.empty())
		{
			cout << "takeOutData子线程:取出一个数据" << dataQuene.front() << endl;
			dataNum--;
			dataQuene.pop_front();
			return;
		}
		return;

	}
private:
	std::list dataQuene;
	mutex myMutex;
	int dataNum = 1000;

};

int main()
{
	//std::list dataQuene;

	//1、创建并开启线程,线程入口是 putInData,takeOutData 函数
	DealData myData;
	thread putThread(&DealData::putInData, ref(myData));
	thread takeThread(&DealData::takeOutData, ref(myData));

	takeThread.join();
	putThread.join();
	cout << "thread done." << endl;

	return 1;
}
多线程中死锁问题

死锁至少有两个互斥量,其产生原因如下:

  • 线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
  • 线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1。此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1, 两个线程没办法继续运行下去。

解决方案:

只要保证多个互斥量加锁的顺序一样就不会造成死锁

#include 
#include    // 多线程的头文件
#include 
#include   // 互斥锁头文件
using namespace std;

#define USE_LOCK_UNLOCK
//#define USE_UNIQUELOCK

class DealData
{
public:
	void putInData()
	{
		for (int i = 0; i < 1000; i++) //数字增大可增加冲突概率
		{
#ifdef USE_LOCK_UNLOCK
			myMutex1.lock();
			myMutex2.lock();
			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
			myMutex1.unlock();
			myMutex2.unlock();
#endif // USE_LOCK_UNLOCK


#ifdef USE_UNIQUELOCK
			unique_lock goldenLock(myMutex1);
			unique_lock silverLock(myMutex2);
			cout << "putInData子线程:放入一个数据" << i << endl;
			dataQuene.push_back(i);
#endif // USE_UNIQUELOCK
		}
	}

	void takeOutData()
	{
		while (dataNum)
		{
#ifdef USE_LOCK_UNLOCK
			myMutex1.lock();
			myMutex2.lock();
			if (!dataQuene.empty())
			{
				cout << "takeOutData子线程:取出一个数据" << dataQuene.front() << endl;
				dataQuene.pop_front();
				dataNum--;
			}
			myMutex1.unlock();
			myMutex2.unlock();
#endif // USE_LOCK_UNLOCK

#ifdef USE_UNIQUELOCK
			unique_lock silverLock(myMutex2);
			unique_lock goldenLock(myMutex1);
			if (!dataQuene.empty())
			{
				cout << "takeOutData子线程:取出一个数据" << dataQuene.front() << endl;
				dataQuene.pop_front();
			}
#endif // USE_UNIQUELOCK

		}
	}

private:
	std::list dataQuene;
	std::mutex myMutex1;
	std::mutex myMutex2;
	int dataNum = 1000;
};

int main()
{
	//std::list dataQuene;

	//1、创建并开启线程,线程入口是 putInData,takeOutData 函数
	DealData myData;
	thread putThread(&DealData::putInData, ref(myData));
	thread takeThread(&DealData::takeOutData, ref(myData));

	takeThread.join();
	putThread.join();
	std::cout << "thread done" << std::endl;

	return 1;
}

总结:

  • 可以看到存数据和取数据都是先给锁1加锁,再给锁2加锁,如果把取数据的加锁顺序改变,就可能会发生死锁(多执行几次)。
  • 解锁的顺序并不会产生死锁现象

内容是学习课程所得总结,如果有不对的地方,或者有更好的见解,欢迎大家加我v:peak1229258698,一起交流学习。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/942571.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号