并发与多线程
1. 并发、进程、线程的基本概念和综述
1.1 并发
两个或者更多任务同时发生;一个程序同时执行多个独立任务
单核CPU:
由操作系统调度,每秒钟进行多次所谓的任务切换,不是真正的并发,上下文切换是要有时间开销的。
硬件发展,出现了多处理器计算机,一块芯片上有多核CPU,能够实行真正的并发执行多个任务,硬件并发。
使用并发的原因:提高性能
1.2 可执行程序
1.3 进程
就是一个可执行程序运行起来了,就叫创建了一个进程
进程就是运行起来了的可执行程序
1.4 线程
每个进程都有一个主线程,这个主线程是唯一的,也就是一个进程只能有一个主线程;
当执行可执行程序时,产生了一个进程后,这个主线程就随着这个进程默默启动起来了;
运行一个程序的时候,实际上是进程的主线程来执行(调用)这个main函数中的代码;
主线程与进程唇齿相依,有你必然有我,没有我必然没有你;
线程:用来执行代码的;可以理解为一条代码的执行道路
通过主线程外,我们可以通过自己写代码来创建其他线程,其他线程走的是别的道路,通向的不同的地方;
每创建一个新线程,我就可以在同一时刻,多干一个不同的事;
多线程(并行)
线程并不是越多越好,每个线程,都需要一个独立的堆栈空间(1M),线程之间的切换要保存很多中间状态;
切换线程会耗费本该属于程序运行的时间;
总结:
a)线程是用来执行代码的;
b)把线程这个东西理解成一条代码的执行通路,一个新线程代表一个新的通路;
c)一个进程自动包含一个主线程,主线程随着进程默默的启动并运行,我们可以通过编码来创建多个其他线程(非主线程)//创建线程数量大都不建议超过200-300个,至于多少合适,实际项目中可以不断优化和调整;
d)因为主线程是自动启动的,所以一个进程中最少也是有一个线程(主线程),进程和主线程感觉是爹和儿子的关系;
e)说白了:多线程程序可以同时干多个事,所以运行效率高。但是到底有多高,并不是一个很容易评估和量化的东西。//需要在是实际项目中编程体会
1.5 学习心得
开发多线程程序:实力的体现,一个商用的必须需求;
线程开发有一定难度,实现代码更复杂。理解上更难一些,需要一定的学习时间;
C++线程会涉及很多新概念,对于C++道路上的成长特别关键,不要急于求成;
想拿高薪,网络通信,网络服务器,网络方向...多线程是绝对绕不开的;
2. 并发的实现方法
两个或者更多的任务(独立的活动)同时发生(进行)
实现并发的手段:
a)我们通过多个进程实现并发;
b)在单独的进程中,我创建多个线程来实现并发,自己写代码来创建除了主线程之外的线程;
2.1 多进程开发
word启动后是进程,游览器启动后就是个进程;
进程间通信(同一电脑上:管道、文件、消息队列、共享内存;不同电脑:socket通信技术);
2.2 多线程开发
线程:感觉像轻量级的进程,每个线程都有自己独立的运行路径,但是一个进程中的所有线程共享地址空间(共享内存)
//全局变量、指针、引用都可以在线程之间传递,所以:使用多线程开销远小于多进程
共享内存带来新的问题:数据一致性问题;线程A,线程B;
多进程并发和多线程并发虽然可以混合使用,但老师建议,优先考虑多线程技术手段而不是多进程;
本章中,只讲多线程并发开发技术,后续谈到并发,都指的是多线程开发。
2.3 总结
和进程比,线程的优点:
1)线程启动速度更快,更轻量级;
2)系统资源开销更少,执行速度更快,比如共享内存这种通信方式比任何其他的通信方式都快;
缺点:
使用有一定难度,要小心处理数据的一致性问题;
3. C++11新标准线程库
以往:
windows:CreateThread(), _beginthread(), _beginthreadexe()创建线程
Linux: pthread_create()创建线程
临界区、互斥量;
以往多线程代码不能跨平台;
C++11新标准,C++语言本身增加对多线程的支持,意味着可移植性(跨平台),大大减少了开发人员的工作量
二、线程启动、结束,创建线程方法、join,detach
1. 范例演示线程运行的开始和结束
程序运行起来,生成一个进程,该进程所属的主线程开始自动运行;
int main()
{
cout<<"I love China!"<<endl;//实际上这个是主线程在运行,主线程从main()函数返回,则整个进程执行完毕
return 0;
}
主线程从main()函数开始执行,那么我们自己创建的线程,也要从一个函数开始执行(初始函数),一旦这个函数运行完毕,就代表着我们这个线程运行结束;
整个进程是否执行完毕的标志是主线程是否执行完,如果主线程执行完毕,就代表整个进程执行完毕了;
此时如果其他子线程没有执行完毕,那么这些子线程也会被操作系统强行终止;
所以一般情况下,我们有这个结论:如果大家想保持子线程(自己用代码创建的线程)的运行状态的话,那么大家要让主线程一直保持运行,不要让主线程运行完毕;
//这条规律有例外,后续会解释这种例外,目前先这样理解和记忆;
a)包含一个头文件<thread>
b)初始函数要写
c)main中开始写代码
#include<iostream>
#include<thread>
using namespace std;
//自己创建线程也要从一个函数(初始函数)开始运行
void myprint()
{
cout<<"我的线程开始执行了"<<endl;
//...
//...
cout<<"我的线程执行完毕了"<<endl;
}
int main()
{
thread mytobj(myprint);
mytobj.join();
cout<<"主线程收尾,最终主线程安全正常退出"<<endl;
return 0;
}
大家必须明确一点:
这个程序有两个线程在跑,相当于整个程序的执行有两条线同时走,即使一条路被堵住了,另一条线还是可以通行的,这就是多线程
1.1 thread
thread:是个标准库里的类
//myprint:可调用对象
thread mytobj(myprint);//(1)创建了线程,线程执行起点(入口)myprint();(2)myprint线程开始执行
mytobj.join();
1.2 join()
join():加入/汇合,说白了就是阻塞,阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合,然后主线程再往下走。
thread mytobj(myprint);
//阻塞主线程并等待myprint()子线程执行完
mytobj.join();//主线程阻塞到这里等待myprint()执行完,当子线程执行完毕,这个join()就执行完毕,主线程就继续往下走
如果主线程执行完毕了,子线程还没执行完毕,这种程序员是不合格的,写出来的程序也是不稳定的;
//mytobj.join();//注释掉Join()
一个书写良好的程序,应该是主线程等待子线程完毕后,自己才能最终退出;
1.3 detach()
传统多线程程序主线程要等待子线程执行完毕,然后自己再最后退出;
detach:分离,也就是主线程不和子线程汇合了,你主线程执行你的,我子线程执行我的,你主线程也不必等我子线程运行结束,你可以先执行结束,这并不影响我子线程的执行;
为什么引入detach():我们创建了很多子线程,让主线程逐个等待子线程结束,这种编程方法不太好,所以引入detach();
一旦detach()之后,与主线程关联的thread对象就会失去与这个主线程的关联,此时这个子线程就会驻留在后台运行(主线程与子线程失去联系)
这个子线程就相当于被C++运行时库接管,当这个子线程执行完毕后,由运行时库负责清理该线程相关的资源(守护线程)
#include<iostream>
#include<thread>
using namespace std;
//自己创建线程也要从一个函数(初始函数)开始运行
void myprint()
{
cout<<"我的线程开始执行了"<<endl;
//...
//...
cout<<"我的线程执行完毕了1"<<endl;
cout<<"我的线程执行完毕了2"<<endl;
cout<<"我的线程执行完毕了3"<<endl;
cout<<"我的线程执行完毕了4"<<endl;
cout<<"我的线程执行完毕了5"<<endl;
cout<<"我的线程执行完毕了6"<<endl;
cout<<"我的线程执行完毕了7"<<endl;
cout<<"我的线程执行完毕了8"<<endl;
cout<<"我的线程执行完毕了9"<<endl;
cout<<"我的线程执行完毕了10"<<endl;
}
int main()
{
thread mytobj(myprint);
//mytobj.join();
mytobj.detach();//一但detach(),就不能在用join(),否则系统会报告异常
cout<<"主线程收尾,最终主线程安全正常退出1"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出2"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出3"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出4"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出5"<<endl;
return 0;
}
detach()使线程myprint失去我们自己的控制
1.4 joinable()
判断是否可以成功使用join()或者detach()的;
返回true或者false
#include<iostream>
#include<thread>
using namespace std;
//自己创建线程也要从一个函数(初始函数)开始运行
void myprint()
{
cout<<"我的线程开始执行了"<<endl;
//...
//...
cout<<"我的线程执行完毕了1"<<endl;
cout<<"我的线程执行完毕了2"<<endl;
cout<<"我的线程执行完毕了3"<<endl;
cout<<"我的线程执行完毕了4"<<endl;
cout<<"我的线程执行完毕了5"<<endl;
cout<<"我的线程执行完毕了6"<<endl;
cout<<"我的线程执行完毕了7"<<endl;
cout<<"我的线程执行完毕了8"<<endl;
cout<<"我的线程执行完毕了9"<<endl;
cout<<"我的线程执行完毕了10"<<endl;
}
int main()
{
thread mytobj(myprint);
if(mytobj.joinable())
{
cout<<"1:joinable()==true"<<endl;
}
else
{
cout<<"1:joinable()==false"<<endl;
}
//mytobj.join();
mytobj.detach();//一但detach(),就不能在用join(),否则系统会报告异常
if(mytobj.joinable())
{
cout<<"1:joinable()==true"<<endl;
}
else
{
cout<<"1:joinable()==false"<<endl;
}
cout<<"主线程收尾,最终主线程安全正常退出1"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出2"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出3"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出4"<<endl;
cout<<"主线程收尾,最终主线程安全正常退出5"<<endl;
return 0;
}
2. 其他创建线程的手法
2.1 用类,以及一个问题范例
join
#include<iostream>
#include<thread>
using namespace std;
class TA
{
public:
void operator()() //不能带参数
{
cout<<"我的线程开始执行了"<<endl;
cout<<"我的线程执行完毕了"<<endl;
}
}
int main()
{
TA ta;
thread mytobj3(ta);//ta:可调用对象
mytobj3.join();//等待子线程执行结束
cout<<"I love China"<<endl;
return 0;
}
detach
#include<iostream>
#include<thread>
using namespace std;
class TA
{
public:
int &m_i;
TA(int &i):m_i(i){}
void operator()() //不能带参数
{
//cout<<"我的线程开始执行了"<<endl;
//cout<<"我的线程执行完毕了"<<endl;
cout<<"m_i1的值为"<<m_i<<endl;
cout<<"m_i2的值为"<<m_i<<endl;
cout<<"m_i3的值为"<<m_i<<endl;
cout<<"m_i4的值为"<<m_i<<endl;
cout<<"m_i5的值为"<<m_i<<endl;
cout<<"m_i6的值为"<<m_i<<endl;
}
}
int main()
{
int myi=6;//主线程先结束后,局部变量被回收,构造函数中的参数是myi的引用,所以结果不可预料
TA ta(myi);
thread mytobj3(ta);//ta:可调用对象
mytobj3.detach();
cout<<"I love China"<<endl;
return 0;
}
大家可能有个疑问:一旦调用了detach(),那我主线程执行结束了,我这里的ta这个对象还在吗?(对象不在了)
这个对象实际上是被复制到线程中去;执行完主线程后,ta会被销毁,但是被复制的TA对象依旧存在。
所以只要你这个TA类对象里没有引用,没有指针,那么就不会产生问题
#include<iostream>
#include<thread>
using namespace std;
class TA
{
public:
int &m_i;
TA(int &i):m_i(i)
{
cout<<"TA()构造函数被执行"<<endl;
}
TA(const TA &ta):m_i(ta.m_i)
{
cout<<"TA()拷贝构造函数被执行"<<endl;
}
~TA()
{
cout<<"TA()析构函数被执行"<<endl;
}
void operator()() //不能带参数
{
//cout<<"我的线程开始执行了"<<endl;
//cout<<"我的线程执行完毕了"<<endl;
cout<<"m_i1的值为"<<m_i<<endl;
cout<<"m_i2的值为"<<m_i<<endl;
cout<<"m_i3的值为"<<m_i<<endl;
cout<<"m_i4的值为"<<m_i<<endl;
cout<<"m_i5的值为"<<m_i<<endl;
cout<<"m_i6的值为"<<m_i<<endl;
}
}
int main()
{
int myi=6;//主线程先结束后,局部变量被回收,构造函数中的参数是myi的引用,所以结果不可预料
TA ta(myi);
thread mytobj3(ta);//ta:可调用对象
mytobj3.join();
cout<<"I love China"<<endl;
return 0;
}
2.2 用lambda表达式
auto mylamthread=[]{
cout<<"我的线程3开始执行了"<<endl;
cout<<"我的线程3执行结束了"<<endl;
};
thread mytobj4(mylamthread);
mytobj4.join();
cout<<"I love China"<<endl;
return 0;
三、线程传参详解,detach()大坑,成员函数做线程函数
1. 传递临时对象作为线程参数
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
void myprint(const int &i,char *pmybuf)
{
cout<<i<<endl;
cout<<pmybuf<<endl;
return;
}
int main()
{
//传递临时对象作为线程参数
int mvar=1;
int &mvary=mvar;
char mybuf[]="this is a test!";
thread mytobj(myprint,mvar,mybuf);
mytobj.join();
cout<<"I love China!"<<endl;
return 0;
}
1.1 要避免的陷阱(解释1)
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
void myprint(const int &i,char *pmybuf)
{
cout<<i<<endl;//分析认为,i并不是mvar的引用,实际上是值传递,那么我们认为,即便是主线程detach了子线程,那么子线程中用i值仍然是安全的
cout<<pmybuf<<endl;//pmybuf是mybuf的引用,指针在detach子线程时,绝对有问题
return;
}
int main()
{
//传递临时对象作为线程参数
int mvar=1;
int &mvary=mvar;
char mybuf[]="this is a test!";
thread mytobj(myprint,mvar,mybuf);
//mytobj.join();
mytobj.detach();//子线程和主线程分别执行
cout<<"I love China!"<<endl;
return 0;
}
1.2 要避免的陷阱(解释2)
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
void myprint(int i, const string &pmybuf)//把mybuf数组隐式转换成pmybuf,子线程就不会用主线程的mybuf的内存
{
cout << i << endl;
//cout << pmybuf << endl;
cout<< pmybuf.c_str()<<endl;
return;
}
int main()
{
//传递临时对象作为线程参数
int mvar = 1;
int& mvary = mvar;
char mybuf[] = "this is a test!";
//read mytobj(myprint, mvar, mybuf);
//但是mybuf到底是什么时候转换成string的?
//事实上,存在mybuf都回收了(main函数执行完了),系统才用mybuf去转string
thread mytobj(myprint, mvar, string(mybuf));//我们这里直接将mybuf转换成string对象,这是一个可以保证在线程中用肯定有效的对象
//在创建线程的同时构造临时对象的方法传递参数是可行的
//mytobj.join();
mytobj.detach();//子线程和主线程分别执行
cout << "I love China!" << endl;
return 0;
}
事实1:只要用临时构造的A类对象作为参数传递给线程,那么就一定能够在主线程执行完毕前把线程函数的第二个参数构建出来,从而确保即便detach(),子线程也安全运行;
1.3 总结
a)若传递int这种简单类型参数,建议都是值传递,不要直接引用,防止节外生枝;
b)如果传递类对象,避免隐式类型转换;全部都在创建线程这一行就构建出临时对象来,然后在函数参数里,用引用来接;否则系统还会多钩爪一次对象,浪费时间;
终极结论:
c)建议不使用detach(),只使用join(),这样就不存在局部变量失效导致线程对内存的非法引用问题;
2. 临时对象作为线程参数继续讲
老师常用测试打法
2.1 线程id的概念
id:是个数字,每个线程(不管是主线程还是子线程)实际上都对应则一个数字,而且每个线程对应的这个id都不同;
也就是说,不用的线程id(数字)必然是不同的;
线程id可以用C++标准库里的函数来获取。std::this_thread::get_id()来获取
2.2 临时对象构造实际抓捕
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
class A
{
public:
int m_i;
//类型转换构造函数,可以把一个int转换成一个类A对象
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) :m_i(a.m_i) {
cout << "[A::A(const A)拷贝构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
};
void myprint2(const A &pmybuf)
{
cout << "子线程myprint2的参数地址是" << &pmybuf << "threadid = " << std::this_thread::get_id() << endl;
}
int main()
{
cout << "主线程id是 " << std::this_thread::get_id() << endl;
int mvar = 1;
//std::thread mytobj(myprint2, mvar);
std::thread mytobj(myprint2, A(mvar));
mytobj.join();
cout << "I love China!" << endl;
return 0;
}
致命的问题:居然是在子线程中构造的A类对象
好消息:用了临时对象后,所有的A类对象都在mian()函数中都已经构建完毕了
3. 传递类对象、智能指针作为线程参数
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
class A
{
public:
mutable int m_i;//不管什么时候,这个m_i都能修改
//类型转换构造函数,可以把一个int转换成一个类A对象
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) :m_i(a.m_i) {
cout << "[A::A(const A)拷贝构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
};
//虽然用了引用作为参数,实际上依然用了拷贝构造函数新构造了一个对象(为了数据安全考虑)
void myprint2(const A &pmybuf)//引用作为参数,前面必须加const;
{
pmybuf.m_i = 199; //我们修改该值不会影响到main函数
cout << "子线程myprint2的参数地址是" << &pmybuf << "threadid = " << std::this_thread::get_id() << endl;
}
int main()
{
A myobj(10);//生成一个类对象
std::thread mytobj(myprint2, myobj);//将类对象作为线程参数
mytobj.join();
//mytobj.detach();
cout << "I love China!" << endl;
return 0;
}
std::ref 函数
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
class A
{
public:
int m_i;//不管什么时候,这个m_i都能修改
//类型转换构造函数,可以把一个int转换成一个类A对象
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) :m_i(a.m_i) {
cout << "[A::A(const A)拷贝构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" << this << "threadid = " << std::this_thread::get_id()<< endl;
}
};
void myprint2( A &pmybuf)
{
pmybuf.m_i = 199; //我们修改该值不会影响到main函数
cout << "子线程myprint2的参数地址是" << &pmybuf << "threadid = " << std::this_thread::get_id() << endl;
}
int main()
{
A myobj(10);//生成一个类对象
std::thread mytobj(myprint2, std::ref(myobj));//加了std::ref引用传参后,地址真正相同了;避免使用构造函数,提高了性能
mytobj.join();
//mytobj.detach();
cout << "I love China!" << endl;
return 0;
}
智能指针
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
class A
{
public:
int m_i;//不管什么时候,这个m_i都能修改
//类型转换构造函数,可以把一个int转换成一个类A对象
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) :m_i(a.m_i) {
cout << "[A::A(const A)拷贝构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" << this << "threadid = " << std::this_thread::get_id()<< endl;
}
};
void myprint2(unique_ptr<int> pzn)
{
cout << "子线程myprint2的参数地址是" << "threadid = " << std::this_thread::get_id() << endl;
}
int main()
{
unique_ptr<int> myp(new int(100));
//std::move:可以使一个独占式智能指针转到另一个独占式智能指针
//把myp的指针转到线程的pzn里,过后myp指针为空
std::thread mytobj(myprint2,std::move(myp));
mytobj.join();//指针指向的是主线程中的内存,因此只能用join(),不能用detach()
//mytobj.detach();
cout << "I love China!" << endl;
return 0;
}
4. 用成员函数指针做线程函数
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
class A
{
public:
int m_i;//不管什么时候,这个m_i都能修改
//类型转换构造函数,可以把一个int转换成一个类A对象
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) :m_i(a.m_i) {
cout << "[A::A(const A)拷贝构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" << this << "threadid = " << std::this_thread::get_id()<< endl;
}
void operator()(int num)
{
cout << "子线程()的参数地址是" << "threadid = " << std::this_thread::get_id() << endl;
}
};
int main()
{
A myobj(10);//生成一个类对象
std::thread mytobj(std::ref(myobj), 15);//加了std::ref,只能用join()
//不调用拷贝构造函数了,那么后续如果继续调用mytobj.detach()就不安全了
mytobj.join();
//mytobj.detach();
cout << "I love China!" << endl;
return 0;
}
operator()做线程入口扩展到任意函数做线程入口:
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<thread>
using namespace std;
class A
{
public:
int m_i;//不管什么时候,这个m_i都能修改
//类型转换构造函数,可以把一个int转换成一个类A对象
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) :m_i(a.m_i) {
cout << "[A::A(const A)拷贝构造函数执行]" << this << "threadid = " << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" << this << "threadid = " << std::this_thread::get_id()<< endl;
}
void thread_work(int num)//来个参数
{
cout << num << endl;
cout << "子线程myprint2的参数地址是" << "threadid = " << std::this_thread::get_id() << endl;
}
};
int main()
{
A myobj(10);//生成一个类对象
std::thread mytobj(&A::thread_work, std::ref(myobj), 15);
//等价 std::thread mytobj(&A::thread_work, &myobj, 15);
mytobj.join();
//mytobj.detach();
cout << "I love China!" << endl;
return 0;
}
四、创建多个线程、数据共享问题分析、案例代码
1.创建和等待多个线程
#include<map>
#include<string>
#include<thread>
#include<list>
#include<mutex>
#include <vector>
#include<iostream>
using namespace std;
//线程入口函数
void myprint(int inum)
{
cout<<"myprint线程开始执行了,线程编号 = "<<inum<<endl;
//...
cout<<"myprint线程结束执行了,线程编号 = "<<inum<<endl;
}
int main()
{
//创建和等待多个线程
vector<thread>mythreads;
//创建10个线程,线程入口函数统一使用myprint
for(int i=0;i<10;i++)
{
mythreads.push_back(thread(myprint,i));//创建10个线程吗,同时这10个线程已经开始执行
}
for(auto iter=mythreads.begin();iter!=mythreads.end();++iter)
{
iter->join();//等待10个线程都返回
}
cout<<"主线程结束"<<endl;//最后执行这句,整个进程退出
return 0;
}
a)多个线程执行顺序是乱的,跟操作系统内部对线程的运行制度机制有关
b)主线程等待所有子线程运行结束,最后主线程结束,老师推荐这种join()的写法,更容易写出更稳定的程序
C)咱们把thread对象放入到容器里管理,看起来像个thread对象数组,这对我们一次创建大量的线程并对大量线程进行管理是很方便的
2.数据共享问题分析
2.1只读的数据
#include<map>
#include<string>
#include<thread>
#include<list>
#include<mutex>
#include <vector>
#include<iostream>
using namespace std;
vector <int>g_v={1,2,3};
//线程入口函数
void myprint(int inum)
{
cout<<"id为 "<<std::this_thread::get_id()<<"的线程 打印g_v值"<<g_v[0]<<g_v[1]<<g_v[2]<<endl;
}
int main()
{
vector<thread>mythreads;
//创建10个线程,线程入口函数统一使用myprint
for(int i=0;i<10;i++)
{
mythreads.push_back(thread(myprint,i));//创建10个线程吗,同时这10个线程已经开始执行
}
for(auto iter=mythreads.begin();iter!=mythreads.end();++iter)
{
iter->join();//等待10个线程都返回
}
cout<<"主线程结束"<<endl;//最后执行这句,整个进程退出
return 0;
}
只读数据,是安全稳定的,不需要特别什么处理。直接读就可以
2.2有读有写
2个程序写,8个程序读,如果代码没有特别的处理,那程序肯定崩溃;
最简单的不崩溃处理,读的时候不能写,写的时候不能读。2个线程不能同时写,8个线程不能同时读;
写的时候分10小步;由于任务切换,导致各种诡异的事情发生(最可能发生崩溃)
2.3其他案例
数据共享;
3.共享数据的保护案例代码
网络游戏服务器。两个自己创建的线程,一个线程收集玩家的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
另一个线程,从队列中取出玩家发送来的命令,解析,然后执行玩家需要的动作;
vector, list;
list:频繁的按顺序插入和删除数据时效率高;
vector:容器水机的插入和删除数据效率高;
准备用成员函数作为线程函数的方法写线程
#include<map>
#include<string>
#include<thread>
#include<list>
#include<mutex>
#include <vector>
#include<iostream>
using namespace std;
class A {
public:
//把收到的消息(玩家命令)输入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inmsgRecvQueue()执行,插入一个元素" << i << endl;
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列里边来;
}
}
//把数据从消息队列取出的线程
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front();//移除第一个元素,但不返回;
//这里就考虑处理数据...
}
else
{
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器(消息队列),专门用于代表玩家给咱们发送过来的命令 //共享数据
};
int main()
{
A myobja;
std::thread myOut(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myIn(&A::inMsgRecvQueue, &myobja);
myOut.join();
myIn.join();
cout << "主线程结束" << endl;//最后执行这句,整个进程退出
return 0;
}
代码化解决问题:引入一个C++解决多线程保护共享数据问题的第一个概念”互斥量“,记住这个词
五、互斥量概念、用法、死锁演示及解决详解
保护共享数据,操作时,某个线程用代码把共享数据锁住,操作数据,解锁,其他想操作共享数据的线程必须等待解锁,锁定住,操作,解锁
互斥量
1. 互斥量(mutex)的基本概念
互斥量是个类对象。理解成一把锁,多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程能锁成功(成功的标记是lock()函数返回),如果没锁成功,那么线程卡在lock()这里不断尝试去锁这把锁头;
互斥量使用要小心,保护数据不多也不少,少了,没达到保护效果;多了,影响效率;
2. 互斥量的用法
2.1 lock(),unlock()
步骤:先lock(),操作共享数据,再unlock()
lock()和unlock()要承兑使用,有lock()必然要有unlock(),没调用一次lock(),必然应该调用一次unlock();
不应该也不允许调用1次lock()却调用2次unlock(),也不允许调用2次lock却调用1次unlock();
这些非对称数量的调用都会导致代码不稳定甚至崩溃;
#include<map>
#include<string>
#include<thread>
#include<list>
#include<mutex>
#include <vector>
#include<iostream>
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();
}
return;
}
bool outMsgLULProc(int& command)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在;
cout << "我移除了 " << command << endl;
msgRecvQueue.pop_front();//移除第一个元素,但不返回;
my_mutex.unlock();
return true;
}
my_mutex.unlock();
cout << " 为空 " << endl;
return false;
}
//把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if(result==true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//考虑继续处理数据...
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器(消息队列),专门用于代表玩家给咱们发送过来的命令 //共享数据
std::mutex my_mutex; //创建一个互斥量
};
int main()
{
A myobja;
std::thread myIn(&A::inMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myOut(&A::outMsgRecvQueue, &myobja);
myIn.join();
myOut.join();
cout << "主线程结束" << endl;//最后执行这句,整个进程退出
return 0;
}
2.2 std::lock_guard类模板
为了防止大家忘记unlock(),引入了一个叫std::lock_guard的类模板:你忘记unlock不要紧,我替你unlock();
学过智能指针(unique_ptr<>):你忘记释放内存不要紧,我给你释放;保姆;
std::lock_guard类模板:直接取代lock()和Unlock():也就是说,你用了lock_guard之后,在不能用lock()和Unlock();
#include<map>
#include<string>
#include<thread>
#include<list>
#include<mutex>
#include <vector>
#include<iostream>
using namespace std;
class A {
public:
//把收到的消息(玩家命令)输入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inmsgRecvQueue()执行,插入一个元素" << i << endl;
{
std::lock_guard<std::mutex> sbguard(my_mutex);
//my_mutex.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列里边来;
//my_mutex.unlock();
}
}
return;
}
bool outMsgLULProc(int& command)
{
std::lock_guard<std::mutex> sbguard(my_mutex); //sbguard是自己起的对象名
//lock_guard构造函数里执行了mutex::lock()
//lock_guard析构函数里执行了mutex::unlock() (return之前)
//my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在;
cout << "我移除了 " << command << endl;
msgRecvQueue.pop_front();//移除第一个元素,但不返回;
//my_mutex.unlock();
return true;
}
//my_mutex.unlock();
cout << " 为空 " << endl;
return false;
}
//把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if(result==true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//考虑继续处理数据...
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器(消息队列),专门用于代表玩家给咱们发送过来的命令 //共享数据
std::mutex my_mutex; //创建一个互斥量
};
int main()
{
A myobja;
std::thread myIn(&A::inMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myOut(&A::outMsgRecvQueue, &myobja);
myIn.join();
myOut.join();
cout << "主线程结束" << endl;//最后执行这句,整个进程退出
return 0;
}
3. 死锁
一个互斥量是一把锁
C++中:
比如有两把锁(死锁这个问题,是由至少两把锁头也就是两个互斥量才能产生);
金锁和银锁;
两个线程A,B
1)线程A执行的时候,这个线程先锁 金锁,把金锁lock()成功了,然后它去lock()银锁。。
出现了上下文切换
2)线程B执行了,这个线程先锁 银锁,因为银锁还没有被锁,所以银锁会lock()成功,线程B要去lock()金锁。。。
此时此刻,死锁就发生了;
3)线程A因为拿不到银锁头,流程走不下去(所有后边的代码有解锁金锁头的但是流程走不下去,所以金锁头解不开)
4)线程B因为拿不到金锁头,流程走不下去(所有后边的代码有解锁银锁头的但是流程走不下去,所以银锁头解不开)
大家都晾在这里,你等我,我等你
3.1 死锁演示
#include<map>
#include<string>
#include<thread>
#include<list>
#include<mutex>
#include <vector>
#include<iostream>
using namespace std;
class A {
public:
//把收到的消息(玩家命令)输入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inmsgRecvQueue()执行,插入一个元素" << i << endl;
{
my_mutex1.lock();//实际工程这两个锁头不一定挨着,需要保护不同的数据共享块
my_mutex2.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列里边来;
my_mutex2.unlock();
my_mutex1.unlock();
}
}
return;
}
bool outMsgLULProc(int& command)
{
my_mutex2.lock();
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在;
cout << "我移除了 " << command << endl;
msgRecvQueue.pop_front();//移除第一个元素,但不返回;
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
cout << " 为空 " << endl;
return false;
}
//把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if(result==true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//考虑继续处理数据...
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器(消息队列),专门用于代表玩家给咱们发送过来的命令 //共享数据
std::mutex my_mutex1; //创建一个互斥量(一把锁头)
std::mutex my_mutex2; //创建一个互斥量
};
int main()
{
A myobja;
std::thread myIn(&A::inMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myOut(&A::outMsgRecvQueue, &myobja);
myIn.join();
myOut.join();
cout << "主线程结束" << endl;//最后执行这句,整个进程退出
return 0;
}
3.2 死锁的一般解决方案
两个互斥量只要调用顺序保持一致就不发生死锁
3.3 std::lock()函数模板
用来处理多个互斥量的时候出场
能力:一次锁住两个或者两个以上的互斥量(至少两个,多了不限,1个不行);
它不存在这种因为再多个线程中,因为锁的顺序问题导致死锁的风险问题
std::lock():如果互斥量中有一个没锁住,它就在那等着,等所有互斥量都锁住,它才能往下走(返回);
要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另一个没锁成功,则它立即把已经锁住的解锁;
std::lock(my_mutex1,my_mutex2)
...
my_mutex1.unlock()
my_mutex2.unlock()
3.4 std::lock_guard的std::adopt_lock参数
std::lock(my_mutex1,my_mutex2)//开始lock()
//std::adopt_lock是个结构体对象,起一个标记作用,作用就是表示这个互斥量已经lock(),不需要在std::lock_guard<std::mutex>里面对mutex对象进行再次lock()
std::lock_guard<std::mutex>sbguard1(my_mutex1,std::adopt_lock);
std::lock_guard<std::mutex>sbguard2(my_mutex2,std::adopt_lock);
...
//析构自动unlock()
总结:std::lock():一次锁定多个互斥量;谨慎使用(建议一个一个锁);
加了临界区时的代码及输出
#include<iostream>
#include<thread>
#include<windows.h>
using namespace std;
CRITICAL_SECTION Critical; //定义临界区句柄
int i = 0; // 公共数据
// 线程1函数
void aaa()
{
EnterCriticalSection(&Critical);
for (int t = 0; t < 5; t++)
{
i++;
cout << "线程1中 i = " << i << endl;
}
LeaveCriticalSection(&Critical);
}
// 线程2函数
void bbb()
{
EnterCriticalSection(&Critical);
for (int t = 0; t < 5; t++)
{
i++;
cout << "线程2中 i = " << i << endl;
}
LeaveCriticalSection(&Critical);
}
int main(int argc, char* argv[])
{
InitializeCriticalSection(&Critical); //初始化临界区对象
thread mythread(aaa);
thread mythread2(bbb);
mythread.join();
mythread2.join();
return 0;
}