C++11特性-多线程

1.多线程概念

程序:程序是指令和数据的结合,是一个静态概念,如果不运行,相当于一个静态文件。

进程:进程是程序的执行过程,此时它是动态的,它加载到了内存当中,使用了操作系统的各种资源。

线程:线程是进程中的实际运行单位,是操作系统可以调度的最小单位。每个进程至少包括一个线程。

多线程就是指一个进程中同时有多个线程在执行。

2.thread类:

C++11之前,是没有提供多线程类库的,所以在编写多线程程序的时候,只能调用操作系统的多线程接口。

C++11之后提供了线程库:thread,可以实现跨平台编写多线程程序。需要引入头文件      #include <thread>。

thread的构造函数:

1)thread();默认构造函数,构造一个空线程对象,没有关联任何线程执行函数的,它不会启动。

2)thread t(func);这个线程关联了一个函数func,但是它不会立刻执行,而是需要我们手动让它选择执行方式来执行:阻塞join和分离detach这两种方式。

阻塞执行:

join()函数来完成,阻塞即在某处停下来,指的是当前这个线程必须要执行完毕后,才能继续往下执行,这个线程就起到了阻塞的作用。

        比如说:执行程序的时候都是从main函数开始的,此时main函数本身也是个线程,main函数里面如果再启动一个线程,这个线程就称为main函数的子线程,子线程是可以被父线程控制的,这时候存在一种情况,如果这两个线程同时执行,当main函数的线程先结束了,子线程还没有结束,main函数的线程会杀死子线程,这个时候就无法保证子线程的完整执行。所以需要通过阻塞的方式来执行子线程。

//阻塞执行的效果
void test02()
{
	thread t5(fun3);//没有参数的fun3,不会开始执行,需要手动执行
	t5.join();//阻塞执行
	cout << "t5的父线程会等待t5执行完毕,再继续执行" << endl;
	for (int i = 10; i < 15; i++)
	{
		cout << "这是父线程调用的:" << i << endl;
	}
}

分离执行:

detach()函数来实现,这个时候main函数不会取得对子线程的控制权,这个时候main函数的线程和子线程它们是各自执行的,互不影响,也可以保证子线程可以顺利执行完毕,这个时候子线程可以成为守护线程。

//分离执行的效果,守护线程
void test03()
{
	thread t6(fun3);//没有参数的fun3,不会开始执行,需要手动执行
	t6.detach();//detach执行
	cout << "t5和父线程互不影响,同时执行" << endl;
	for (int i = 10; i < 15; i++)
	{
		cout << "这是父线程调用的:" << i << endl;
	}
}

3)thread t(func,args1,args2...);这个线程关联了func函数,同时给func函数提供了参数args1,args2...这种情况下,线程会马上开始执行,为了避免线程意外被父线程杀死,我们同样需要指定执行的方式:join或者detach。

4)拷贝构造,是被禁用的,线程是禁止拷贝的。

5)移动构造函数,原型:thread(thread&& other);因为线程无法拷贝,所以需要移动构造函数来转移所有权。这样我们在构造线程对象的时候,可以通过移动的方式直接转移所有权,省去了拷贝的动作。移动构造函数需要传入右值引用,所以在使用的时候需要通过std::move()将线程对象从左值转换为右值。

        thread t(move(other));调用之后other线程的所有权会转移给t。

        左值:可以放到等号左边的值,可以取地址并且有名字。比如变量名、函数名、字符串的字面值"abc"。

        右值:不能放到等号左边的值,不能取地址也没有名字。比如运算表达式产生的临时变量、除字符串意外的字面值、匿名函数。

        举例:int a=b+c; //a是左值,b+c是右值  int num=10;//num是左值,10是右值

thread的其他成员函数:

1)get_id();//获取线程id

2)joinable();//返回bool值,如果一个线程已经join了,返回false,如果没有join,返回true

//joinable的用法
void test04()
{
	thread t7(fun4);
	cout << "joinable=" << t7.joinable() << endl;//1
	t7.join();
	cout << "joinable=" << t7.joinable() << endl;//0
}

