文章目录
并安、进程、线程的基本概念
-
并发
表示两个或更多任务同时发生
因为有多个CPU就可以同时做多件事情
线程数量就可以理解为并发的任务数量 -
可执行程序
一个可执行程序运行起来就相当于创建了一个进程 -
线程
每一个进程都有一个主线程且唯一
主线程从北京到深圳
编写代码创建其他线程从北京到南京
把线程理解成一条代码的执行通路,一个新线程代表一条新的通路
17.1.2 并发的实现方法
多进程并发和多线程并发
- 多进程并发:
进程之间的通信手段比较多,如果是同一台计算机上的进程之间的通信可以使用管道、文件、消息队列、共享内存等技术来实现,不同计算机之间的通信可以用socket(网络套接字)等网络通信技术来实现。由于进程之间的数据保护问题,即使在同一计算机上进程之间的通信也是挺复杂的。 - 多线程并发
多线程就是在单个进程中创建多个线程,一个进程中的所有线程共享地址空间(共享内存),还有诸如全局变量、指针、引用等都是可以在线程之间传递的。
多线程使用的共享内存虽然灵活,但是也带了了新的问题:数据一致性问题例如线程A要写一块数据,线程B也要写。
本章只讲解多线程的并发技术,所以后续谈到并发都是多线程。
线程的启动、结束与创建线程的写法
- join()
join是汇合的意思
{
auto mylamthread = [] {
cout << "我的线程开始执行了" << endl;
//...
cout << "我的线程执行完毕了" << endl;
};
thread mytobj4(mylamthread); // 使用thread创建一个线程
mytobj4.join(); // 主线程等待子线程执行完毕后,自己才能最终退出
cout << "main主函数执行结束!" << endl;
}
- detach
detach是分离的意思,主线程不和子线程汇合了,主线程执行主线程,子线程执行子线程,主线程不必等待子线程运行结束,可以先执行结束,这并不影响子线程的执行,此时子线程转入后台运行
- detach会导致程序员失去对线程的控制,所以join更为常用
17.2.2 其他创建线程的方法
- 用类来创建线程
{
int myi = 6;
TA ta(myi); // ta就是一个类
thread mytobj3(ta); //创建并执行子线程
mytobj3.join();
//mytobj3.detach();
cout << "main主函数执行结束!" << endl;
}
- 用lambda表达式来创建线程
lambda表达式见20.8
lambda表达式是一种可调用对象,它定义了一个匿名函数(也可以理解为可调用的代码单元或者是未命名的内联函数),并且可以捕获一定范围内的变量。
优点是:可以在函数内部定义,内联函数没有执行 函数调用 的开销,加快了程序运行时间
缺点:内联函数是将整个函数体的代码插入到调用语句处,会增大代码的大小
lambda表达式的一般形式
[捕获列表](参数列表)->返回类型{函数体;};
# 捕获列表和函数体不能省略
[]{} # 最简化的形式
其中[捕获列表]有好几种形式,决定了你是按值捕获还是按引用捕获,或者是获得当前类成员函数同样的访问权限
lambda创建线程的例子
{
auto mylamthread = [] {
cout << "我的线程开始执行了" << endl;
//...
cout << "我的线程执行完毕了" << endl;
};
thread mytobj4(mylamthread);
mytobj4.join();
cout << "main主函数执行结束!" << endl;
}
17.3 线程传参详解、detach坑与成员函数作为线程函数
- 用detach()这种方式,创建线程时,不要往线程中传递引用指针之类的参数,因为主线程结束了就会回收在主线程中分配的内存,而这段内存被其他线程使用,当主线程先退出时,其他线程使用就不安全
17.3.2 临时对象作为线程参数继续讲
为什么手工构建临时对象就安全,而用mysecondpar让系统帮我们用类型转换构造函数构造对象就不安全(对应17.3.1 2.要避免的陷阱2)
经过几次测试后最终使用的例子
void myprint2(const A &pmybuf)
// void myprint2(const A pmybuf)
{
// pmybuf.m_i = 199; //修改该值不会影响到main主函数中实参的该成员变量
cout << "子线程myprint2的参数pmybuf的地址是:" << &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(); //用join方便观察
cout << "main主函数执行结束!" << endl;
}
}
17.3.2 传递类对象与智能指针作为线程参数
为了数据安全,往线程入口函数传递类类型对象作为参数的时候,不管接收者(形参)是否用引用接收,都一概采用复制对象的方式来进行参数的传递,如果真的有需求明确告诉编译器要传递一个能够影响原始参数(实参)的引用过去,就得使用std::ref
如果将智能指针作为形参传递到线程入口函数,该怎样写代码呢:
void myprint3(unique_ptr<int> pzn)
{
return;
}
int main(){
{
unique_ptr<int> myp(new int(100));
std::thread mytobj(myprint3, std::move(myp)); // 该行执行完之后,myp指针就应该为空
mytobj.join(); // 如果用detach(),
cout << "main主函数执行结束!" << endl;
}
}
17.3.4 用成员函数作为线程入口函数
- 综上,用join()就对了,用detach()会有意向不到的麻烦
17.4 创建多个线程、共享数据问题分析与案例代码
17.4.1 创建和等待多个线程
17.4.2 数据共享问题分析
- 有读有写
17.4.3 共享数据的保护实战范例
17.5 互斥量的概念、用法、死锁演示与解决详解
互斥量,翻译成英文是mutex,互斥量其实是一个类,可以理解为一把锁,在同一时间,多个线程都可以调用lock成员函数尝试给这把锁头加锁,但是只有一个线程可以加锁成功,其他没加锁成功的线程,执行流程就会卡在lock语句行这里,不断地尝试去加锁这把锁头,一直到加锁成功,执行流程才会继续走下去
17.5.2 互斥量的用法
类mutex的两个成员函数lock和unlock
class A{
public:
//把收到的消息入到队列的线程
void inMsgRecvQueue(){
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
// std::lock_guard<std::mutex> sbguard1(my_mutex);
// std::lock_guard<std::mutex> sbguard2(my_mutex2);
my_mutex.lock(); //两行lock()代码不一定紧张挨着,可能它们要保护不同的数据共享块
//......需要保护的一些共享数据
//my_mutex2.lock();
msgRecvQueue.push_back(i);
//my_mutex2.unlock();
my_mutex.unlock();
}
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行了,从容器中取出一个元素" << command << endl;
//这里可以考虑处理数据
//......
}
else
{
cout << "outMsgRecvQueue()执行了,但目前收消息队列中是空元素" << i << endl;
}
}
cout << "end" << endl;
}
bool outMsgLULProc(int &command)
{
std::lock(my_mutex2, my_mutex); //两个顺序谁在前谁在后无所谓
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex.unlock(); //先unlock谁后unlock谁并没关系
//my_mutex2.unlock();
return true;
}
//my_mutex2.unlock();
my_mutex.unlock();
return false;
}
private:
std::list<int> msgRecvQueue; //容器(收消息队列),专门用于代表玩家给咱们发送过来的命令
std::mutex my_mutex; //创建互斥量
}
2. std::lock_guard类模板
std::lock_guard类模板可以用来取代lock和unlock,请注意,lock_guard是同时取代lock和unlock两个函数,也就是说,使用了lock_guard之后,就再也不需要使用lock和unlock了但不灵活
17.6 unique_lock详解
unique_lock是一个类模板,它的功能与lock_guard类似,但是比lock_guard更灵活,在日常的开发工作中,lock_guard够用了
17.7 单例设计模式共享数据分析、解决与call_once
17.7.2 单例设计模式
张三把这个配置文件相关的类写成了一个单例类
用c++如何写一个比较实用的单例类,这段代码也许读者在日后的工作中可以拿来商用
17.7.3 单例设计模式共享数据问题分析、解决
std::mutex resource_mutex; // 全局互斥量
class MyCAS //这是一个单例类
{
private:
MyCAS() {} //构造函数是私有的
private:
static MyCAS *m_instance;
public:
static MyCAS *GetInstance()
{
if (m_instance == NULL) // 双重锁定或者双重检查,这样就不会每次调用GetInstance()都创建一次互斥量
{
// std::unique_lock<std::mutex> mymutex(resource_mutex); //自动加锁
if (m_instance == NULL)
{
m_instance = new MyCAS();
static CGarhuishou cl; //生命周期一直到程序退出
}
}
return m_instance;
}
}
17.7.4 std::call_once
这是一个c++11引入的函数,它的功能保证函数a只被调用一次,可以使用达到之前17.7.3中双重否定写法的功能
针对单例类对象的初始化工作,强烈建议放在主线程中其他子线程创建之前进行
17.8 condition_variable、wait、notify_one与notify_all
17.8.1 条件变量std::condition_variable、wait与notify_one
条件变量用在线程中,当线程A等待一个条件满足(如等待消息队列中有要处理的数据),另外还有个线程B(专门往消息队列中扔数据),当条件满足时,线程B通知线程A,那么线程A就会从等待这个条件的地方往下继续执行。
通过一个循环不断地检测一个标记,当标记成立时,就去做一件事情,那么如何避免不断地判断消息队列是否为空,而改为当消息队列不为空的时候做一个通知,相关代码段(其他线程的代码段)得到通知后再去取数据呢
这就需要用到std::condition_variable,这是一个类(条件相关的类),用于等待一个条件达成
本节提供了一个额外的课件文件展现了一个商业质量的线程池代码(比较完善)见文件17.8
17.9 async、future、packaged_task与promise
17.9.1 std::async和std::future创建后台任务并返回值
以往的多线程编程中,用std::thread创建线程,用join来等待线程
现在有一个需求,希望线程返回一个结果。当然,可以把线程执行结果赋给一个全局变量,这是一种从线程返回结果的方法,但是更好的方法:
std::async是一个函数模板,通常的说法是启动一个异步任务,启动起来这个异步任务后,它会返回一个std::future对象(也是一个类模板)
future的中文含义是:将来,std::future提供了一个异步操作结果的机制,就是说这个结果可能没办法马上拿到,但不久的将来等线程执行完了就可以拿到。future会保存一个值在将来某个时刻能够拿到。
3.std::async和std::thread的区别
创建线程一般用std::thread,如果资源紧张可能失败,这个时候想到std::async,它可能创建线程也可能不创建而且独有优势是可以在未来通过future获得异步任务返回的值
17.9.3 std::promise
这个类模板的作用是,能过在某个线程中为其赋值,然后就可以在其他的线程中,通过std::promise对象实现了两个线程之间的数据传递
17.10.4 原子操作
17.5介绍互斥量,互斥量在多线程编程时保护共享数据,形象地说就是用一把锁把共享数据锁住,操作完了这个共享数据后在把这个锁打开这就是互斥量的应用。
除了用互斥量加锁的方式来解决对g_mycount进行自加(++)操作时的临界问题保证g_mycount在自加的时候不被打断,还有没有其他的方法达到这个效果,就是我们讲的原子操作,g_mycout++就不会被打断(效率比加锁的方式高)
互斥量可以达到原子操作的效果,所以把原子操作理解成一种不需要用到互斥量加锁(无锁)技术的多线程并发编程方式,原子操作的效率更胜一筹,不然用互斥量就行了,谁还会用原子操作呢?
互斥量的加锁一般是针对一个代码段(几行代码),而原子操作针对的是一个变量而不是代码段
原子操作示例
std::atomic<int> g_mycout = 0; //这是个原子整型类型变量;可以向使用整型变量一样使用
std::atomic<bool> g_ifend = false; //线程退出标记,用原子操作,防止读和写混乱
void mythread()
{
for (int i = 0; i < 10000000; i++) //1千万
{
//g_my_mutex.lock();
//g_mycout++; //对应的操作就是原子操作,不会被打断
//g_mycout+=1; //对应的操作就是原子操作,不会被打断
g_mycout = g_mycout + 1; //这样写就不是原子操作了
//g_my_mutex.unlock();
}
return;
}
在笔者的实际工作中,这种原子操作适用的场合有限,一般常用于做计数(数据统计)之类的工作,例如累积发出去了多少个数据包,累计接收到了多少个数据包等,试想多个线程用来计数,如果没有原子操作,那么跟上面一样统计的数据就会出现混乱的情形如果用了原子操作那么统计结果的数据就能够保持正确
17.12 补充知识、线程池浅谈、数量谈与总结
17.12.1 知识点补充
- 虚假唤醒
虚假唤醒就是wait代码被唤醒了,但是不排除msgRecvQueue(消息队列)里面没有数据的情形。wait的第二个参数(lambda表达式)特别重要,通过里面的if判断语句来应付虚假唤醒。 - atomic原子操作进一步理解
17.12.2 浅谈线程池
开发者提出了线程池,池表示把一堆线程放到一起进行统一的管理调度,发挥一下想象力,就是把多个线程放到一个池子里,用的时候随手抓一个线程拿来用,用完了再把这个线程扔回到池子里,供下一次使用。《c++新经典: Linux C++通信架构实战》书籍中详细讲解线程池