并发
两个或者更多的任务(独立的活动)同时发生,宏观上是同时进行的,微观上是同一时刻只有一个任务在进行;一个程序同时执行多个独立的任务
单核cpu:某一时刻只能进行一个任务,由操作系统调度,每秒钟进行多次的任务切换,不知真正的同时进行,这种切换(上下文切换)是由时间开销的,操作系统需要保存切换时的各种状态,执行进度等,切换回来时需要恢复
多核cpu:多处理器计算机,能够实现真正的并行执行多个任务
进程
当一个可执行程序运行起来后,就叫创建了一个进程
进程就是运行起来的程序
线程
每个进程就是执行起来的可执行程序,每个进程都有一个主线程,这个主线程是唯一的,也就是一个进程中只能有一个主线程,轻量级进程
当你执行可执行程序,产生了一个进程后,这个主线程就随着这个进程默默启动。
线程实际是执行代码的,线程可以理解为一条代码的执行通路
除了主线程之外,我们可以通过代码创建其他线程,其他线程走的是别的道路
每创建一个新线程,就可以在同一时刻多干一个不同的事
多线程(并发)
线程并不是越多越好,每个线程都需要一个独立的堆栈空间(1M),线程之间的切换需要保存很多中间状态,切换会耗费本该属于程序运行的时间.
多线程可以提高运行效率,但是不是很容易评估,需要在实际项目中不断优化
实现并发的方式
1)通过多个进程实现并发
2)在单独的进程中,创建多个线程来实现并发,自己写代码来创建除了主线程之外的其他线程
多进程并发
同一电脑上进程间通信:管道,文件,消息队列,共享内存
不同电脑上进程间通信:socket通信技术
多线程并发
同一进程中的所有线程共享地址空间(共享内存)
全局变量,指针,引用都可以在线程之间传递,所以使用多线程开销远远小于多进程
共享内存带来的问题:数据一致性问题
多进程和多线程虽然可以混合使用 ,但优先考虑多线程
c++11新标准线程库
以往的不能跨平台,windows:CreateThread(),_beginthred(),_beginthredexe()创建线程
linux:pthread_create()创建线程
从c11新标准,c本身增加对多线程的支持,增加了可移植性
示例
主线程从main()开始执行,我们自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,就代表这个线程结束
整个函数执行完毕的标志是主线程是否执行完毕,如果主线程执行完毕,就代表整个线程执行完毕,一般情况下,如果还有其他子线程没有执行完毕,那么这些子线程会被操作系统强行终止。一般情况下,如果想保持子线程的运行状态,那么就要让主线程一直保持运行(有例外)
1)要包含#include<thread>
2)初始函数
#include<thread>
#include<iostream>
using namespace std;
void myprint()
{
cout<<"myprint_start"<<endl;
cout<<"myprint_end"<<endl;
}
int main(int argc, char const *argv[])
{
/* code */
thread mytobj(myprint); //创建了线程,线程起点为myprint(),并让myprint()开始执行
mytobj.join(); //阻塞主线程,让主线程等待子线程执行完毕,然后主线程和子线程汇合,然后主线程继续往下执行
cout<<"main"<<endl;
return 0;
}
/*输出: //join阻塞主线程,等待子线程执行完
myprint_start
myprint_end
main
*/
linux使用g++编译标准线程库项目时,需要-lpthread
参数,如上面的代码名为test.cpp,编译命令为g++ test7.cpp -o test7 -lpthread
如果主线程执行完毕,但子线程没执行完毕,这种程序是不稳定的
detach():传统的多线程程序需要等待子线程执行完毕,然后主线程退出;但是detach()可以让主线程与子线程分离,主线程可以先执行结束,不用等待子线程执行结束。一旦detach()之后,与主线程关联的thread对象就会失去与主线程的关联,此时子线程就会驻留在后台运行,子线程相当于被c++运行时库接管,当这个子线程执行完毕后,由运行时库负责清理该线程的相关资源(守护线程)
joinable():判断是否可以使用join()和detach()
其他创建线程的手法
使用类创建
class TA
{
public:
TA(){
cout<<"TA()"<<endl;
}
TA(const TA &ta){
cout<<"TA(const TA &ta)"<<endl;
}
~TA(){
cout<<"~TA()"<<endl;
}
void operator()(){
cout<<"TA_start"<<endl;
cout<<"TA_end"<<endl;
}
};
TA ta; //TA()
thread myobj(ta); //TA(const TA &ta),TA_start,TA_end,~TA() 析构拷贝构造函数创建的TA对象
myobj.join();
cout<<"main"<<endl; //main
//~TA() 析构ta
如果使用detach(),主线程执行完毕了,那么ta这个对象还在吗?
这个对象实际是被复制到线程中,执行完主线程,ta被销毁,但是所复制的TA对象依旧存在;所以只要TA类中没有引用,指针,就不会产生问题
使用lambda表达式创建
auto mylamthread = []{
cout<<"mylamthread_start"<<endl;
cout<<"mylamthread_end"<<endl;
}
thread myobj(mylamthread);
myobj.join();
cout<<"main"<<endl;
线程传参
传递临时对象作为线程参数
#include<thread>
#include<iostream>
using namespace std;
void myprint(const int &i,char *buf) //如果使用detach,不推荐使用引用,不能使用指针
{
cout<<i<<endl; //分析认为,i并不是myarg的引用,实际是值传递,即使主线程detach()那么子线程中也是安全的
cout<<buf<<endl; //指针如果使用detach()则是不安全的,主线程执行完毕会被回收
}
void myprint2(const int i,const string &buf)
{
cout<<i<<endl;
cout<<buf<<endl;
}
int main(int argc, char const *argv[])
{
int myarg=10;
char mybuf[] = "this is a test";
// thread t1(myprint,myarg,mybuf); //不安全
// thread t1(myprint2,myarg,mybuf); //有隐患,事实上存在,mybuf被回收了,系统才用mybuf转string的可能性
thread t1(myprint2,myarg,string(mybuf)); //稳定
t1.join();
cout<<"main"<<endl;
return 0;
}
总结:
1)若传递int这种简单类型参数,建议都是值传递,不要用引用
2)如果传递类对象,避免隐式类型转换,全部都在创建线程这一行就构建出临时对象,然后再函数参数中使用引用来接收;否则系统还会创建一个临时对象
建议不使用detach(),只是用join(),就不会出现临时对象失效导致线程对内存的非法引用的问题
线程id
每个线程,不管是主线程还是子线程,实际上都对应一个数字,而且每个线程对应的这个数字都不相同,也就是说不同线程他们的线程id不同,可以使用标准库std::this_thread()::get_id()
获取
传递类对象、智能指针作为线程参数
class TA
{
public:
// mutable int m_i; //在子线程中修改m_i的值,主线程仍然不改变
int m_i;
TA(int tmp):m_i(tmp){
cout<<"TA()"<<endl;
}
TA(const TA &ta):m_i(ta.m_i){
cout<<"TA(const TA &ta)"<<endl;
}
~TA(){
cout<<"~TA()"<<endl;
}
};
void myprint(TA &ta) //不使用std::ref()时,必须加const
{
ta.m_i = 199; // 可以正常修改
cout<<”thtreadid=“<<this_thread()::get_id()<<endl;
}
void myprint2(unique_ptr<int> pzn)
{
cout<<”thtreadid=“<<this_thread()::get_id()<<endl;
}
unique_ptr<int> myp(new int(100));
//thread myobjp(myprint2,myp); //不能直接传递
thread myobjp(myprint2,move(myp)); //不能配detach()
myobjp.join();
TA ta(10);
thread myobj(myprint,ref(ta));
myobj.join();
cout<<"main"<<endl;
std::ref()
:传递的参数不会再拷贝一份,是真正的引用
创建和等待多个线程
//线程入口函数
void myprint(const int i)
{
cout<<"myprint_start"<<i<<endl;
cout<<"myprint_end"<<i<<endl;
}
vector<thread> mythreads;
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<<"all finish"<<endl;
多个线程的执行顺序是乱的,跟操作系统内部的调度机制有关;主线程等待所有子线程运行结束,最后主线程结束,推荐使用join(),程序更加稳定
数据共享问题
只读的数据
只读的数据是安全稳定的,不需要特别的处理手段,直接读就可以
有读有写的数据
多个线程写,多个线程读,如果代码没有特殊处理,程序肯定崩溃
最简单的处理方式:读的时候不能写,写的时候不能读,多个线程不能同时写,多个线程不能同时读
其他案例
火车订票
共享数据保护示例
class A
{
public:
//收到的数据加入到消息队列的线程
void inmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
msgRecvQueue.push_back(i);
}
}
//把数据从消息队列中取出的线程
void outmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
if(!msgRecvQueue.empty())
{
cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
msgRecvQueue.pop_front();
}
else
{
cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
}
}
}
private:
list<int> msgRecvQueue;
};
A myobj;
thread mymsginobj(&A::inmsgRecvQueue,&myobj); //第二个参数必须为引用,这样才能保证线程中用的是同一个对象
thread mymsgoutobj(&A::outmsgRecvQueue,&myobj);
mymsginobj.join();
mymsgoutobj.join();
// 有异常,又读有写,同时操作出现异常
互斥量
保护共享数据操作时,用代码将共享数据锁住,操作数据,解锁,其他想操作共享数据的线程必须等待共享线程解锁
互斥量是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数加锁,只有一个线程可以锁定成功,成功的标志是lock()函数返回,如果没有锁成功,那么流程会阻塞在lock()这里直到成功
使用互斥量mutex,需要#include<mutex>
,使用时先lock()
,操作共享数据,再unlock()
。lock()和unlock()必须成对使用,非对称数量的调用会导致代码不稳定
class A
{
public:
//收到的数据加入到消息队列的线程
void inmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
my_mutex.lock();
cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
}
//把数据从消息队列中取出的线程
void outmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
my_mutex.lock();
if(!msgRecvQueue.empty())
{
cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
msgRecvQueue.pop_front();
my_mutex.unlock();
}
else
{
cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
my_mutex.unlock();
}
}
}
private:
list<int> msgRecvQueue;
mutex my_mutex; //创建一个互斥量
};
为了防止忘记unlock(),引入了std::lock_guard()的类模板,可以自动unlock(),类似于智能指针自动释放内存;std::lock_guard()直接取代lock()和unlock(),用了std::lock_guard(),就不能再使用lock()和unlock()了
class A
{
public:
//收到的数据加入到消息队列的线程
void inmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
my_mutex.lock();
cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
}
//把数据从消息队列中取出的线程
void outmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
std::lock_guard<std::mutex> sbguard(my_mutex);
if(!msgRecvQueue.empty())
{
cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
msgRecvQueue.pop_front();
}
else
{
cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex my_mutex; //创建一个互斥量
};
死锁:要产生死锁,必须至少含有两个互斥量。
有两个锁lock1和lock2,两个线程都需要两个互斥量加锁,线程A执行的时候,先加锁lock1,再加锁lock2;线程B相反。线程A加锁lock1之后,线程切换,线程B再加锁lock2,这样就形成了死锁
保证两个互斥量的上锁顺序一致就不会产生死锁
std::lock()
函数模板:可以一次加锁两个及两个以上的互斥量,可以有效解决因为锁的顺序问题导致的死锁风险。用了std::lock(),还是得使用unlock()来解锁;也可以使用下面的方法进行处理
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);
//这样就不需要unlock()了
std::adopt_lock
:是个结构体对象,起一个标志作用,作用是表示这个互斥量已经Lock了,不需要在std::lock_guardstd::mutex
构造函数中再对mutex对象加锁
unique_lock
unique_lock是一个类模板,工作中,一般使用lock_guard()(推荐使用)
unique_lock比lock_guard更加灵活,但是效率上差一点,内存占用多一点
正常情况下,unique_lock可以直接替换lock_guard使用
unique_lock的第二个参数
std::adopt_lock:表示这个互斥量已经Lock了,不需要在std::lock_guardstd::mutex
构造函数中再对mutex对象加锁,使用之前需要先加锁;lock_guard也可以带这个参数,含义相同
std::try_to_lock:尝试使用mutex的lock()来锁定这个mutex,如果没有成功,会立即返回,并不会阻塞;前面不能lock()
class A
{
public:
//收到的数据加入到消息队列的线程
void inmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
std::unique_lock<mutex> sbguard(my_mutex, std::try_to_lock);
if(sbguard.owns_lock())
{
cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
msgRecvQueue.push_back(i);
}else{
cout<<"inmsgRecvQueue()执行,没有拿到锁"<<i<<endl;
}
}
}
//把数据从消息队列中取出的线程
void outmsgRecvQueue()
{
for(int i = 0; i<100000;i++)
{
std::unique_lock<std::mutex> sbguard(my_mutex);
std::chrono::milliseconds dura(100);
std::this_thread::sleep_for(dura);
if(!msgRecvQueue.empty())
{
cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
msgRecvQueue.pop_front();
}
else
{
cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex my_mutex; //创建一个互斥量
};
std::defer_lock:使用defer_lock()时,不能先lock();并没有给mutex加锁,即初始化一个未加锁的mutex,就可以调用unique_lock的成员函数
lock()
:unique_lock的加锁函数,不需要unlock()
unlock()
:unique_lock的解锁函数,可以随时解锁
try_lock()
:尝试给互斥量加锁,如果返回true,表示拿到锁;返回false,表示未拿到锁。这个函数是非阻塞的
release()
:返回它所管理的mutex对象指针,并释放所有权,也就是说,这个unique_lock和mutex不再有关系
有人把锁头锁住的代码多少称为锁的粒度,锁住的代码少,则粒度细,执行效率高;锁住的代码多,粒度粗,执行效率低。所以要尽量选择合适的粒度进行保护
评论区