3)join();//阻塞执行

4)detach();//分离执行,后台执行。

休眠函数:

C++11开始提供了专门的休眠函数,需要引用头文件#include <chrono>

this_thread::sleep_for(休眠时间),this_thread代表当前线程的命名空间

休眠时间的指定一般以毫秒作为单位。举例:this_thread::sleep_for(chrono::milliseconds(500)); 休眠500毫秒

void fun1(int n)
{
	for (int i = 0; i < 5; i++)
	{
		cout << "fun1在执行,传入的参数是:" << n << endl;
		//休眠一段时间,方便观察
		this_thread::sleep_for(chrono::milliseconds(500));//休息500毫秒
	}
}

3.多线程应用

#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <chrono>
#include <mutex>
#include <atomic>
#include <condition_variable>
using namespace std;

//多线程应用
//准备线程需要关联的函数
void fun1(int n)
{
	for (int i = 0; i < 5; i++)
	{
		cout << "fun1在执行,传入的参数是:" << n << endl;
		//休眠一段时间,方便观察
		this_thread::sleep_for(chrono::milliseconds(500));//休息500毫秒
	}
}
void fun2(int& n)//引用传参
{
	for (int i = 0; i < 5; i++)
	{
		cout << "fun2在执行,传入的参数是:" << n << endl;
		n++;//每次修改n的值
		//休眠一段时间,方便观察
		this_thread::sleep_for(chrono::milliseconds(300));//休息300毫秒
	}
}
void fun3()
{
	int i = 0;
	cout << "这是子线程调用的:" << endl;
	while (i<10)
	{
		cout << i << " ";
		i++;
	}
	cout << endl;
}
void fun4()
{
	cout << "执行fun4" << endl;
}
void test01()
{
	//使用线程的构造函数和成员函数
	int n = 0;
	thread t1;//无参构造,不绑定任何函数,不会执行
	cout << "t1=" << t1.get_id() << endl;//它没有执行,所以id=0
	thread t2(fun1, n);//t2线程绑定了fun1函数,并且给fun1函数传了参数n,这时候t2会马上运行
	cout << "t2=" << t2.get_id() << endl;//t2马上运行了,所以它有id号
	thread t3(fun2, ref(n));//绑定fun2函数,用引用的方式将n传给fun2
	cout << "t3=" << t3.get_id() << endl;//t3马上运行了,所以它有id号
	thread t4(move(t3));//移动构造,t3线程转移所有权给t4,t3小时,t4拥有了t3的所有权,可以看到t4的id号跟消失前t3是一样的
	cout << "t3=" << t3.get_id() << endl;//t3消失了,id=0
	cout << "t4=" << t4.get_id() << endl;//t4的id号跟原来的t3一样
	//为了保证子线程可以顺利执行到完毕,需要通过阻塞或者分离的方式来执行
	t2.join();
	t4.join();
	//t2.detach();
	//t4.detach();
	cout << "t2和t4已经执行完毕,最后n的值是:" << n << endl;
	cout << "t2=" << t2.get_id() << endl;//t2已经执行完毕,id=0
	cout << "t4=" << t4.get_id() << endl;//t4已经执行完毕,id=0
}

4.线程互斥:

平时写程序的时候都是单线程,大家只需要考虑逻辑问题语法问题。但是在多线程情况下编程,事情会变得复杂,这时候需要考虑多个线程之间的竞争关系。

考虑它们之间有没有矛盾和冲突的地方,这种矛盾和冲突称之为静态条件。对于存在静态条件的代码,称之为临界区代码,或者叫做关键代码段。

这种代码需要排除冲突才可以保证程序的正确性,这些代码需要在执行前,让某个线程独占资源去执行,其他线程不能进入,这就是线程互斥。

线程互斥的实现方式:

        第一种:通过互斥锁将临界区代码段进行锁定,让这些代码独占资源。等它们执行完毕后,其他线程才能使用它们的资源继续执行。互斥锁需要引入头文件#include <mutex>,主要有两个动作:加锁lock()和解锁unlock()

