网站信息登记表扫描件,学平面设计网上哪个培训好,网站的建设方法包括哪些内容,哈尔滨做网站价格一、背景知识
1、地址空间进一步理解
在父子进程对同一变量进行修改时发生写时拷贝#xff0c;这时候拷贝的基本单位是4KB#xff0c;会将该变量所在的页框全拷贝一份#xff0c;这是因为修改该变量很有可能会修改其周围的变量#xff08;局部性原理#xff09;#xf… 一、背景知识
1、地址空间进一步理解
在父子进程对同一变量进行修改时发生写时拷贝这时候拷贝的基本单位是4KB会将该变量所在的页框全拷贝一份这是因为修改该变量很有可能会修改其周围的变量局部性原理这是一种以空间换时间的做法malloc和new其实对申请内存做了封装申请的也是4KB的整数倍。
OS如何管理页框先描述再组织。描述用一个结构体struct page管理采用数组struct page memory[n] 等数据结构。一个4GB的内存有10485761MB计算方式4*1024*1024*1024 / (4*1024)个页框,那么管理的数组有1048576个元素每一个page有了下标这里的下标*4KB就是对应的每个page的下标。 2、页表的深入理解
如果页表是真的如下图所示左边是虚拟地址右边是物理地址那么每一个条目就要有8个字节加上表示位置的一个字节就是9个字节一个进程的虚拟地址空间一般是4GB那么一张页表就有4GB*936GB大小这明显是不合适的。
物理地址一定是在物理内存中的某个页的。只要找到该物理地址在该页内的偏移量就可以找到该物理地址对应的那个字节。
对于一个物理地址其实就是一个32位的二进制数字。真实的页表是将这32位数字划分为101012三类。 3、虚拟地址的本质
函数是有地址的函数的地址一般指函数入口的地址而函数这一段代码块是一段连续的地址。函数的本质就是一段连续的代码地址。
虚拟地址是一种资源。
二、线程的概念与代码实现
1、概念
线程是进程内部运行的CPU调度的基本单位。
线程是在进程的PCB中运行的一个进程中可能有多个PCB对应在LINUX中就是多个task_struct
Linux中的线程 复用pcb用pcb统一表示执行流这样就不需要为线程单独设计数据结构和调度算法了。
windows中的线程对线程先描述再组织有一个struct tcb结构体内部有线程ID、优先级、状态、上下文、连接属性等属性。每一个线程都要与进程相连接。最后设计成的示意图如下 Linux系统实现线程的方式是更优的维护成本低更简单也就更不容易出错。
Windows中是真的有进程而Linux中叫轻量级进程。
2、代码实现
创建线程的函数 #include iostream
#include pthread.h
#include unistd.husing namespace std;void *newthread_process(void *str)
{while (1){sleep(1);coutnew thread processing... pid:getpid()endl;}
}int main()
{pthread_t mythread1;int thread_ret pthread_create(mythread1, nullptr, newthread_process, (void *)new thread);//主线程while(1){coutmain thread processing... pid:getpid()endl;sleep(1);}return 0;
}
.PHONY: all
all:test_threadtest_thread : test_thread.cppg -o $ $^ -stdc11 -lpthread.PHONY: clean
clean: rm -f test_thread
在编译时需要带入pthread动态库即-lpthread ps-aL查看轻量级进程(ps axj查看进程)其中LWP表示的就是轻量级进程的ID 3、关于线程的几个问题
1已经有多进程为何要有多线程
① 进程创建成本非常高要创建PCB、地址空间、页表并建立映射等创建线程只需要创建PCB然后将资源分配给该PCB。
② 运行时线程相对于进程切换时不需要切换地址空间和页表。线程调度成本低。
线程调度成本为何低
实际页表对应一个寄存器仅仅是这个寄存器的切换不会有太大的效率影响。
CPU内部有一个硬件cache,根据局部性原理存储热数据如果是进程间切换cache中的热数据需要切换而线程切换的cache热数据无需变化。
③ 删除时线程也成本低。
2那为何要有进程
线程也有劣势。在一个进程中的多个线程当一个线程出现野指针、除0等类似的错误时整个进程都要崩溃回收其他线程也就崩溃了线程的健壮性比较差。一个线程出异常可能会改变其他线程的数据这样其他线程的正确性无法保证所以全部崩溃。
3不同的OS实现线程的方式不一样
Windows实现线程是先描述再组织Linux则是通过复用代码。尽管实现方式不一样但都满足线程的统一定义进程内部运行的CPU调度的基本单位。只是具体的如何实现在进程内部如何实现其是CPU调度的基本单位是不一样的。线程的原理都是正确的。
4线程的页表划分
每一个线程有自己的代码对应在页表中不同的区域。也就是说对于同一张页表不同的线程对应着页表上不同的区域。这样就是线程对页表的划分。
5OS与进程的关系
OS其实就是一个进程。虚拟机的原理就是这样虚拟机中一个OS挂掉了不会影响其他OS。
6如何分几个线程
对于计算密集型应用对于一个单核CPU分再多的线程效率只会变低因为线程切换需要成本。最好分的线程个数与CPU核数一样合适。
对于I/O密集型应用可以多创建几个线程因为IO操作的大部分时间都在等待多分几个线程这样等待的时间重叠。
4、代码验证线程的健壮性较低
代码
#include iostream
#include pthread.h
#include unistd.husing namespace std;void *newthread_process(void *str)
{while (1){int xrand()%5;coutnew thread processing... pid:getpid()endl;sleep(1);if(x0){int *pnullptr;*p100;}}
}int main()
{ srand(time(nullptr));pthread_t mythread1,mythread2,mythread3;int thread_ret1 pthread_create(mythread1, nullptr, newthread_process, (void *)new thread);int thread_ret2 pthread_create(mythread2, nullptr, newthread_process, (void *)new thread);int thread_ret3 pthread_create(mythread3, nullptr, newthread_process, (void *)new thread);//主线程while(1){sleep(1);coutmain thread processing... pid:getpid()endl;}return 0;
}
验证结果当一个进程出现报错所有进程都退出。 不仅健壮性较低由于多线程共享地址空间的大部分资源所以其缺乏访问控制。
5、进程与线程对比 线程独有的数据比较重要的是一组寄存器和栈。
寄存器硬件中的上下文数据反映了线程可以动态运行的
栈:每个线程都要有自己独立的栈结构因为函数执行要是独立的。线程在运行的时候会形成各种临时变量临时变量会被每个线程保存在自己的栈区。 三、线程控制
在编译以上代码时加上了-lpthread。由于linux中没有线程只有轻量级进程所以系统调用的接口只会给用户提供创建轻量级进程的接口而我们在写代码时直接使用的是线程的相关接口这是通过pthread动态库实现的。 1、pthread_create函数详解 第一个参数是输出型参数其实际是Linux对 unsigned long int 类型的一个封装是一个地址第二个参数是要修改的线程的属性可以直接设置为nullptr第三个参数是一个返回值为void*且参数也为void*的函数指针第四个参数是回调的函数参数传给第三个参数作为函数参数。
返回值为0表示正确创建了线程否则返回错误码thread中的内容将会是未定义。 2、代码验证
1等待线程的函数 第一个参数即为pthread_create函数中的第一个参数表示要等待哪一个线程第二个参数是得到pthread_create函数中的第三个参数的函数最终的返回值.
2代码
①两个线程的代码
#include iostream
#include pthread.h
#include unistd.husing namespace std;class ThreadData
{
public:int x;int y;string name;int Add(){return xy;}private:};
class ThreadResult
{
public:int x;int y;int result;private:};void* threadRun(void* args)
{/*int cnt5;while(cnt){coutnew thread run,cnt:cnt--endl;sleep(1);}*/auto tdstatic_castThreadData*(args);// 安全强转//coutid: td-id name:td-nameendl;ThreadResult* retnew ThreadResult();ret-xtd-x;ret-ytd-y;ret-resulttd-Add();delete td;return (void*)ret;}string PrintToHex(pthread_t tid)
{char cache[64];snprintf(cache,sizeof(cache),0x%lx,tid);return cache;
}int main()
{pthread_t tid;ThreadData* tdnew ThreadData();//推荐在堆空间上开辟td-x10;td-y20;td-namethread-1;int npthread_create(tid,nullptr,threadRun,(void*)td);if(n!0){cerrcreate threadendl;return 1;}string tid_hexPrintToHex(tid);couttid的16进制形式为:tid_hexendl;//验证tid//join等待coutpthread join begin...endl;ThreadResult* retnullptr;pthread_join(tid,(void**)ret);//会阻塞到这等待 类似于wait//如果不join当主线程退出所有的线程都退出了因为进程都退出了 因为当main线程不退出而new线程退出时不join会造成类似僵尸线程的问题coutret:ret-resultendl;coutpthread join successendl;return 0;
}
②多线程代码
#include iostream
#include pthread.h
#include unistd.h
#include vectorusing namespace std;string PrintToHex(pthread_t tid)
{char cache[64];snprintf(cache,sizeof(cache),0x%lx,tid);return cache;
}void *ThreadRun(void *args)
{string name static_castchar *(args);while (1){cout new thread running,name: name endl;sleep(1);//break;}//pthread_exit(args);//等价于return args//return args;
}int main()
{int num 10;vectorpthread_t tids;for (int i 0; i num; i){pthread_t tid;/*char name[64];*///这样写是在栈空间上开辟会有问题——线程覆盖问题char* namenew char[64];//这样在堆空间上开辟sprintf(name, thread-%d, i 1);pthread_create(tid, nullptr, ThreadRun, (void *)name);tids.emplace_back(tid);}//join等待sleep(5);for(auto tid:tids){pthread_cancel(tid);void* retnullptr;pthread_join(tid,(void**)ret);coutPrintToHex(tid) quit ... ret:(long long int)retendl;// delete ret;}//sleep(100);return 0;
}
3几个问题
①main线程和new线程谁先运行是不确定的。
②我们期望main线程是最后退出如何做到
main线程需要对new线程进行回收所以我们期望main线程最后退出。做到这一点是通过join函数做到的。如果new线程退出了main线程还没退出且main线程没有join那么此时new线程会进入类似僵尸进程的状态。
③tid到底是什么
虚拟地址可以以16进制的形式打印出来方便观察。
打印tid的代码
#include iostream
#include pthread.h
#include unistd.h
#include vector
#includestringusing namespace std;string PrintToHex(pthread_t tid)
{char cache[64];snprintf(cache,sizeof(cache),0x%lx,tid);return cache;
}void* ThreadRun(void* args)
{string namestatic_castconst char*(args);while(1){sleep(1);pthread_t tidpthread_self();coutnew thread running...name:name tid:PrintToHex(tid)endl;}}int main()
{pthread_t tid;pthread_create(tid,nullptr,ThreadRun,(void*)thread-1);coutnew thread tid:PrintToHex(tid)endl;pthread_join(tid,nullptr);return 0;
}
运行结果 而查到的LWP 也就是说给用户提供的线程ID不是内核中的LWP而是pthread维护的一个唯一值。库内部也要承担对线程的管理。首先便是对线程ID的赋值
要理解线程ID是一个地址首先理解pthread库。pthread库本质是一个文件进程未开启时是在磁盘上的。
多线程在创建之前首先是一个进程最开始加载时磁盘上的可执行程序加载到内存然后建立pcb、虚拟地址空间、页表然后页表在内存中作映射这时候一个进程被创建好然后才有多线程。
当进程创建好之后要创建新线程调用pthread_create方法这是就需要将pthread库加载到内存中。同时要将被加载的库映射到地址空间的堆栈之间的共享区。此时就可以创建新线程了。这是加载的pthread库叫做共享库。原因是当有新进程创建时不需要再从磁盘中加载pthread库了而只需要在新进程的共享区建立与内存中 pthread库的映射。这和以前的动态库加载是一样的。 总结线程ID就是线程属性集合的起始虚拟地址其是在pthread库中维护的。
④全面看待对线程运行函数传参
给线程运行函数传参是穿一个void*即一个地址我们可以通过这个地址传整数、字符串、甚至类对象地址。传递一个类对象那么可以给线程传递多个参数、方法。
如果直接在主线程定义一个结构体那么这是在主线程的栈上开辟的空间存储该变量这与线程要有自己的独立栈空间矛盾了更重要的是如果有多个新线程其中某个新线程对该栈空间上的变量做修改那么就会导致全部都会变化。
所以一般采用new的写法在堆空间上开辟空间。
⑤线程运行函数的返回值
pthread_join函数的第二个参数是输出型参数用于获取线程运行函数的返回值。返回值void* ret是指针变量意味着其是有空间来接收返回值的。
与进程退出的区别是进程异常退出时会有退出信号但线程异常退出时意味着整个进程退出信号是发给进程的其余线程也就退出了。所以线程退出时只关注正确退出的情况。
线程返回一个void*即一个地址我们可以通过这个地址传整数、字符串、甚至类对象地址。传递一个类对象那么可以让线程返回多个参数、方法。
⑥如何创建多线程
见2②多线程代码
⑦新线程如何终止
a、线程函数return
b、exit() 不可以其表示的是进程终止return表示函数退出只有main函数的return表示进程退出exit则是进程退出
c、pthread_exit()专门用来终止一个线程是新线程自己主动调用
d、pthread_cancel取消一个线程 一般都用主线程取消一个新线程新线程退出结果是-1 注main线程结束表示进程结束所以要尽量保证主线程最后终止。
⑧如何不join线程而是让其结束后直接退出
使用线程分离。一个线程被创建默认是必须要被 join的。如果一个线程被分离那么该线程的工作状态为分离状态不需要也不能被join但被分离的进程依旧属于进程内部。 可以新线程自己把自己分离pthread_detach(pthread_self())也可以主线程将新线程分离。
四、C中的多线程
以上说的是linux中的原生线程库的操作C标准中的线程库其实是对linux中的原生线程库的封装这意味着在编译时也要加上-lpthread.
真实情况是C标准对每个环境下的线程都做了封装所以语言具有跨平台性。
文件操作也是类似的。
类似C标准库简单封装一下线程库
hpp文件代码
#pragma once#include pthread.h
#include iostream
#include stringclass mythread
{typedef void (*fun_t)(const std::string name);public:mythread(const std::string name,fun_t func):_name(name),_func(func){_isrunningfalse;}static void *threadRun(void *args){mythread *thread static_castmythread *(args);thread-_func(thread-_name);return nullptr;}bool Start(){int n pthread_create(_tid, nullptr, threadRun, (void *)this);if (n ! 0)return false;else{_isrunning true;return true;}}void Stop(){if(_isrunning){_isrunningfalse;::pthread_cancel(_tid);}}void Join(){pthread_join(_tid,nullptr);}~mythread(){if(_isrunning){Stop();Join();}}private:pthread_t _tid;std::string _name;bool _isrunning;fun_t _func;
};
注这里的线程运行函数要注意写在类内部默认有this指针参数所以要加上static使其成为静态的。然后传入this指针调用类内部的函数fun_t.
#includeiostream
#includeunistd.h
#includestring
#includemythreadlib.hppvoid mythreadrun(const std::string name)
{while(1){std::coutnew thread is running,name:namestd::endl;sleep(1);}
}int main()
{std::string namethread1;mythread thread1(name,mythreadrun);thread1.Start();sleep(5);thread1.Stop();thread1.Join();return 0;
}
以上就是我们自己模拟的对原生线程的管理。描述是通过这个类描述的管理可以通过一个vector进行管理。
五、线程互斥
线程之间天然就很容易看到同一份资源所以通信很容易但容易造成数据不一致问题对于多个线程能看到的资源我们称之为共享资源我们需要对这部分资源进行保护。保护资源的方式分为互斥和同步。
以下是一个抢票的代码体现了数据不一致的问题。
#include iostream
#include unistd.h
#include string
#include mythreadlib.hppint g_tickets 10000;void mythreadrun(const std::string name)
{while (1){if (g_tickets 0){usleep(1000); // 休息1000微秒 1ms -抢票花费的时间//std::cout here success std::endl;std::cout name get ticket: g_tickets std::endl;g_tickets--;}else{break;}}
}int main()
{// std::string namethread1;mythread thread1(thread1, mythreadrun);mythread thread2(thread2, mythreadrun);mythread thread3(thread3, mythreadrun);mythread thread4(thread4, mythreadrun);thread1.Start();thread2.Start();thread3.Start();thread4.Start();thread1.Join();thread2.Join();thread3.Join();thread4.Join();return 0;
}
抢到最后程序运行结果 会发现出现抢票负数的问题但在代码逻辑中是对g_tickets做了是否大于0的判断的。
解释原因
直接原因首先对g_tickets做是否大于0的判断是一种计算。该计算是通过CPU进行调度的。判断tickets是否大于0要通过三步 CPU寄存器内部的数据可以有多套属于线程私有看起来放在了一套公共寄存器但是属于线程私有当线程切换时要带走自己的数据回来的时候会恢复。tickets只剩一张时线程1被调度判断其是满足条件的但当①步骤做完了②步骤还没做时线程被切换到线程2此时全局变量tickets还没变化还是1那么线程2也判断其是满足条件的也可以抢这张票。tickets--和判断tickets是否大于0是两个独立的操作其也需要分三步重读数据--数据写回数据。那么在重读数据时就会发生tickets变为负数的情况。
1、相关接口使用 如果锁是全局的或者静态的那么直接init即可由于此时锁的生命周期与整个程序的生命周期是一致的则不需要destory如果锁是动态开辟的那么则需要初始化函数对其初始化且需要destory
锁被创建出来、初始化之后需要加锁、解锁 我们使用锁对临界资源进行保护本质是对临界区代码进行保护。我们对所有资源进行访问本质都是通过代码进行访问的。所以我们保护资源就是把访问资源的代码保护起来。 ①加锁的原则是加锁的范围粒度一定要尽量小临界区包含的代码要尽量少
②任何线程要进行抢票都要先申请锁原则上不该有例外
③所有线程申请锁前提是所有线程都得看到这把锁锁本身也是共享资源那么就要求加锁的过程必须是原子的。
④原子性要么不做要么做完没有中间状态就是原子性。
⑤如果线程申请锁失败那么该线程要被阻塞
⑥如果线程申请锁成功那么该线程继续向后运行
⑦线程申请锁成功执行临界区的代码期间可以切换。线程切换在任意时刻都可能做但其他线程无法进入临界区因为申请锁成功的线程未释放锁。也就是说申请锁成功的线程可以放心的执行临界区代码没有其他线程可以进入临界区。
这对于其他线程要么本线程没有申请锁要么释放了锁对其他线程才有意义。这意味着本线程访问临界区对其他线程是原子的。
相关代码
#include iostream
#include unistd.h
#include string
#includevector
#include mythreadlib.hpp
#includeLockGuard.hppint g_tickets 10000;
//pthread_mutex_t gmutex PTHREAD_MUTEX_INITIALIZER; // 定义全局锁并初始化/*void mythreadrun(const std::string name)
{while (1){//pthread_mutex_lock(gmutex);if (g_tickets 0){usleep(1000); // 休息1000微秒 1ms -抢票花费的时间// std::cout here success std::endl;std::cout name get ticket: g_tickets std::endl;g_tickets--;//pthread_mutex_unlock(gmutex);}else{//pthread_mutex_unlock(gmutex);break;}}
}*/ //全局变量锁的写法void myRoute (ThreadData* td)
{while(1){lockguard lock(td-_lock); //临时变量 此段代码执行完时生命周期结束if (g_tickets 0){usleep(1000); // 休息1000微秒 1ms -抢票花费的时间// std::cout here success std::endl;std::cout td-_name get ticket: g_tickets std::endl;g_tickets--;//pthread_mutex_unlock(td-_lock);}else{//pthread_mutex_unlock(gmutex);//pthread_mutex_unlock(td-_lock);break;}}}static int thread_num4;int main()
{std::vectormythread threads;pthread_mutex_t lock;pthread_mutex_init(lock,nullptr);for(int i0;ithread_num;i){std::string namethread-std::to_string(i1); ThreadData* tdnew ThreadData(name,lock);threads.emplace_back(name,myRoute,td);}for(auto thread:threads){thread.Start();}for(auto thread:threads){thread.Join();}return 0;
}
#pragma once
//hpp代码一
#include pthread.h
#include iostream
#include stringclass ThreadData
{
public:ThreadData(const std::string name, pthread_mutex_t *lock): _name(name), _lock(lock){}public:std::string _name;pthread_mutex_t *_lock;
};class mythread
{typedef void (*fun_t)(ThreadData *td);public:mythread(const std::string name, fun_t func, ThreadData *td): _name(name), _func(func), _td(td){_isrunning false;}static void *threadRun(void *args){mythread *thread static_castmythread *(args);thread-_func(thread-_td);thread-_isrunning false; // 运行结束return nullptr;}bool Start(){int n pthread_create(_tid, nullptr, threadRun, (void *)this);if (n ! 0)return false;else{_isrunning true;return true;}}void Stop(){if (_isrunning){_isrunning false;::pthread_cancel(_tid);}}void Join(){pthread_join(_tid, nullptr);}~mythread(){if (_isrunning){Stop();Join();}}private:pthread_t _tid;std::string _name;bool _isrunning;fun_t _func;ThreadData *_td;
};
#pragma once
//hpp代码2
#includepthread.hclass lockguard
{
public:lockguard(pthread_mutex_t* lock):_lock(lock){pthread_mutex_lock(_lock);}~lockguard(){pthread_mutex_unlock(_lock);}
private:pthread_mutex_t* _lock;
};
2、从原理角度理解锁
申请锁成功允许进入临界区的本质是pthread_mutex_lock()函数会返回反之申请锁失败表示锁没有就绪pthread_mutex_lock()函数不返回线程阻塞了。当申请锁成功的线程pthread_mutex_unlock()之后库中的线程会被唤醒从而重新申请锁。
3、从实现角度理解锁
前提
①CPU的寄存器只有一套被所有的线程共享。但是寄存器里面的数据属于执行流的上下文属于执行流私有的数据。
②CPU在执行代码的时候一定要有对应的执行载体。进程线程。
③数据在内存中是被所有线程共享的。
结论把数据移动到寄存器本质是把数据从共享的状态变为线程私有的状态。 六、线程同步
仅仅是线程互斥的话可能会导致一个线程在某段时间内一直获取资源因为某线程在第一次获取临界资源的时候下一次其再次获取临界资源的可能性是更大的。我们为了让线程获取临界资源更合理、让其具有顺序性这里的顺序性就是同步。需要注意的是这里的顺序可以是严格的顺序性也可以是相对的顺序性。
1、条件变量
1相关接口 2理解条件变量
一个线程队列通知机制唤醒一个或者唤醒全部
用一个测试代码测试
#include iostream
#include pthread.h
#include unistd.h
#include string
#include vectorconst int threadnum 4;
int gtickets 10000;
pthread_mutex_t lockPTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condPTHREAD_COND_INITIALIZER;void *thread_Run(void *args)
{char* namestatic_castchar*(args);while (1){//加锁、同步usleep(1000);pthread_mutex_lock(lock);pthread_cond_wait(cond,lock);std::coutI am name,running...std::endl;pthread_mutex_unlock(lock);}
}int main()
{// 对线程同步进行测试std::vectorpthread_t threads;for (int i 0; i threadnum; i){pthread_t tid;char *name new char[128];snprintf(name, 128, thread-%d, i 1);pthread_create(tid, nullptr, thread_Run, (void *)name);threads.emplace_back(tid);}// 线程等待while(1){//唤醒线程//pthread_cond_broadcast(cond);pthread_cond_signal(cond);sleep(1);}// sleep(10);for (auto thread : threads){pthread_join(thread, nullptr);}return 0;
}
测试结果 可以发现是按照一定的顺序打印这就保证了线程同步。
条件变量的使用需要配合互斥锁、等待以及唤醒函数。
2、生产消费模型
生产消费模型有一个三原则
①一个交易场所特定数据结构形式存在的一段内存空间
②两种角色生产者和消费者对应的就是生产线程和消费线程
③三种关系生产者和生产者消费者和消费者生产者和消费者以上的三种关系全都是互斥关系。
3、代码实现生产消费模型
首先是利用阻塞队列来实现生产消费模型什么是阻塞队列 hpp代码
#pragma once#include iostream
#include unistd.h
#include pthread.h
#include queue#define DEFAULT_CAP 4template typename T
class Prod_Cons_Model
{
public:Prod_Cons_Model() {}Prod_Cons_Model(int max_cap DEFAULT_CAP): _max_cap(max_cap){pthread_mutex_init(_mutex,nullptr);pthread_cond_init(_cons_cond,nullptr);pthread_cond_init(_prod_cond,nullptr);}~Prod_Cons_Model(){pthread_mutex_destroy(_mutex);// pthread_cond_destroy(_cons_cond);// pthread_cond_destroy(_prod_cond);pthread_cond_destroy(_cons_cond);pthread_cond_destroy(_prod_cond);}bool Is_Full(){return _block_queue.size() _max_cap;}bool Is_Empty(){return _block_queue.size() 0;}void Productor(const T data){// 临界资源pthread_mutex_lock(_mutex);while (Is_Full()) // 是while而不是if{pthread_cond_wait(_prod_cond, _mutex);}_block_queue.push(data);pthread_mutex_unlock(_mutex);//和signal函数的顺序对调也可以pthread_cond_signal(_cons_cond);}// 消费数据void Consume(T *ret){pthread_mutex_lock(_mutex);while (Is_Empty()){pthread_cond_wait(_cons_cond, _mutex);}*ret _block_queue.front();_block_queue.pop();// 唤醒生产者pthread_mutex_unlock(_mutex);pthread_cond_signal(_prod_cond);}private:std::queueT _block_queue;int _max_cap;pthread_mutex_t _mutex;pthread_cond_t _cons_cond;pthread_cond_t _prod_cond;
};
cpp代码
#include model.hpp
#include vector
#include cstdlib
#include pthread.h
#include ctime
#include iostream
void *myconsume(void *args)
{auto model static_castProd_Cons_Modelint *(args);while (1){int ret;model-Consume(ret);std::cout I am customer,get data: ret std::endl;}return nullptr;
}void *myproductor(void *args)
{auto model static_castProd_Cons_Modelint *(args);while (1){sleep(2);int data rand() % 5; // 生成5以内的随机数model-Productor(data);std::cout I am productor,prodece data: data std::endl;}return nullptr;
}int main()
{Prod_Cons_Modelint *Int_Model new Prod_Cons_Modelint(4);pthread_t cons1;pthread_t cons2;pthread_t prod1;pthread_t prod2;pthread_t prod3;srand(time(nullptr) ^ getpid());pthread_create(cons1, nullptr, myconsume, Int_Model);pthread_create(cons2, nullptr, myconsume, Int_Model);pthread_create(prod1, nullptr, myproductor, Int_Model);pthread_create(prod2, nullptr, myproductor, Int_Model);pthread_create(prod3, nullptr, myproductor, Int_Model);pthread_join(cons1, nullptr);pthread_join(cons2, nullptr);pthread_join(prod1, nullptr);pthread_join(prod2, nullptr);pthread_join(prod3, nullptr);return 0;
}
①pthread_cond_wait函数详解
当pthread_cond_wait函数调用的时候不仅让自己这个线程进入等待状态停在该函数内部还释放获取的锁当被唤醒时再次进入队列中竞争锁。当再次竞争到锁时函数才返回这就是该函数第二个参数还要传入一个锁的原因。
pthread_cond_wait函数一定要在临界代码中原因是等待函数要检测队列中数据是否满足条件这个检测的过程需要在临界代码中。而信号量是不需要判断。
②用while而不是if
这里注意在消费函数和生产函数内部需要等待的时候要用while而不是if原因是当有2个线程进入等待状态而唤醒用的是broad_cast的话2个线程同时被唤醒其中一个竞争到锁另一个被阻塞在锁里而不是wait里。竞争到锁的线程对队列中的内容进行操作以消耗数据为例如果此时将队列中的数据全消耗完了然后释放锁而另一个线程竞争到锁之后往后走会直接消耗队列中的数据而队列中的数据已经为空了这就会导致问题。所以要用while而不是if.
③解锁和唤醒函数的顺序
在生产函数和消费函数内部解锁和唤醒两个函数的顺序是可以对调的。因为无论哪种顺序唤醒线程之后被唤醒的线程还是要竞争同一把锁所以在解锁前和解锁后都一样。但是唤醒的操作一定要在对队列进行操作之后。
④分配任务
注意这里生产消费模型还可以用于任务的分配执行。例如生产出一个任务交给消费者去执行。
⑤代码适用性
以上的代码不仅适合单生产单消费情景也适合多生产多消费情景。具体在实际应用中的如何选择则要根据需要。
4、生产消费模型的特点
1协调忙闲不均
通过线程同步实现对忙闲不均的协调
2生产者代码和消费者代码解耦
生产者代码和消费者代码互不影响。
3效率高
尽管在临界资源中永远只有一个线程在执行但是生产任务、处理任务也是需要花费时间的对于消费者来说获取任务和处理任务是并发的对于生产者来说发送任务和产生任务是并发的这里的并发对于整个工作流程是效率高的。
5、信号量
1信号量概念
见《共享内存与信号量》一文。
2信号量相关接口
初始化 value表示的是多少信号量。
信号量的P操作 该操作的意思是申请信号量申请成功时信号量--申请失败时则会阻塞在这个函数中。
信号量的V操作 3环形队列配合信号量实现生产消费模型
环形队列的特点当headend时队列为空或者队列为满。如何判断空还是满引入一个计数器。
多线程如何在环形队列中生产和消费
①当队列为空让生产者先生产
②当队列为满时让消费者先消费
③为空为满是少数情况大部分情况是既不为空也不为满此时head生产者下标和end消费者下标一定不指向一个位置此时允许生产和消费同时进行。此时可以看出环形队列一定是比阻塞队列实现的生产消费模型快的。
以上的这些条件可以直接使用信号量实现。所以说信号量是用来实现互斥与同步的。
对于消费者来说数据资源是他真正的资源而对于生产者而言空间资源是其真正的资源。所以我们得设置两个信号量一个是对应数据资源一个是对应空间资源。在初始化时设置数据资源为空空间资源为满等于环形队列的容量。
对于生产者要申请空间资源P一个空间资源但释放的是一个数据资源V一个空间资源因为生产者申请到一个空间资源后是向这个空间资源中放数据的放完数据后空间资源并未增多而是数据资源增多了一个对于消费者要申请数据资源而要释放一个空间资源因为消费者申请一个数据资源后是拿到该数据拿到之后该数据已经没有用了所以是释放空间资源可以让生产者再在这个空间生产数据。
4代码实现
先写上层调用逻辑
对于单生产单消费模型让生产者生产消费者消费让两者看到同一份环形队列资源生产者不断地生产消费者不断地消费对于队列空和满的情况在.hpp文件中实现即可。
Main.cc代码
#include RingQueue.hpp
#include pthread.h
#include unistd.h
#include ctimevoid *Consume(void *args)
{RingQueueint *rq static_castRingQueueint *(args);while (1){// 消费int out;rq-Pop(out);// 处理数据std::cout 得到的数据为: out std::endl;sleep(1);}
}void *Productor(void *args)
{RingQueueint *rq static_castRingQueueint *(args);sleep(2);while (1){// 构造数据int in rand() % 10 1;// 生产rq-Push(in);std::cout 构建的数据为: in std::endl;sleep(2);}
}int main()
{srand(time(nullptr) ^ getpid());RingQueueint *rq new RingQueueint();pthread_t c, p;pthread_create(c, nullptr, Consume, rq);pthread_create(p, nullptr, Productor, rq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}
hpp代码
#pragma once#includepthread.h
#includeiostream
#includestring
#includevector
#include semaphore.h#define DEFAULT_SIZE 5templatetypename t
class RingQueue
{
private:void P(sem_t sem){sem_wait(sem);}void V(sem_t sem){sem_post(sem);}
public:RingQueue(int sizeDEFAULT_SIZE):_max_cap(size),_head(0),_end(0) {_queue.resize(_max_cap);sem_init(_data_sem,0,0);sem_init(_space_sem,0,_max_cap);pthread_mutex_init(_c_mutex,nullptr);pthread_mutex_init(_p_mutex,nullptr);}~RingQueue(){sem_destroy(_data_sem);sem_destroy(_space_sem);pthread_mutex_destroy(_c_mutex);pthread_mutex_destroy(_p_mutex);}void Push(const t data)//生产{P(_space_sem);pthread_mutex_lock(_p_mutex);_queue[_head]data;_head;_head%_max_cap;pthread_mutex_unlock(_p_mutex);V(_data_sem);}void Pop(t* out)//消费{P(_data_sem);pthread_mutex_lock(_c_mutex);*out_queue[_end];_end;_end%_max_cap;pthread_mutex_unlock(_c_mutex);V(_space_sem);}private:std::vectort _queue;int _max_cap;int _head;//生产者下标int _end;//消费者下标// int _data_num; //不太需要sem_t _data_sem;sem_t _space_sem;pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex;};
代码验证结果保持了同步以及互斥 但如果要实现多生产多消费则要加上锁。