es网站开发,做国学类网站合法吗,企业邮箱是啥意思,上海比较有名的公司目录 线程概念
1.什么是线程#xff1f; 2.线程的优缺点
3.线程异常
4.线程用途
线程操作
1.如何给线程传参
2.线程终止
3.获取返回值
4.分离状态
5.退出线程
线程的用户级地址空间#xff1a;
线程的局部存储
线程的同步与互斥
互斥量mutex
数据不一致的主要过…
目录 线程概念
1.什么是线程 2.线程的优缺点
3.线程异常
4.线程用途
线程操作
1.如何给线程传参
2.线程终止
3.获取返回值
4.分离状态
5.退出线程
线程的用户级地址空间
线程的局部存储
线程的同步与互斥
互斥量mutex
数据不一致的主要过程
互斥锁
临界区
线程同步
原子性操作
锁的应用---封装
线程安全
死锁
Linux线程同步
先认识一下与等待有关的接口
条件变量
条件变量的初始化
等待条件满足条件变量
唤醒等待
cp问题生产者消费者模型
代码实现生产者消费者模型 线程概念
1.什么是线程
课本的概念线程是比进程更加轻量化的一种执行流/线程是进程内部的一种执行流。
我们的概念线程是cpu调度的基本单位/进程是系统资源承载的实体
我们知道线程的创建过程非常的复杂构建pcb构建地址空间构建页表构建运行队列初始化各个字段......而我们的线程可以理解为进程创建之后 创建pcb参与分配资源。因此线程的创建比进程更加简单。其次线程实在进程的地址空间内运行。
而线程在进程的地址空间也需要被管理因此和进程一样线程的管理需要先描述出他的所有属性在组织出它的数据结构但这样再次设计调度算法数据结构太过麻烦而刚好进程与线程其实都是执行流因此我们直接复用进程的一套东西。
实际上我们在真正去创建使用线程时与教材上的点可能不太一样linux下具体的实现方案教材也不会谈到而是以抽象的概念介绍。
因此在现在cpu拿到一个pcb它的执行力度时小于等于进程的。在Linux中其实并不会真正的找到线程而是使用进程的数据结构模拟的线程因此这些除了整个进程pcb构成的数据进程被称为轻量级进程线程。
我们先以一段代码看看
在linux操作系统下在c中调用的头文件是pthread.h创建线程的接口pthread_create(): #includeiostream
#includeunistd.h
using namespace std;
//c创建线程需要的头文件、
#includepthread.hvoid *start(void *arg)
{const char*name(char*)arg;coutnameendl;while(true){couti am a new thread,my name is nameendl;sleep(1);}}
int main()
{pthread_t tid;//创建线程的接口int n10;pthread_create(tid,nullptr,start, (void*)thread 1);while(true){couti am main thread,i am runningendl;sleep(1);}return 0;}
可以看到在编译运行时在进入到main时就已经创建了进程了在进程中我们有创建了一个线程用来执行一段代码。这也就侧面说明了线程是进程中的一种执行流。
此时我们在主函数和线程调用的函数同时打印pid可以看到是同一个进程..
那么现成的调度应该看神呢呢进程的调度我们看pid那么线程呢?
使用ps -aL 指令查看进程时我们会发现还有一个LWP,而线程的调度就是看LWP, 如果更想清晰的观察我们还可以创建多个线程都来执行这个代码在ps -aL查看旧就能清楚看到pid与LWP.
线程更新为什么比进程快呢
线程到线程切换本质就是pcb-pcb数据切换,地址空间页表等不变而pcb-pcb之间也是寄存器产生的临时数据的切换不需要切换所有的寄存器只需要局部的寄存器的数据的切换。
其次cpu上除了集成了寄存器还有其他比如一块较大的缓存cahce通过缓存由于局部性原理的预加载机制我们的要切换之前的数据可能就被放到了缓存中此时的cpu继续访问内存是就直接存缓存中读取数据。
总结1.使用的寄存器少 2.不需要从新更新缓存。
对于操作系统无论是物理内存文件在磁盘上的加载都是以4kb的大小呈现以4kb为基本大小对数据加载物理内存中以4kb为一个页框而整个物理内存中有很多页框这些页框有没有被使用即需要管理这些页框-先描述在组织在内核中会为其维护一个数据结构strcut page不是很大主要看flag。
对与虚拟地址一般为32位在使用虚拟地址是被分为三部分前十位用作页目录的映射中间十位用做二级页表映射的就是物理内存的页框也就是前20位用来找页框最后用页框的起始地址加上最低的十二位页内偏移来找到我们的物理内存。
根据页框目录找到页框再从页框里的呢容找到叶匡的起始地址再根据起始地址加上12位低地址找到物理内存的地址。整个转化过程在cpu上集成的MMU上已经转化完成了。
所以划分页表的本质就是划分虚拟地址。 2.线程的优缺点
优点
1.创建调度释放的量级更轻
2.可利用多处理器的可并行数量可以被多个执行
3.计算密集型应用可以分解到多个线程运行。
4.占用资源少
缺点 性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密型 线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的 同步和调度开销而可用的资源不变。线程的数量一般有cpu的个数以及大小决定 健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。 缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。线程的资源可能因为共享而造成许多问题。 3.线程异常 单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃 线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该 进程内的所有线程也就随即退出. 4.线程用途 合理的使用多线程能提高CPU密集型程序的执行效率 合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是 多线程运行的一种表现 进程是资源分配的基本单位 线程是调度的基本单位 线程共享进程数据但也拥有自己的一部分数据: 线程ID 一组寄存器 栈 errno 信号屏蔽字 调度优先级 最主要的两个是线程的上下文结构与线程栈。 线程操作
首先需要注意的是在Linux系统下没有真正的线程只是进程模拟出来的轻量级进程那么我们在使用的时候也没有线程的一些列接口操作有的只是进程。因此在应用层提供了一套线程的原生库pthread,该库中提供了线程一系列的操作方法。
这也是Linux系统设计这的特殊设计。因此在编译时我们需要指明我们的库pthread。
1.如何给线程传参
pthread_create创建线程 对于参数第一个为线程id本质上就是一个长整型第二参数我们一般为空第三个为线程执行的接口第四个为接口的参数参数会被接口回调。
以创建多进程为例
using functfunctionvoid();
class ThreadData{
public:
ThreadData(const string Name,const uint64_t Time,funct f ):name(Name),time(Time),fc(f)
{}
string name;
uint64_t time;
funct fc;
};
void *ThreadRotine(void *args)
{ThreadData *datastatic_castThreadData*(args);cout获取到线程数据,threadname:data-name,threadtime:data-time,调用接口:endl;data-fc();sleep(1);}void func()
{couti am a part of actionendl;sleep(1);
}
#define N 5
int main()
{vectorpthread_t pthreads;pthread_t tid;//创建多个线程for(int i0;iN;i){char threadname[20];snprintf(threadname,sizeof(threadname),%s-%d,thread,i1);ThreadData *tnew ThreadData(threadname,uint64_t(nullptr),func);pthread_create(tid,nullptr,ThreadRotine,t);sleep(2);}//创建完成之后保存tid,pthreads.push_back(tid);while(true){coutmain thread running....endl;sleep(1);}}
此时运行代码我们可以看到正常运行 但是我们说过编写线程代码我们需要小心仔细线程的鲁棒性较低例如这里我们让其中一个线程出现除零错误 此时所有的线程就全崩掉了。
2.线程终止
由上述我们基本了解到了线程的使用那么初次之外如何终止线程呢方法也恒简单
1.直接在线程接口给出返回值例如返回一个空指针。
2.exit直接终止但是不仅仅是线程进程也会退出
3.因此线程库也有提供的接口pthread_exit(),用于终止线程。
3.获取返回值
线程退出默认要被等待如果线程退出没被等待子也可能会产生与进程一样的僵尸问题其次线程退出时主线程如何获取新线程的退出信息。
pthread_join 用于等待子线程终止 返回值为整形退出码参数一为tid参数二为一个二级指针用来接收线程接口执行完的的返回值但我们看到线程函数的返回值为void*而这里是void**, string Tohex(pthread_t id){char tid[64];snprintf(tid,sizeof(tid),0x%x,id);//以十六进程写return tid;}
void *ThreadRotine(void *args)
{string namestatic_castconst char*(args);int cnt5;while(cnt--){cout获取到线程数据,threadname:name,线程地址Tohex(pthread_self())endl; sleep(1);}//return nullptr;pthread_exit(args); //两个效果一样return args;
}int main()
{vectorpthread_t pthreads;pthread_t tid;pthread_create(tid,nullptr,ThreadRotine,(void*)thread 1);coutmain thread running....,线程地址Tohex(pthread_self())endl;sleep(5);//参与线程终止 ,以阻塞状态一直等待void*retnullptr;int npthread_join(tid,ret);coutget new thread data:(char*)retendl;coutmain thread done exit code:nendl;sleep(5);return 0;
}
可以看到我们设置ret指针,通过接口pthread_join我们可以拿到线程执行这里就是threadrotine的返回值,之后将该返回值给给我们的ret并且完成线程等待这也是为什么参数二是一个输出型参数。
那么如果线程异常呢返回拿到的是什么实际上异常时就会返回-1;
其次返回值的类型为void*,即任意类型的数据都可以传参返回给我们。
4.分离状态
线程模式默认是joinable状态但是 线程可以被设置分离状态如果有一个线程此时我们不管不顾也没有等待但我们希望退出时他的资源都会被回收此时就可以把该线程设置为分离状态。
线程” 分离“不” 分家“虽然主线程不管分离的线程分离状态的线程退出运行结果都不知道但是分离状态的线程出现了问题还是会影响到主线程的。 一般我们这样设置
pthread_detach(pthread_self());
此时pthread_join去等待时等待失败返回值为22不需要去等待.
5.退出线程
当子线程一直在运行时作为主线程我们想去结束掉子线程此时就可以调用接口pthread_cancel: 参数为线程的id。
总结 调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的 终止状态是不同的总结如下: 1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。 2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。 3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。 4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数 以上的所有接口都是在库中提供的线程的所有东西也是在库中被维护的。 线程的用户级地址空间 由于线程整个是一个库而库的使用是加载到内存当中那么加载到内存中的那一部分呢由于该库是对线程做管理即就是对用户级内存的管理加载到共享区堆栈之间。 当然我们当前学的都是系统级别的线程实际上现在主流语言都支持多线程这是在语言层面上的那系统层面与语言层面的线程一样吗需要再去学习吗 以c为例编写线程代码
void running()
{int cnt5;while(cnt--){couti am a thread,i am running endl;sleep(1);}
}
int main()
{//定义线程对象thread t(running);//等待线程t.join();while(true){couti am main threadendl;sleep(1);}return 0;
} 在编译的时候每有问题可是到了链接还是报错此时还需要我们在编译时 -lpthread,指明库。 实际上这些语言支持的多线程都是用的原生的线程库故此还需要指明库文件。之后运行与系统的多线程一摸一样。 线程的局部存储 其次当我们对全局变量加上__thread修饰此时每个线程用该变量都是独一个全局变量。__thread只能在内置类型上使用。 注意的点 线程可以fork创建子进程其次也可以进行进程替换但是会影响自身及其他线程。 对于用户级执行流与内核级的lwp是1:1的。此外tid是系统内核级别的用户及时接触不到的。 其中较为重要的就是线程栈线程栈决定了一个线程本身执行流。每一个线程都有自己的线程栈虽然如此但线程与线程之间是可见的虽然独立但不私有我们可以获取线程栈中的数据升值修改。对于全局变量所有的线程都可以看到且地址一样。 线程的局部存储 在线程中还提供了一个编译选项__thread,在定义变量时我们可以用__thread修饰修饰之后定义的全局变量每一个线程都是单独的一份了相互不影响。需要注意的是只能对内置类型使用。 线程的同步与互斥 首先我们先来了解一下相关的几个概念进程线程间的互斥相关背景概念 临界资源多线程执行流共享的资源就叫做临界资源 临界区每个线程内部访问临界资源的代码就叫做临界区 互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用 原子性后面讨论如何实现不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成。 互斥量mutex 原因大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况变量归属单个 线程其他线程无法获得这种变量。 但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之 间的交互。 多个线程并发的操作共享变量会带来一些问题。 以一个简单的抢票代码为例
int tickets 100;
struct threadData
{threadData(int id){namethread_to_string(id);}string name;
};void *getticks(void *args)
{threadData * datastatic_castthreadData* (args);while(true){if(tickets0){usleep(1000);coutdata-nameget a tiket:ticketsendl;tickets--;}else{couttickets is nullendl;break;}}coutget tickets quit,name:data-nameendl;return nullptr;}int main()
{//一个线程代表一个用户vectorpthread_t tids;vectorthreadData * thread_datas;for(int i0;iNum;i){//创建线程pthread_t id;threadData *tmpnew threadData(i);pthread_create(id,nullptr,getticks,tmp);tids.push_back(id);thread_datas.push_back(tmp);}for( int i0;itids.size();i){pthread_join(tids[i],nullptr);}for( int i0;itids.size();i){delete thread_datas[i];thread_datas[i]nullptr;}return 0;
} 四个用户同时进行抢票抢100张票此时我们在看看执行结果 我们很清楚的看出一些问题 首先就是抢相同的票其次命名判断tickets0旧停止还会有抢到负数的。 此时对于这段代码tickets就是全局数据也是线程共享数据我们的四个进程并发的去访问这些数据。 对于上述的问题tickets最后变为负数这种现象被称为由于共享数据而产生的数据不一致问题即此时我在进行数据操作时你也在做数据操作。所以对于ticktes-- /这种操作是不安全的。 数据不一致的主要过程 cpu先从内存读取到数据ickets,之后在cpu中做--操作之后在写回内存。每一步都有对应的汇编操作线程在执行时将共享数据加载到内存当中把数据内容通过拷贝的方式变成了自己的上下文此时别的线程也来干这种操作线程2先成功的把数据改完并返回给cpu但是此时线程2辛运分给他的时间片也多于是线程2执行了多次之后结束了带着结果走了此时又回到了第一个线程线程一此时并不是立马继续先前的工作而是先恢复上下文数据此时的数据还是之前保存的数据再进行cpu的操作此时线程1还认为该数据没有变实际上线程2已经执行多次了于是就出现了数据不一致的问题。 既然出现了这种问题那么我们如何来解决呢 互斥锁 了解互斥锁就先了解一下源生库中提供的关于互斥锁的接口 对于类型pthread_mutext_t 时库中提供的一种类型。 除了这种方式外也可以直接定义一把全局的锁之后就不需要初始化不需要释放。定义方式就是上图中的最后一行。 由接口名也能看到分别有销毁和初始化通过该接口我们创建锁。之后便是“上锁”需要另一个接口pthread_mutex_lock pthread_mutex_lock 就是加锁pthread_mutex_unlock就是解锁。 临界区 之前已经说过我们的共享资源线程同时访问会出现数据不一致这份资源我们也叫临界资源对于临界资源我们是需要加锁的因此我们也只需要对之访问了临界资源的那一小部分的临界区加锁。加锁的原则也是代码越小区域越小更好。 加锁之后就会限制每次只有一个线程访问串型访问。 加锁之后的抢票系统 #define Num 4
//利用多线程模拟一轮抢票int tickets 100;
struct threadData
{threadData(int id, pthread_mutex_t * tmp){namethread_to_string(id);Locktmp;}string name;//添加锁pthread_mutex_t* Lock;};void *getticks(void *args)
{threadData * datastatic_castthreadData* (args);while(true){//加锁pthread_mutex_lock(data-Lock);//线程到锁这里如果申请锁成功就执行否则就阻塞等待if(tickets0){usleep(1000); coutdata-name get a tiket make : ticketsendl;tickets--;//解锁pthread_mutex_unlock(data-Lock);}else{couttickets is nullendl;//解锁pthread_mutex_unlock(data-Lock);break;}//合适的sleep使得 线程不会一释放完就去拿锁 而是其他线程去拿锁usleep(15);}coutget tickets quit,name:data-nameendl;return nullptr;}int main()
{//创建锁pthread_mutex_t lock;pthread_mutex_init(lock,nullptr);//一个线程代表一个用户vectorpthread_t tids;vectorthreadData * thread_datas;for(int i0;iNum;i){//创建线程pthread_t id;threadData *tmpnew threadData(i,lock);pthread_create(id,nullptr,getticks,tmp);tids.push_back(id);thread_datas.push_back(tmp);}//当不用的时候销毁锁pthread_mutex_destroy(lock);for( int i0;itids.size();i){pthread_join(tids[i],nullptr);}for( int i0;itids.size();i){delete thread_datas[i];thread_datas[i]nullptr;}return 0;
} 修改代码之后此时我们在运行首先看到确实不会抢到负数了但是此时票只有一个进程在抢。但当我们在线程抢完票之后休眠个十几毫秒此时就能看到多个进程都在抢这是因为如果不休眠某个线程拿到了锁之后执行完代码释放锁之后立马再去申请锁导致其他线程拿不到锁 一次你会看到只有一个线程抢票。而且在现实中也不可靠我们不会抢完一张票非常快速再去抢一张。 线程同步 所谓的线程同步就是让所有的线程按顺序平均的申请锁获取资源。例如上述抢票如果我们设置一个观察员首先让没有拿到锁的线程在外面排队让拿到锁的线程获取资源之后释放锁然后排到队尾以这种方式来竞争资源更加的有顺序性。 原子性操作 锁本身就是共享资源申请锁和释放锁本身就被设计成原子性操作。那么如何做到的呢 在临界区中时线程可以被切换不过把锁带走了即使我不在了也没有其它线程会去访问资源。对于线程要么就没锁要么就释放锁因此对于当前线程访问临界区时对于其他线程是原子的。 对与每一条汇编语句 我们认为他也是原子。 上锁与解锁的本质 实际上加锁也并不是好的这是利用时间来保证安全。 锁的应用---封装 在c中有一种利用对象生命周期能更好的管理资源的思想--RAII直接在代码的临界区上锁解锁没问题但书写起来看起来都不太好此时我们可以将需要调用的接口都封装在一个类中通过构造与析构调用对应的上锁解锁。此时我们再创建一个全局的锁此时代码就更加轻便。 class Mutex{public:Mutex(pthread_mutex_t* lock):_lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t* _lock;
};class LockGuard
{public:LockGuard(pthread_mutex_t* lock):_lockguard(lock){_lockguard.Lock();}~LockGuard(){_lockguard.Unlock();}private:Mutex _lockguard;
}; //代码更加简便
void *getticks(void *args)
{threadData * datastatic_castthreadData* (args);while(true){//用一个花括号表示临界区{LockGuard lockguard(lock);//创建完成时自动调用构造与析构完成 上锁与解锁//创建在临界区 在该区域进行线程上锁if(tickets0){usleep(1000); coutdata-name get a tiket make : ticketsendl;tickets--;}else{break;}}//合适的sleep使得 线程不会一释放完就去拿锁 而是其他线程去拿锁usleep(15);}coutget tickets quit,name:data-nameendl;return nullptr;} 线程安全 首先来看看两个概念 线程安全多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作 并且没有锁保护的情况下会出现该问题。 重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们 称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重 入函数否则是不可重入函数 死锁 死锁指的是一组进程中各个进程占用不会释放的资源但因互相申请被其他进程给占用不会释放的资源而出一种永久等待的状态。
死锁的四个必要条件
1.互斥条件每一个资源每次只有一个执行流使用(前提)
2.请求与保持条件一个执行流因请求资源而阻塞时不释放已占有的资源。
3.不剥夺条件一个执行流已经获得资源时在未使用完成前不能被剥夺。
4.循环等待关系若干执行流形成了一种头尾相接的循环等待资源的关系。 死锁问题的避免
想要避免死锁就要破坏四个必要条件
1.互斥条件一般不可避免
2.请求与不保持条件一个执行流因请求资源而阻塞时释放已占有的资源。pthread_mutex_trylock方式请求锁失败直接返回不再等待避免锁未被释放。
3.剥夺条件在未使用完资源前可以进行剥夺。资源一次性分配。
4.不形成环路按顺序申请锁不交叉的申请锁。 Linux线程同步
先认识一下与等待有关的接口
条件变量 当一个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。 例如一个线程访问队列时发现队列为空它只能等待只到其它线程将一个节点添加到队列中。这种情 况就需要用到条件变量。 同步概念与竞态条件 同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问 题叫做同步竞态条件因为时序问题而导致程序异常我们称之为竞态条件。在 条件变量的初始化 pthread_cond_init 接口是就用来初始化条件变量的。
销毁pthread_cond_detrory。 也可以定义全局变量接口的使用与线程锁非常类似。
等待条件满足条件变量 线程会先去队列等待而不是获取资源。
唤醒等待 唤醒在指定条件变量下的线程从等待队列中。 sgnal是唤醒一个线程broadcast是唤醒所有线程。
了解了以上接口我们用代码了解一下 uint64_t cnt0;
using namespace std;
pthread_mutex_t mutexPTHREAD_MUTEX_INITIALIZER;pthread_cond_t condPTHREAD_COND_INITIALIZER;void *Count(void *args)
{//分离线程 pthread_detach(pthread_self());uint64_t count(uint64_t)args;coutpthread create succeed: pthread_countendl;while(true){{pthread_mutex_lock(mutex);//无论是哪个线程进来时先要去队列等待 sleeppthread_cond_wait(cond,mutex);coutpthread name :pthread_countis running,:cnt:cntendl;sleep(1);pthread_mutex_unlock(mutex);}}}
int main()
{for(uint64_t i0;i5;i){//创建线程pthread_t id;pthread_create(id,nullptr,Count,(void*)i);sleep(1);}sleep(2);couti am main thread ,control beginendl;while(true){//唤醒条件变量下等待的线程,默认一般都是队头的线程pthread_cond_signal(cond);sleep(2);coutpthread is signal sucessfullyendl;}
}
首先有一个问题就是为什么 先上锁在进行等待
实际上pthread_cond_wait接口有一个参数就是传锁进来当使用该接口等待时会自动释放锁。等待之后我们就需要在主线程再次唤醒她之后就可以去获取资源了。当然我们当前的资源并不是一个临界资源如果是临界资源我们就要去判断
首先判断资源是否就绪是否还有资源临界资源也是有状态的就是要访问临界资源也就是加锁之后。 因此决定是否休眠等待是在加锁与解锁之间。
cp问题生产者消费者模型
生产者消费者模型在我们的日常生活也是常见的生产者 供货商消费者顾客超市缓存因为有超市的存在使得生产者与消费者没有直接关系且使得生产与消费解耦通过这种方式资源能得到更好的处理。 在开发过程中使用生产消费模型更有优点 1.解耦 2.支持并发 3.支持忙闲不均 超市或者仓库对于消费者和生产者都是共享资源而共享临界资源都会存并发问题
1.生产者与生产者存在竞争要保证互斥。
2.生产者与消费者 首先要保证互斥关系其次还要保证同步。
3.消费者与消费者互斥关系。
因此生产与消费模型存在3个关系 2个角色1个场所特定结构的内存空间。
代码实现生产者消费者模型 基于 BlockingQueue 的生产者消费者模型 在多线程编程中阻塞队列 (Blocking Queue) 是一种常用于实现生产者和消费者模型的数据结构。 其与普通的队列区别在于当队列为空时从队列获取元素的操作将会被阻塞直到队列中被放入了元素当队列满时往队列里存放元素的操作也会被阻塞直到有元素被从队列中取出( 以上的操作都是基于不同的线程来说的线程在对阻塞队列进程操作时会被阻塞)。 BlockQueue.hpp templateclass Tclass BlockQueue
{static const int defaltnum100;public:BlockQueue(int maxcapacitydefaltnum):max_capacity(maxcapacity){min_capacity0;pthread_mutex_init(_mutex,nullptr);pthread_cond_init(c_cond,nullptr);pthread_cond_init(p_cond,nullptr);low_watermax_capacity/2;high_watermax_capacity*2/3;}T pop(){//对数据操作需要加锁pthread_mutex_lock(_mutex);//判断是否进行等待if(_q.size()min_capacity){pthread_cond_wait(c_cond,_mutex);}T temp_q.front();_q.pop();//消费者消费了所以一定保证有空余让生产者生产我们可以等待消费max/2之后生产if(low_water_q.size())pthread_cond_signal((p_cond));pthread_mutex_unlock((_mutex));return temp;}void push(const T data){//对数据操作需要加锁pthread_mutex_lock(_mutex);//之后就需要判断等待if(_q.size()max_capacity){//进行等待释放锁进入阻塞pthread_cond_wait(p_cond,_mutex);}_q.push(data);//生产者保证一定有数据所以可以唤醒消费者来消费,我们可以等待生产到capacity的2/3之后一会在消费if(high_water_q.size())pthread_cond_signal((c_cond));pthread_mutex_unlock((_mutex));}~BlockQueue(){pthread_mutex_destroy(_mutex);pthread_cond_destroy(c_cond);pthread_cond_destroy(p_cond);}private:std::queueT _q; //共享资源//最大值T max_capacity;//最小值T min_capacity;//需要对共享资源加锁pthread_mutex_t _mutex;//生产消费都是按顺序的需要实现同步提供条件变量pthread_cond_t c_cond;pthread_cond_t p_cond;//生产与消费的水位线int low_water;int high_water;}; blockqueue.cc #includepthread.h
#includeunistd.h
#includeiostream
#includeBlockQueue.hppusing namespace std;//生产者
void *Producer(void *args)
{BlockQueueint * qstatic_castBlockQueueint* (args);//生产int data0;while(true){data;q-push(data);cout生产一个数据dataendl;sleep(2);}}//消费者
void *Consumer(void *args)
{//消费BlockQueueint * qstatic_castBlockQueueint* (args);while(true){ int dataq-pop(); cout已消费一个数据 :dataendl;}
}int main()
{pthread_t prod;pthread_t cons;//线程创建的顺序一定是先消费者再生产者 否则生产者一生产消费者就会消费BlockQueueint *queuenew BlockQueueint();pthread_create(cons,nullptr,Consumer,queue);pthread_create(prod,nullptr,Producer,queue);//等待子线程 pthread_join(cons,nullptr);pthread_join(prod,nullptr);delete queue;return 0;
} 结果