//没有互斥锁时
int num = 0;
void fun5()
{
	for (int i = 0; i < 5; i++)
	{
		cout << this_thread::get_id() << "," << ++num << endl;
	}
}
void test05()
{
	//没有互斥锁的情况,下面两个线程绑定的都是同一个函数,操作的都是同一个变量
	thread t8(fun5);
	thread t9(fun5);
	t8.join();
	t9.join();
}
//加上互斥锁时
mutex num_lock;//定义一个互斥锁对象
void fun6()
{
	//在操作num变量这个资源之前,先加锁,保证当前线程可以独占资源
	num_lock.lock();//加锁
	for (int i= 0; i < 5; i++)
	{
		cout << this_thread::get_id() << "," << ++num << endl;
	}
	num_lock.unlock();//用完之后别忘了解锁,让其他线程去继续加锁
}
void test06()
{
	thread t8(fun6);
	thread t9(fun6);
	t8.join();
	t9.join();
}

        第二种:更轻量级的互斥方案,基于CAS技术的原子操作类,使用这个类中的数据类型就可以实现线程互斥,实现线程安全。引入头文件#include<atomic>

//通过原子操作类atomic实现线程互斥
atomic_int sum = 0;//这是一个原子整型,它是线程安全的
//下面定义两个大量运算的函数
void fun_atomic()
{
	for (int i = 0; i < 10000; i++)
	{
		sum++;//这个是原子整型
	}
}
void fun_int()
{
	for (int i = 0; i < 10000; i++)
	{
		num++;//这个是普通整型
	}
}
void test07()//这是线程安全的原子整型操作
{
	//为了创建并启动多个线程,需要用到容器
	vector<thread> vec;
	//启动100个线程,每个线程调用执行一万次加法的函数,共计运行一百万次
	for (int i = 0; i < 100; i++)
	{
		//每个循环,起个线程,放到容器中
		vec.push_back(thread(fun_atomic));
	}
	for (int i = 0; i < 100; i++)
	{
		vec[i].join();
	}
	cout << "sum=" << sum << endl;
}
void test08()//这是普通整型操作
{
	//为了创建并启动多个线程,需要用到容器
	vector<thread> vec;
	//启动100个线程,每个线程调用执行一万次加法的函数,共计运行一百万次
	for (int i = 0; i < 100; i++)
	{
		//每个循环,起个线程,放到容器中
		vec.push_back(thread(fun_int));
	}
	for (int i = 0; i < 100; i++)
	{
		vec[i].join();
	}
	cout << "num=" << num << endl;//这个地方的结果应该是一百万,如果不是就证明出错了,这个是可能发生的
}

5.线程同步:

某些应用场景下,需要线程之间来通信打配合,一个线程需要等待另外一个线程的运行结果,才能往下执行。

线程通信需要通过条件变量condition_variable来完成,引入头文件#include <condition_variable>

注意:线程同步要和互斥锁一起配合使用

比较经典的线程同步案例是生产者-消费者模型。生产者生成出产品后,会通知消费者去消费,如果消费者发现还没有可以消费的产品,它也会通知生产者去生产。

生产者和消费者各式一个独立的线程,它们之间要进行同步。

