目录
1. 了解线程概念,理解线程与进程区别与联系。
线程概念
铺设阶段:
一般教材:
线程:是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细和轻量化
一个进程内存在多个线程?可能
进程:线程 = 1:n
OS内存在更多的线程?是的!
OS要不要管理线程?要!
OS如何管理线程?先描述,在组织
线程也应该要有线程控制块TCB
struct tcb{};
这是常规OS做法,比如:windows
cpu此时看到的pcb<=之前的pcb的概念
一个pcb就是一个需要被调度的执行流!
结论:
1.linux中没有专门为线程设计TCB,而是用进程的PCB来模拟线程
好处:不用维护复杂的进程和线程的关系,不用单独为线程设计任何算法,直接使用进程的一套相关的方法。OS只需要聚焦在线程间的资源分配上就可以了!
一般教材:
线程:是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细和轻量化
内部:线程在进程的地址空间内运行!
执行分支(执行流):CPU调度的时候只看PCB,每一个PCB曾经被指派过执行方法和数据,CPU可以直接的调度
属于进程的一部分:
上述绿框内所有东西加起来才是进程
进程:
今天的进程vs之前的进程
之前的进程:内部只有一个执行流的进程
今天的进程:内部可以具有多个执行流
创建进程的“成本”非常高,成本:时间+空间
创建进程要使用的资源是非常多的(0-1)
内核视角:进程是承担分配系统资源的基本实体!!!
线程:是CPU调度的基本单位,承担进程资源的一部分的基本实体,
进程划分资源给线程
Linux线程与接口关系的认识
Linux PCB<= 传统意义上的进程PCB
1.OS创建“线程”
2.CPU调度
Linux进程,称为:轻量级进程
(较大概率比别的进程轻)
Linux因为是用进程模拟的,所以Linux下一般不会给我们提供直接操作线程的接口,而是给我们提供,在同一个地址空间内创建PCB的方法,分配资源给指定的PCB的接口
结论:linux本身不会提供线程操作的接口!
对用户特别不友好!
用户需要:
创建线程的接口,
释放线程
等待线程
or。。。。。。
Linux将“用户需要”交给了:系统级别的工程师,在用户层对Linux轻量级进程接口进行封装,给我们打包成库,让用户直接使用库接口,原生线程库(在用户层)
原生:距离操作系统最近的一套库
线程和进程的共享与私有
所有的轻量级进程(可能是“线程”)都是在进程的内部运行(地址空间:标识进程所能看到的大部分资源!)
进程,独立性,可以有部分共享资源(管道,IPC资源)
线程,大部分资源是共享的,也可以有部分资源是“私有”(pcb,栈,上下文)
如何验证?
(代码输出有点问题,还缺少输出第一行(截取时问题),但是整体思路是没有问题的,只需要关注输出的内容和 另一个窗口是需要对应的即可)
说明,此时依旧是只有一个进程,但是进程内部一定具有两个执行流
LWP轻量级进程
Linux OS调度,看的是LWP
如何理解我们之前单独一个进程的情况?
之前的 PID=LWP
线程的优缺点?
优点:
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
计算密集型:加密,大数据运算等-》主要使用的是CPU资源
I/O密集型:网络下载,云盘,ssh,在线直播,看电影,,,-》内存和外设的IO资源
大部分应用是:CPU+IO密集型这样的应用:网络游戏,
问题:
对于计算密集型线程越多越好?不一定!
如果线程太多,会导致线程间被过度调度切换(也是有成本的),
对于I/O密集型,线程是不是越多越好?不一定!
不过,IO允许多一些线程
为了提高性能,将I/O操作重叠。(IO大部分时间在等待IO就绪的)(将等待时间进行重叠)
缺点:
1.性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2.健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3.缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4.编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
2. 学会线程控制,线程创建,线程终止,线程等待。
1.线程创建:
1.1创建单线程
1.2创建多线程:
六个线程,一个主线程,五个新线程
1.3线程的健壮性是有问题的:
一个线程出现崩溃问题,整个进程就会跟着出现崩溃
2.线程等待
一般而言,线程也是需要被等待的,如果不等待,可能会导致类似于“僵尸进程”的问题!
输出型参数,用来获取新线程退出的时候,函数的返回值
pthread_join内部实现了:找到返回值x 类型为void*
*retval=x;
新线程的创建需要十秒,主线程必须阻塞式等待,直到新线程退出,才能pthread_join获取成功拿到对应返回值
(可以根据这个status 来判断新线程将任务进行的如何)
2.1延伸问题:
问题一:
程序运行结束会出现三种情况:
1.代码跑完结果对
2.代码跑完结果不对
3.代码异常了
12可以通过返回值来判断
3pthread_join能或者需要处理吗?根本不需要
线程出现异常了,这不是线程的任务,而是进程的任务
线程出现崩溃,主线程也会跟着崩溃,即使你pthread_join也没有意义了
问题二:
线程的返回值一定是整数吗?
也可以是结构体等其他类型的东西
不要认为这里的返回值只是int,也可以是其他变量,对象的地址(不能是临时的)
返回的是一个指针,一旦返回的内容超过 8个字节,就需要将其封装成为一个结构体,然后再返回
延伸问题三:
当你创建多个线程时,该如何等待?
for循环,一个一个的等
不能同时等
3.线程终止
线程终止的方案:
3.1函数中return,
A.main函数退出return的时候代表(主线程and进程退出)
B.其他线程函数return,只代表当前线程退出,
3.2新线程通过pthread_exit()终止自己
3.2.1pthread_exit()
3.2.2vs exit
exit是终止进程,不要在其他线程中调用,如果你就想终止一个进程的话!
可以看到主进程接收到了返回值,但是直接退出了,退出码使这个进程的退出码>
3.pthread_cancel 取消目标线程
可以看到我们取消线程的时候,退出码是-1
3.1新线程取消主线程
主线程不会退出,而是会变成“僵尸进程”
linux没有线程概念,所以这里叫做“僵尸进程”
取消只是仅仅让主进程的线程退出了,而不是进程退出了
此时主进程也是不存在的,此时主进程的状态就是 “僵尸进程”
一般推荐前两种
延伸问题:
如果一个线程是被合法取消的?退出结果是-1
-1是多少呢?
4.线程等待
一个线程被设置分离之后,绝对不能再进行join!!!
主线程不退出,新线程处理业务处理完毕在退出!
5.延伸:
红色框内是:内核LWP
绿色框内是:我们查看到的线程id是pthread库的线程id,不是Linux内核中的LWP,pthread库的线程id是一个内存地址!
问题:这个内存地址是虚拟地址还是物理地址?
虚拟地址!!!
(我们能看到的地址都是虚拟地址)
我们编写的程序叫做线程函数,依赖的是libpthread-2.17.so库
问题:
这个库是文件吗?是的,他就是磁盘上的一个文件!
每个线程都要有运行时的临时数据,每个线程都要有自己的私有栈结构!
描述线程的用户级控制块
线程崩溃的影响一定时有限的-》在进程内部-》进程具有独立性!
总结:
绿色框内是:
用户层id属于库当中在,地址空间当中映射之后的地址,能够快速帮我们找到用户级线程库的属性,他的属性和OS的LWP是一对一的关系!
3. 学会线程同步。
引入:
线程的大部分资源是共享的,包括我们定义的全局变量,全局变量是可以被多个线程同时访问的,包括线程内大部分资源都是共享的,我们有可能会出现一个问题:
因为多个线程是共享地址空间的,也就是很多资源都是共享的,
共享带来的优点:通信方便
缺点:缺乏访问控制,
因为一个线程的操作问题,给其他线程造成了不可控或者引起崩溃,异常,逻辑不正确等,这种现象我们称之为线程安全!
我们在上面的实验中有个是:
创建五个线程,然后这五个线程同时访问 pthread_run 这个函数,
输出的时候会出现问题:输出没有按照顺序进行输出,
叫做这个函数被重入了,所以在多线程情况下,
函数被重入是很常见的
所以这个问题不算是问题
创建一个函数没有线程安全问题的话,不要使用全局,stl,malloc,new等会在全局内有效的数据,
如果要使用:需要有访问控制,
因为我们全部都是局部变量!线程有自己独立栈结构!
为什么需要进行后续的访问控制:
互斥,同步!
感性认识!
1.临界资源:凡是被线程共享访问的资源都是临界资源(多线程,多进程打印数据到显示器)
2.临界区:我的代码中访问临界资源的代码(在我的代码中,不是所有的代码都是访问临界资源的, 而访问临界资源的代码区域我们称之为临界区)
3.对临界区进行保护的功能,本质:就是对临界资源的保护,方式:互斥或者同步,
4.互斥:在任意时刻,只允许一个执行流访问某段代码(访问某部分资源),就可以称之为互斥!
5.例如: printf(“hello sakeww”) -> lock();printf();unlock();-> 一个事情,要么不执行,要么就执行完毕,原子性。
6.同步:一般而言,让访问临界资源的过程在安全的前提下(一般都是互斥 和 原子的),让访问资源具有一定的顺序性!
同步让访问资源具有合理性!
理性认识:
正常现象,打印到0就会结束
但是这个会直接打印出负数,
抢票逻辑,1000票,5个线程同时抢
tickets就是临界资源!
tickets-- tickets>0是原子的吗?(是安全的吗?)
为了让多个线程进行切换,线程什么时候可能切换?
1.时间片到了。2.检测的时间点:从内核态返回用户态的时候,
只有19到25是访问临界资源!这部分叫做临界区
如何解决这个问题?
对临界去进行对应的加锁
加锁:
这样就可以解决票数到0,还会继续下去减票数的bug
(不同编译器会出现不同结果,加锁和不加锁也看不到访问到负数的情况)
输出现象中有时候会出现一页都是一个线程id,这是因为时间片的原因
一个时间片内可能抢了很多票
解决:将票数增加,就可以看到线程id 的变化
延伸:
pthread_mutex_t mtx; //原生线程库,系统级别的
std::mutex mymtx;//C++语言级别的
我要访问临界资源tickets的时候,需要先访问mtx,前提是所有线程必须得先看到它!
那么锁本身,是不是也是临界资源?
你如何保证锁本身是安全的?
原理:lock,unlock->原子的!
一行代码是原子的:只有一行汇编的情况
问题:
我申请成功的时候,我在临界区是否会改变al?
完全有可能的!
线程被切走的时候,要做什么?
上下文保护!而锁数据也是在上下文中!
拥有锁,被切走的线程,是抱着锁走的!!!
在此期间,其他线程休想申请锁成功,休想进入临界区!
站在其他线程的视角,是不是对其他线程有意义的状态,是不是A线程要么没有申请,要么线程A使用完锁----》线程A的访问临界区的原子性!
代码是程序员写的!为了保证临界区的安全,必须保证每个线程都必须遵守相同的“编码规范”(A 申请锁,其他线程的代码也必须要申请)
死锁问题:
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
产生死锁的四个必要条件:(一旦产生死锁,必定会出现下面所有条件)
互斥条件:一个资源每次只能被一个执行流使用(锁的根本特征,一般我们不管)
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放( )
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
(避免死锁,也就是破坏这四个条件中的一个)
避免死锁:
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
避免死锁算法
死锁检测算法(了解)
银行家算法(了解)
4. 条件变量:
一般而言,只有锁的情况,我们起始比较困难的知道,临界资源的状态!
1.当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
2.例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
可以看到线程谁先跑起来是不确定的,但是是按照一定顺序的
如果想确定,可以在创建线程的时候,停顿一秒,,,
问题:
二号线程跑完了,然后去干什么了?
一号呢?零号呢?
条件变量内部一定存在一个叫做等待队列!
延伸:
上述代码中第十三行的问题:
pthread_cond_signal 唤醒的是在cond等待队列里等待的第一个线程
pthread_cond_broadcast :唤醒所有线程
5. 生产者消费者模型
函数调用:
好处:
1.提高效率
2.将生产环节和消费环节进行了“解耦”
三种关系:
供货商和供货商:竞争,互斥
消费者和消费者:竞争,互斥
供货商和消费者:互斥,同步
两种角色:生产者n : 消费者n
超市-》交易场所-》一段缓冲区(内存空间,stl容器等)-》1个交易场所
基于BlockingQueue(阻塞队列)的生产者消费者模型
简易生产者消费者模型代码
修改:将if改为while
BlockQueue,hpp
#pragma once
#include<iostream>
#include<queue>
#include<pthread.h>
namespace ns_blockqueue
{
const int default_cap = 5;
template <class T>
class BlockQueue
{
private:
std::queue<T> bq_;//阻塞队列
int cap_;//队列的元素上限
pthread_mutex_t mtx_;//保护临界资源的锁
//1.当生产满了的时候,就应该不要生产了(不要竞争锁了),而应该让消费者来消费
//2.当消费空了,就不应该消费了(不要竞争锁了),而应该让生产者来进行生产
pthread_cond_t is_full_;//bq_满的,消费者在该条件变量下等待
pthread_cond_t is_empty_;//bq_空的,生产者在该条件变量下等待
private:
bool Isfull(){
return bq_.size()==cap_;
}
bool IsEmpty(){
return bq_.size()==0;
}
void LockQueue()
{
pthread_mutex_lock(&mtx_);
}
void UnLockQueue()
{
pthread_mutex_unlock(&mtx_);
}
void ProducterWait()
{
//pthread_cond_wait
//1.调用的时候,会首先自动释放mtx_,然后再挂起自己
//2.返回的时候,会首先自动竞争锁,获取到锁之后,才能返回!
pthread_cond_wait(&is_empty_,&mtx_);
}
void ConsumerWait()
{
pthread_cond_wait(&is_full_,&mtx_);
}
void WakeComsumer()
{
pthread_cond_signal(&is_full_);
}
void WakeupProducter()
{
pthread_cond_signal(&is_empty_);
}
public:
BlockQueue(int cap = default_cap):cap_(cap)
{
pthread_mutex_init(&mtx_,nullptr);
pthread_cond_init(&is_empty_,nullptr);
pthread_cond_init(&is_full_,nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&is_empty_);
pthread_cond_destroy(&is_full_);
}
public:
//const &:输入
//*:输出
//&:输入输出
void Push(const T& in)
{
LockQueue();
while(Isfull()){
ProducterWait();
}
//向队列中放数据,生产函数
bq_.push(in);
if(bq_.size()>cap_/2) WakeComsumer();
UnLockQueue();
}
void Pop(T* out )
{
LockQueue();
//从队列中拿数据,消费函数
while(IsEmpty()){
//无法消费
ConsumerWait();
}
*out = bq_.front();
bq_.pop();
if(bq_.size() < cap_/2) WakeupProducter();
UnLockQueue();
}
};
}
//.hpp-》开源软件使用-》声明和实现可以放在一个文件里
#include"BlockQueue.hpp"
#include<time.h>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
using namespace ns_blockqueue;
void *consumer(void *args)
{
BlockQueue<int> *bq=(BlockQueue<int>*)args;
while(true){
sleep(2);
int data=0;
bq->Pop(&data);
std::cout<<"消费者消费了一个数据:"<<data<<std::endl;
}
}
void *producter(void* args)
{
BlockQueue<int> *bq=(BlockQueue<int>*)args;
while(true){
//sleep(2);
//1.制造数据
int data = rand()%20+1;
std::cout<<"生产者生产数据:"<<data<<std::endl;
bq->Push(data);
}
}
int main()
{
srand((long long)time(nullptr));
BlockQueue<int> *bq = new BlockQueue<int>();
pthread_t c,p;
pthread_create(&c,nullptr,consumer,(void*)bq);
pthread_create(&p,nullptr,producter,(void*)bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
Makefile
CpTest:CpTest.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf CpTest
延伸问题:
什么是挂起失败?
可以理解为函数调用失败,不能将自己放进等待队列,
什么是被伪唤醒?
条件可能没有满足,但是我这个线程被唤醒了,
举个栗子:
单核的不会出现这个问题,
如果是多核的,会向目标条件变量发送,条件就绪,从而被导致,,,
延伸:
生产和消费,传输数据只是第一步
1.数据怎么来的??耗时吗?
2.数据怎么处理??耗时吗?
加点任务的pc代码
BlockQueue.hpp不需要修改,
CpTest.cc
#include"BlockQueue.hpp"
#include"Task.hpp"
#include<time.h>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
using namespace ns_blockqueue;
using namespace ns_task;
void *consumer(void *args)
{
BlockQueue<Task> *bq=(BlockQueue<Task>*)args;
while(true){
Task t;
bq->Pop(&t);
t.Run();
}
}
void *producter(void* args)
{
BlockQueue<Task> *bq=(BlockQueue<Task>*)args;
std::string ops="+-*/%";
while(true){
int x = rand()%20+1;
int y = rand()%10+1;
char op = ops[rand()%5];
Task t(x,y,op);
bq->Push(t);
}
}
int main()
{
srand((long long)time(nullptr));
BlockQueue<Task> *bq = new BlockQueue<Task>();
pthread_t c,p;
pthread_create(&c,nullptr,consumer,(void*)bq);
pthread_create(&p,nullptr,producter,(void*)bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
Task.hpp
#pragma once
#include<iostream>
#include<pthread.h>
namespace ns_task{
class Task
{
private:
int x_;
int y_;
char op_;//+-*/%
public:
Task(){}
Task(int x,int y,char op):x_(x),y_(y),op_(op)
{}
void Run()
{
int res=0;
switch(op_){
case '+':
res=x_+y_;
break;
case '-':
res=x_-y_;
break;
case '*':
res=x_*y_;
break;
case '/':
res=x_/y_;
break;
case '%':
res=x_%y_;
break;
default:
std::cout<<"bug?"<<std::endl;
break;
}
std::cout<<"当前任务正在被"<<pthread_self()<<"处理:"\
<<x_<<op_<<y_<<"="<<res<<std::endl;
}
~Task(){}
};
}
延伸问题:
这个代码支持多生产者,多消费者吗?
支持
因为每次添加和去掉数据的时候,都加了锁,保证了
生产者和消费者的 竞争和互斥
6. 信号量
1.概念
a.信号量本质就是一把计数器,描述临界资源中资源数目的大小!(最多能有多少资源分配给线程)
b.买票的本质:预定资源。临界资源如果可以被划分成为一个一个的小资源,如果处理得当,我们也有可能让多个线程同时访问临界资源的不同区域,从而实现并发。
–》多线程预定资源的手段
每个线程想访问临界资源,都得先申请信号量资源
一定会有你的小块资源的!
2.信号量对应的操作函数
3.环形队列
1.环形队列什么时候为空,什么时候为满?
a.计数器
b.镂空一个位置
使用数组通过模运算来模拟环形结构!
多线程情况,来进行环形队列的并发访问!
实现一个基于环形队列的生产消费者模型
一、基本原理:
1.生产者和消费者开始的时候,指向的就是同一个位置!(队列为空的时候(应该让生产者执行))
2.生产者和消费者在队列满(应该让消费者执行)的时候,也指向同一个位置!(1.2. 不能让生产和消费同时进行(互斥特性+同步特性))
3.那么,当队列不为空,不为满的时候,生产者和消费者一定指向的不是同一个位置!!!(生产和消费可以并发执行!!!)
二、基本实现思想:
生产者,最关心什么资源?环形队列中空的位置
消费者,最关心什么资源?环形队列中的数据
规则1:生产者不能把消费者套一个圈
规则2:消费者不能超过生产者
规则3:当指向同一个位置的时候,要根据空,满的状态,来判断让谁先执行
其他:除此之外,生产和消费可以并发执行
4.结合sem+环形队列编写pc消费模型
单PC模型
ring_queue.hpp
#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>//信号量
namespace ns_ring_queue{
const int g_cap_default=10;//Global 全局的
template<class T>
class RingQueue{
private:
std::vector<T> ring_queue_;
int cap_;
//生产者关心的空位置资源
sem_t blank_sem_;
//消费者关心的空位置资源
sem_t data_sem_;
//生产和消费的写入位置
int c_step_;
int p_step_;
public:
RingQueue(int cap= g_cap_default)
:ring_queue_(cap),cap_(cap){
sem_init(&blank_sem_,0,cap);
sem_init(&data_sem_,0,0);
c_step_=p_step_=0;
}
~RingQueue(){
sem_destroy(&blank_sem_);
sem_destroy(&data_sem_);
}
public:
//目前高优先级的先实现单生产和单消费
void Push(const T& in){
int index=0;
//生产接口
sem_wait(&blank_sem_);//P(空位置)
ring_queue_[p_step_]=in;
sem_post(&data_sem_);//V(数据)
p_step_++;
p_step_%=cap_;
}
void Pop(T* out){
//消费接口
sem_wait(&data_sem_);//P
*out=ring_queue_[c_step_];
sem_post(&blank_sem_);//V
c_step_++;
c_step_%=cap_;
}
};
}
ring_cp.cc
#include "ring_queue.hpp"
#include<pthread.h>
#include<time.h>
#include<unistd.h>
using namespace ns_ring_queue;
void* consumer(void* args)
{
RingQueue<int> * rq =(RingQueue<int>*)args;
while(true){
int data = 0;
rq->Pop(&data);
std::cout<<"消费的数据是:"<<data<<std::endl;
sleep(1);
}
}
void* producter(void* args)
{
RingQueue<int> * rq =(RingQueue<int>*)args;
while(true){
int data = rand()%20+1;
std::cout<<"生产的数据是:"<<data<<std::endl;
rq->Push(data);
}
}
int main()
{
srand((long long)time(nullptr));
RingQueue<int> * rq = new RingQueue<int>();
pthread_t c,p;
pthread_create(&c,nullptr,consumer,(void*)rq);
pthread_create(&p,nullptr,producter,(void*)rq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
Makefile
ring_cp:ring_cp.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f ring_cp
多PC模型
多PC情况下
0.多个生产者先申请锁,然后申请信号量,
a.感觉有点多余,因为只要消费者申请到信号量的时候,那么此时的资源绝对是可以满足生产者的
b.当一个生产者申请锁后,其他生产者就不能进入了,相比较于1,有点浪费时间
c.当生产者生产满的时候,你拿到了锁,此时需要等待消费者消费完后,你才可以继续生产,这个阶段的生产者身上状态(生产者申请锁成功,根据信号量得知没有空间了,然后挂起等待,,,消费者消费产品后,消费者唤起生产者,此时生产者继续生产,,,)(缺点:浪费时间)
1.多个生产者申请信号量,然后申请锁
a.生产者的状态:
多个生产者先申请信号量,(只要生产者能够申请到信号量,那么这个生产者是一定可以生产的),然后按照顺序(先来后到)申请锁,当申请到锁的时候,然后开始生产,,,,,,当没有资源可以生产的时候,此时的生产者是申请不了信号量的,(相比较于0,能更好点(节约时间))。
多生产和多消费的优势:
并发的获取和处理任务
ring_queue.hpp
#pragma once
#include<iostream>
#include<vector>
#include<pthread.h>
#include<semaphore.h>//信号量
namespace ns_ring_queue{
const int g_cap_default=10;//Global 全局的
template<class T>
class RingQueue{
private:
std::vector<T> ring_queue_;
int cap_;
//生产者关心的空位置资源
sem_t blank_sem_;
//消费者关心的空位置资源
sem_t data_sem_;
//生产和消费的写入位置
int c_step_;
int p_step_;
pthread_mutex_t c_mtx_;
pthread_mutex_t p_mtx_;
public:
RingQueue(int cap= g_cap_default)
:ring_queue_(cap),cap_(cap){
sem_init(&blank_sem_,0,cap);
sem_init(&data_sem_,0,0);
c_step_=p_step_=0;
pthread_mutex_init(&c_mtx_,nullptr);
pthread_mutex_init(&p_mtx_,nullptr);
}
~RingQueue(){
sem_destroy(&blank_sem_);
sem_destroy(&data_sem_);
pthread_mutex_destroy(&c_mtx_);
pthread_mutex_destroy(&p_mtx_);
}
public:
//多PC
void Push(const T& in){
int index=0;
//生产接口
sem_wait(&blank_sem_);//P(空位置)
pthread_mutex_lock(&p_mtx_);
ring_queue_[p_step_]=in;
p_step_++;//多PC情况下,是临界资源
p_step_%=cap_;
pthread_mutex_unlock(&p_mtx_);
sem_post(&data_sem_);//V(数据)
}
void Pop(T* out){
//消费接口
sem_wait(&data_sem_);//P
pthread_mutex_lock(&c_mtx_);
*out=ring_queue_[c_step_];
c_step_++;
c_step_%=cap_;
pthread_mutex_unlock(&c_mtx_);
sem_post(&blank_sem_);//V
}
};
}
ring_cp.cc
#include "ring_queue.hpp"
#include<pthread.h>
#include<time.h>
#include<unistd.h>
#include"Task.hpp"
using namespace ns_ring_queue;
using namespace ns_task;
void* consumer(void* args)
{
RingQueue<Task> * rq =(RingQueue<Task>*)args;
while(true){
Task t;
rq->Pop(&t);
t.Run();
sleep(1);
}
}
void* producter(void* args)
{
RingQueue<Task> * rq =(RingQueue<Task>*)args;
const std::string ops="+-*/";
while(true){
int x = rand()%20+1;
int y = rand()%10+1;
char op=ops[rand()%ops.size()];
Task t(x,y,op);
std::cout<<"生产的数据是:"<<t.Show()<<"我是:"<<pthread_self()<< std::endl;
rq->Push(t);
}
}
int main()
{
srand((long long)time(nullptr));
RingQueue<Task> * rq = new RingQueue<Task>();
pthread_t c0,c1,c2,c3,p0,p1,p2;
pthread_create(&c0,nullptr,consumer,(void*)rq);
pthread_create(&c1,nullptr,consumer,(void*)rq);
pthread_create(&c2,nullptr,consumer,(void*)rq);
pthread_create(&c3,nullptr,consumer,(void*)rq);
pthread_create(&p0,nullptr,producter,(void*)rq);
pthread_create(&p1,nullptr,producter,(void*)rq);
pthread_create(&p2,nullptr,producter,(void*)rq);
pthread_join(c0,nullptr);
pthread_join(c1,nullptr);
pthread_join(c2,nullptr);
pthread_join(c3,nullptr);
pthread_join(p0,nullptr);
pthread_join(p1,nullptr);
pthread_join(p2,nullptr);
return 0;
}
Task.hpp
#pragma once
#include<iostream>
#include<pthread.h>
namespace ns_task{
class Task
{
private:
int x_;
int y_;
char op_;//+-*/%
public:
Task(){}
Task(int x,int y,char op):x_(x),y_(y),op_(op)
{}
std::string Show(){
std::string message=std::to_string(x_);
message+=op_;
message+=std::to_string(y_);
message+="=?";
return message;
}
void Run()
{
int res=0;
switch(op_){
case '+':
res=x_+y_;
break;
case '-':
res=x_-y_;
break;
case '*':
res=x_*y_;
break;
case '/':
res=x_/y_;
break;
case '%':
res=x_%y_;
break;
default:
std::cout<<"bug?"<<std::endl;
break;
}
std::cout<<"当前任务正在被"<<pthread_self()<<"处理:"\
<<x_<<op_<<y_<<"="<<res<<std::endl;
}
~Task(){}
};
}
7.线程池
提前准备好的线程,用来随时处理任务,就称之为线程池!
在类中要让线程执行类内成员方法,是不可行的!
因为在我们使用线程接口的时候,我们需要传递一个参数,但是原本人家就有一个this,从而导致传递了两个参数,在语法上就会直接报错,
解决:
必须让线程执行静态方法!
代码
thread_pool.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
namespace ns_threadpool
{
const int g_num = 5;
template <class T>
class ThreadPool
{
private:
int num_;
std::queue<T> task_queue_; //该成员是一个临界资源
pthread_mutex_t mtx_;
pthread_cond_t cond_;
public:
void Lock()
{
pthread_mutex_lock(&mtx_);
}
void Unlock()
{
pthread_mutex_unlock(&mtx_);
}
void Wait()
{
pthread_cond_wait(&cond_, &mtx_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
bool IsEmpey()
{
return task_queue_.empty();
}
public:
ThreadPool(int num = g_num) : num_(num)
{
pthread_mutex_init(&mtx_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
// 在类中要让线程执行类内成员方法,是不可行的!
// 必须让线程执行静态方法
static void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpey())
{
tp->Wait();
}
//此时任务队列中一定有任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < num_; i++)
{
pthread_create(&tid, nullptr, Rountine, (void *)this /*?*/);
}
}
void PushTask(const T &in)
{
Lock();
task_queue_.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out = task_queue_.front();
task_queue_.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&cond_);
}
};
}
main.cc
#include "thread_pool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
using namespace ns_threadpool;
using namespace ns_task;
int main()
{
ThreadPool<Task> *tp = new ThreadPool<Task>(3);
tp->InitThreadPool();
srand((long long)time(nullptr));
while(true)
{
Task t(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);
tp->PushTask(t);
sleep(1);
}
return 0;
}
Task.cc
#pragma once
#include <iostream>
#include <pthread.h>
namespace ns_task
{
class Task
{
private:
int x_;
int y_;
char op_; //+/*/%
public:
// void (*callback)();
Task() {}
Task(int x, int y, char op) : x_(x), y_(y), op_(op)
{
}
std::string Show()
{
std::string message = std::to_string(x_);
message += op_;
message += std::to_string(y_);
message += "=?";
return message;
}
int Run()
{
int res = 0;
switch (op_)
{
case '+':
res = x_ + y_;
break;
case '-':
res = x_ - y_;
break;
case '*':
res = x_ * y_;
break;
case '/':
res = x_ / y_;
break;
case '%':
res = x_ % y_;
break;
default:
std::cout << "bug??" << std::endl;
break;
}
std::cout << "当前任务正在被: " << pthread_self() << " 处理: " \
<< x_ << op_ << y_ << "=" << res << std::endl;
return res;
}
int operator()()
{
return Run();
}
~Task() {}
};
}
Makefile
thread_pool:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f thread_pool
8.线程安全的单例模式
单例模式的特点:
某些类, 只应该具有一个对象(实例), 就称之为单例.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.
懒汉模拟的单例模式:
thread_pool.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
namespace ns_threadpool
{
const int g_num = 5;
template <class T>
class ThreadPool
{
private:
int num_;
std::queue<T> task_queue_; //该成员是一个临界资源
pthread_mutex_t mtx_;
pthread_cond_t cond_;
static ThreadPool<T> *ins;
//构造函数必须要实现,但是必须需要私有化
//将构造函数放进私有,这个就不能定义对象
ThreadPool(int num = g_num) : num_(num)
{
pthread_mutex_init(&mtx_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
//不让这个线程池进行拷贝构造或者赋值,不让他进行写实拷贝
ThreadPool(const ThreadPool<T> &tp) = delete;
//赋值语句
ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;
public:
static ThreadPool<T> *GetInstance()
{
//这里会被多个线程同时进入,所以需要加锁
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
if (ins == nullptr)//双判断,减少锁的争用,提高获取单例的效率!
{
pthread_mutex_lock(&lock);
//当前单例对象还没有被创建
if (ins == nullptr)
{
ins = new ThreadPool<T>();
ins->InitThreadPool();
std::cout << "首次加载对象" << std::endl;
}
pthread_mutex_unlock(&lock);
}
return ins;
}
void Lock()
{
pthread_mutex_lock(&mtx_);
}
void Unlock()
{
pthread_mutex_unlock(&mtx_);
}
void Wait()
{
pthread_cond_wait(&cond_, &mtx_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
bool IsEmpey()
{
return task_queue_.empty();
}
public:
static void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpey())
{
tp->Wait();
}
//此时任务队列中一定有任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < num_; i++)
{
pthread_create(&tid, nullptr, Rountine, (void *)this /*?*/);
}
}
void PushTask(const T &in)
{
Lock();
task_queue_.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out = task_queue_.front();
task_queue_.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&cond_);
}
};
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
Task.hpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace ns_task
{
class Task
{
private:
int x_;
int y_;
char op_; //+/*/%
public:
// void (*callback)();
Task() {}
Task(int x, int y, char op) : x_(x), y_(y), op_(op)
{
}
std::string Show()
{
std::string message = std::to_string(x_);
message += op_;
message += std::to_string(y_);
message += "=?";
return message;
}
int Run()
{
int res = 0;
switch (op_)
{
case '+':
res = x_ + y_;
break;
case '-':
res = x_ - y_;
break;
case '*':
res = x_ * y_;
break;
case '/':
res = x_ / y_;
break;
case '%':
res = x_ % y_;
break;
default:
std::cout << "bug??" << std::endl;
break;
}
std::cout << "当前任务正在被: " << pthread_self() << " 处理: " \
<< x_ << op_ << y_ << "=" << res << std::endl;
return res;
}
int operator()()
{
return Run();
}
~Task() {}
};
}
Makefile
thread_pool:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f thread_pool
main.cc
#include "thread_pool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
using namespace ns_threadpool;
using namespace ns_task;
int main()
{
std::cout<<"其他任务"<<std::endl;
std::cout<<"其他任务"<<std::endl;
std::cout<<"其他任务"<<std::endl;
std::cout<<"其他任务"<<std::endl;
std::cout<<"其他任务"<<std::endl;
std::cout<<"其他任务"<<std::endl;
sleep(5);
srand((long long)time(nullptr));
while(true)
{
Task t(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);
ThreadPool<Task>::GetInstance()->PushTask(t);
std::cout<<ThreadPool<Task>::GetInstance()<<std::endl;
sleep(1);
}
return 0;
}
9. 理解基于读写锁的读者写者问题。
读者写者模型应用场景
- 对数据,大部分的操作是读取,少量的操作是写入,
- 判断依据:进行数据读取或者消费的一端,是否会将数据取走,如果不取走,就可以考虑读写模型
读者写者模型原则
3种关系:
读者和读者:没有关系
写者和写者:互斥
读者和写者:互斥,同步
区别:
生产者和消费者 vs 读者和写者
读者不会取走资源
消费者会拿走数据!
2种角色:
读者和写者
由线程承担
1个交易场所:
提供一段缓冲区(自己申请的,或者STL)
读者写者模型基本操作:
关于读者写者模型的优先级:
读者优先:让读者和写者同时到来的时候,我们让读者先进入访问
写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有读者,都不要进入临界区访问了,等临界区没有读者的时候,让写者先写入。
10.挂起等待特性的锁(悲观锁) vs 自旋锁
线程访问临界资源花费的时间问题?
如果花费的时间非常短:自旋锁(不断的通过循环检测锁的状态)
如果花费的时间非常长:等待锁
线程该如何得知,自己会在临界资源中待多长时间?
线程不知道,我们自己定