//生产者-消费者模型
mutex g_mtx;//互斥锁
condition_variable g_convar;//条件变量
vector<int> g_vector;//存放产品的容器
//写生产者线程调用的函数
void producter()
{
	//生产者每生产一个产品,就通知消费者消费一个
	for (int i = 0; i < 10; i++)
	{
		//生产之前进行加锁
		//这里使用新的一个模板类来管理mutex,叫做unique_lock
		unique_lock<mutex> lock(g_mtx);
		while (!g_vector.empty())//容器不为空,那就不生产,那就等待消费发来的通知
		{
			g_convar.wait(lock);
			//wait函数做了几件事:先释放锁资源,等待消费者发信号唤醒当前线程,在此之前这个线程处于阻塞状态,等收到信号之后马上去拿锁执行。
		}
		g_vector.push_back(i);//如果容器为空,那就生产一个产品
		cout << "生产者生产了产品:" << i << endl;
		g_convar.notify_all();//通知所有在队列中等待的消费者
		this_thread::sleep_for(chrono::milliseconds(100));
	}
}
//写消费者者线程调用的函数
void consumer()
{
	//消费每消费一个产品,就通知生产者生产一个
	for (int i = 0; i < 10; i++)
	{
		//生产之前进行加锁
		//这里使用新的一个模板类来管理mutex,叫做unique_lock
		unique_lock<mutex> lock(g_mtx);
		while (g_vector.empty())//容器为空,需要等待生产者发来的通知
		{
			g_convar.wait(lock);
			//wait函数做了几件事:先释放锁资源,等待生产者发信号唤醒当前线程,在此之前这个线程处于阻塞状态,等收到信号之后马上去拿锁执行。
		}
		cout << "消费者消费了产品:" << g_vector.back() << endl;
		g_vector.pop_back();//消费者消费一个产品
		g_convar.notify_all();//通知所有在队列中等待的生产者
		this_thread::sleep_for(chrono::milliseconds(100));
	}
}
void test09()
{
	thread t_p(producter);//生产者线程
	thread t_c(consumer);//消费者线程
	t_p.join();
	t_c.join();
}

6.练习

1、多线程模拟售票,简单模拟一下火车票的售票系统:有100张从北京到西安的火车票,
 在5个窗口同时出售(每个窗口是一个子线程),要保证票的数量始终是对的(不能卖超,票的数量要对的上)。

方法一:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>
#include <vector>
using namespace std;

//1、多线程模拟售票,简单模拟一下火车票的售票系统:有100张从北京到西安的火车票,
// 在5个窗口同时出售(每个窗口是一个子线程),要保证票的数量始终是对的(不能卖超,票的数量要对的上)。
//备注:不需要使用生产者消费者模型来实现,卖票就是直接更新余票数量即可

class TicketSeller
{
public:
	mutex g_mtx; //锁
	condition_variable cv; //线程同步
	int tickets;
public:
	TicketSeller(int n) : tickets(n) {}
	void sell()
	{
		unique_lock<mutex>lock(g_mtx);
		while (tickets <= 0)
		{
			cv.wait(lock);
		}
		if (tickets > 0)
		{
			cout << "窗口 " << this_thread::get_id() << "售出一张票,剩余:" << --tickets << "张票  \n";
			cv.notify_all();
		}
	}

};
void selltickets(TicketSeller&seller,int windowsId)
{
	while (true)
	{
		seller.sell();
		this_thread::sleep_for(chrono::milliseconds(100));
		if (seller.tickets == 0)
			break;
	}
}
void test1()
{
	TicketSeller sell(100);
	vector<thread>threads;//多线程容器

	for (int i = 0; i < 5; ++i)
	{
		threads.emplace_back(selltickets, ref(sell), i + 1);
	}
	for (auto& th : threads)
	{
		th.join();
	}
}

方法二:
//多线程模拟售票
mutex mutx;//需要使用互斥锁
int ticket = 100;//全局变量保存票数
//定义卖票的函数
void sell()
{
	//有票才能卖票,一次只卖一张票。
	while (ticket>0)
	{
		//接下来能不能直接卖呢?不能,因为还没有加锁,因为票数是多个线程共享的,不加锁无法保证正确性
		mutx.lock();//加锁之后的代码才是可靠的
		//重新写一个判断,有票才能卖,因为外层的while循环对ticket变量的判断是不可靠的
		if (ticket>0)//这里的ticket变量才是可靠的
		{
			cout << "售票窗口[" << this_thread::get_id() << "]:";
			ticket -= 1;
			cout << "售出1张票,还剩" << ticket << "张票" << endl;
		}
		else
		{
			cout << "售票窗口[" << this_thread::get_id() << "]:票已售完" << endl;
		}
		mutx.unlock();//解锁
		this_thread::sleep_for(chrono::milliseconds(100));
	}
}
void test10()
{
	thread c[5];
	for (int i = 0; i < 5; i++)
	{
		c[i] = thread(sell);
	}
	for (int i = 0; i < 5; i++)
	{
		c[i].join();
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值