1)引入
①线程概念
什么是线程:
- 在一个程序里的一个执行路线就叫做线程(thread)更准确的定义是:线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
如果我们创建”进程”,不独立创建地址空间,户级页表,甚至不进行I/O将程序的数据和代码加载到内存,我们只创建task struct, 然后让新的PCB,指向和老的PC指向同样的mm_struct,然后,通过合理的资源分配(当前进程的资源) ,让每个task struct都能使用进程的一-部分资源, 此时 每个PCB被CPU调度的时候,执行的‘粒度“比原始进程执行的’粒度’会更小一些
②重新认识进程
站在OS的角度
:进程是承担系统资源分配的基本单位 一个进程创建好后,内部可能存在多个执行流(线程)
以往我们所认识的进程
:承担系统资源的基本实体,但内部只有一个执行流
③Linux线程和其他平台的线程
站在CPU的角度进程
:没有任何区别实际上,CPU执行的时候,进程([可能] 执行的“进程流”)已经比历史的进程更加轻量化
Linux下
,其实是没有真正意义上面的线程概念的,而是用进程来模拟的轻量级进程,所以Linux不可能直接在OS层面提供线程的系统调用接口,顶多是轻量级进程调度接口
Windows下
,系统存在大量的进程(一个进程对应多个线程),所以为了管理这些大量的线程,windows必须描述线程为TCB
并组织起来,这样往往会比较复杂(一定会有大量的线程相关操作的系统调用接口)
④线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
⑤Linux进程和线程的区别
进程的多个线程共享 同一地址空间,因此代码段、数据段都是共享的,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表(
注意:多进程是不同的文件描述符表,但内容可以一样
)- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
线程自己会拥有的数据:
- 线程ID
- 一组寄存器
栈
- errno
- 信号屏蔽字
- 调度优先级
⑥总结
进程本质是承担分配系统资源的基本实体
线程是0S调度的基本单位
线程优点:
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
线程缺点:
性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多
总结
:因为所有的PCB都共享地址空间,理论上,每个”线程“都能看到进程的所有的资源,线程并不是越多越好
带来的优点:
线程间通信,成本特别低,
带来的缺点:
一 定存在大量的临界资源,势必可能需要使用各种互斥和同步机制
2)线程控制
①线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
功能
:创建一个新的线程
返回值
:成功返回0;失败返回错误码
thread
:输出型参数返回线程ID
attr
:设置线程的属性,attr为NULL表示使用默认属性
start_routine
:回调函数,是个函数地址,线程启动后要执行的函数
arg
:传给线程启动函数(回调函数)的参数
编译时要加上-lpthrea选项,
$(CXX) -o $@ $^ $(LDFLAGS) -lpthread
创建一个线程:
void *thread_run(void *args) { while(1){ cout<<(char*)args<<endl; sleep(1); } } int main(int argc, char *argv[]) { pthread_t tid; pthread_create(&tid, nullptr, thread_run, (void*)"thread1"); while(1){ cout<<"main thread ..."<<endl; sleep(1); } return 0; }
ps axj
是查看进程
在Linux中查看轻量级进程的命令:ps -aL
其中LWP表示
:执行流是一个轻量级进程,标识其唯一性,CPU在调度的时候,以LWP为准,在进程只有一个线程的时候,LWP==PID
②线程ID及进程地址空间布局
pthread_self 线程ID
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和前面说的线程ID不一样:
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
- pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴 线程库的后续操作,就是根据该线程ID来操作线程的
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID
#include <pthread.h>
pthread_t pthread_self(void);
功能
:获得线程自身的ID
pthread线程库和pthread_t
pthread库
(-lpthread)
- Linux操作系统没有真正意义上面的线程,是用进程模拟的! —轻量级进程, Linux操作系统本身不会直接提供类似的线程创建,终止,等待,分离等相关system call 接口,但是会提供创建轻量级进程的接口----vfork
- 但是用户需要所谓的线程创建,终止,等待,分离等相关接口,所以,为了更好的适配,系统基于轻量级进程的接口,模拟封装了一个用户原生线程库,pthread.
- 线程id, 状态,优先级,其他属性用来进行用户级线程管理(
TCB, 不用内核维护,而在用户空间维护
)
上面所谓的用户层线程地址(ID),指的是pthread库(映射到共享区)中的某一个起始位置(地址)
(pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址)
注意
:
- 主线程直接使用进程地址空间的栈,而子线程使用pthread库的线程栈
在Linux中用户级线程(tid)和内核级线程(LWP)是1:1对应的
- 定义的全局变所有的线程都可以访问
- 对全局变量做的修改比如: ++, --,由于是临界资源,有可能会有风险 所以应该保证操作是原子性的
③线程终止
pthread_exit
#include <pthread.h>
void pthread_exit(void *retval);
功能
:终止线程
返回值
:返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
注意
:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
void *retval
:同return返回值,不要指向一个局部变量
编译时要加上-lpthrea选项,
$(CXX) -o $@ $^ $(LDFLAGS) -lpthread
终止线程:
void *ThreadRoutine(void *args) { int i=*(int*)args; delete (int*)args; int cnt=5; while(cnt){ cout<<"Thread_index i "<<i<<"cnt="<<cnt<<endl; sleep(1); cnt--; } //return nullptr; pthread_exit((void*)10); } int main(int argc, char *argv[]) { #define NUM 5 pthread_t tids[NUM]; for(auto i=0;i<NUM;i++){ int *p=new int(i); pthread_create(tids+i, nullptr, ThreadRoutine, p); } while(1) { cout<<"main thread ..."<<endl; sleep(1); } return 0; }
return nullptr
ThreadRoutine使用return nullptr结束线程,效果一样
pthread_cancel
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能
:取消一个执行中的线程
返回值
:成功返回0;失败返回错误码)
threadl
:线程id
代码如下:
void *ThreadRoutine(void *args) { int i=*(int*)args; delete (int*)args; int cnt=5; while(true){ cout<<"Thread_index i "<<i<<"cnt="<<cnt<<endl; sleep(1); cnt--; } //return nullptr; //pthread_exit((void*)10); } int main(int argc, char *argv[]) { #define NUM 5 pthread_t tids[NUM]; for(auto i=0;i<NUM;i++){ int *p=new int(i); pthread_create(tids+i, nullptr, ThreadRoutine, p); } sleep(5); for(int i=0;i<NUM;i++) { pthread_cancel(tids[i]); cout<<"Thread "<<tids[i]<<" has been canceled"<<endl; sleep(1); } while(1) { cout<<"main thread ..."<<endl; sleep(1); } return 0; }
注意
:不建议使用
,如果在子线程中取消主线程会造成主线程defunc
,僵尸状态pthread_t main_thread;//main函数中用pthread_self()获取主线程线程id void *ThreadRoutine(void *args) { int i=*(int*)args; delete (int*)args; int cnt=5; while(true){ cout<<"Thread_index i "<<i<<"cnt="<<cnt<<endl; sleep(1); pthread_cancel(main_thread); cnt--; } }
总结
注意:
不可以使用exit()结束子线程
线程一般终止之后, 必须进行等待 main thread,如果不等待,会造成和进程退出类似的效果(僵尸进程)
④线程等待
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能
:等待线程结束
返回值
:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
注意:
- main thread join的时候
不需要考虑线程崩溃问题
,因为出现错误会直接中断此进程
threadl
:线程id
value_ptr
:输出型参数,它指向一个指针,后者指向线程的返回值
代码验证如下:
pthread_t tids[NUM]; void *ThreadRoutine(void *args) { int cnt =rand()%5+5;//随机时间的等待 while(cnt){ cout<<"thread: "<< pthread_self()<<"| cnt: "<<cnt<<"is running..."<<endl; cnt--; sleep(1); } //pthread_cancel(pthread_self()); //sleep(3); return (void*)11; } int main(int argc, char *argv[]) { srand((unsigned long)time(nullptr)); for(auto i=0; i<NUM;i++){ pthread_create(&tids[i], nullptr, ThreadRoutine, nullptr); } //sleep(3); sleep(1); for(auto i=2;i<NUM;i++){ pthread_cancel(tids[i]); cout<<"cancel "<<tids[i]<<" success"<<endl; } cout<<"main thread join ..."<<endl; void *st=nullptr; for(auto i=0;i<NUM;i++){ if(0==pthread_join(tids[i],&st)){ cout<<"thread "<<tids[i]<<"| exit code: "<<(int*)st<<" quit join success..."<<endl; } } cout<<"main thread join over..."<<endl; return 0; }
其中0xff为-1,即PTHREAD_ CANCELED
将线程取消(pthread_cancel)在ThreadRoutine中进行:
,同时线程取消后立马退出void *ThreadRoutine(void *args) { int cnt =rand()%5+5;//随机时间的等待 while(cnt){ cout<<"thread: "<< pthread_self()<<"| cnt: "<<cnt<<"is running..."<<endl; cnt--; sleep(1); } pthread_cancel(pthread_self()); //sleep(3); return (void*)11; }
观察到并未调用pthread_cancel函数
解释:
- cancel本身具有一定的延时性,可能并不是被立即受理,建议在线程执行中cancel最好(main->other thread)
- 上面的情况可能是新线程被创建了,但并未被调度,所以一定要让新线程先完全跑起来在进行pthread_cancel
在return前sleep(3)秒结果正常
pthread_cancel(pthread_self()); //sleep(3); return (void*)11;
⑤线程分离
更改ThreadRoutine函数,execl进程替换
void *ThreadRoutine(void *args) { cout<<"using execl..."<<endl; execl("/bin/ls","ls","-a","-l",nullptr); cout<<"execl over"<<endl; return (void*)11; }
可以发现并没有打印execl over,同时主线程的后续线程创建并没有继续执行,进程替换会替换掉整个进程,main函数也会
是否join
- 默认情况下,新创建的线程是joinable的, 线程退出后,需要对其进行pthread_ join操作, 否则无法释放资源,从而造成系统泄漏
- 如果不关心线程的返回值,join是一种负担, 这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
- 分离的本质,是让主线程不用在join新线程,从而可以让新线程退出的时候,白动回收资源
pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能
:可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
注意
:joinable和分离是冲突的,一个线程不能既是joinable又是分离的
threadl
:线程id
测试代码:
void *ThreadRoutine(void *args) { pthread_detach(pthread_self()); return (void*)11; } int main() { //...pthread_create int ret=pthread_join(tids[i],&st); if(0==ret) cout<<"thread "<<tids[i]<<"| exit code: "<<(int*)st<<" quit join success..."<<endl; else cout<<"thread join wrong: "<<ret<<endl; }
只要返回不为0就是join失败
注意
:
- 如果一个线程被设置为分离状态亥线程不应该被join,如果join, 结果是未定义,join出错
即便线程被设置为分离状态,但是如果该线程依旧出错崩溃,还是会影响主线程和其他正常线程.
⑥线程互斥
引入
:模拟一个抢票系统:int tickets = 900; void *Route(void *args) { while(1){ if(tickets>0){ usleep(10000);//抢票时间 printf("0x%x: get ticket: %d\n", pthread_self(), tickets--); } else break; } printf("0x%x: quit, tickets=%d\n", pthread_self(), tickets); return NULL; } int main() { #define NUM 5 pthread_t nums[NUM]; for(int i=0;i<NUM;i++) pthread_create(nums+i, NULL, Route, NULL); for(int i=0;i<NUM;i++) pthread_join(nums[i], NULL); return 0; }
运行结果
:
注意
:这的tickets–并不是一个操作,并不是原子性的
,而是分三个步骤:分别对应三条汇编指令load update store
- tickets从内存读到到CPU相关的寄存器中
- 我们要对读到CPU内部的tickets进行++(–)操作
- 将操作完成的tickets写回内存
分析:
如图
:在线程A的第二步时,时间片到了,或者B线程的优先级更高,这时寄存器会保存999,同时进行线程B,线程B执行完毕tickets更改为500,返回线程A执行第三步,tickets由500直接变为999,有线程安全问题
pthread_mutex_t
通过pthread_mutex_t定义一个互斥锁
pthread_mutex_t lock
互斥量接口
#include <pthread.h>
动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
功能
:动态或静态分配一个mutex(注意静态分配的mutex可以不用释放)
返回值
:成功返回0,失败返回错误码
mutex
:要初始化的互斥量
attr
:NULL
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能
:销毁互斥量(注意静态分配的mutex可以不用释放)
注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
返回值
:同pthread_mutex_init
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能
:
- lock:阻塞式加锁
- trylock:非阻塞式申请锁(多个线程只有一个线程能申请成功)
- unlock:解锁
返回值
:成功返回0,失败返回错误码
mutex
:互斥量
为模拟抢票加锁:
int tickets = 900; pthread_mutex_t lock; void *Route(void *args) { while(1){ pthread_mutex_lock(&lock); if(tickets>0){ usleep(10000);//抢票时间 printf("0x%x: get ticket: %d\n", pthread_self(), tickets--); pthread_mutex_unlock(&lock); } else{ break; pthread_mutex_unlock(&lock); } } printf("0x%x: quit, tickets=%d\n", pthread_self(), tickets); return NULL; } int main() { pthread_mutex_init(&lock, NULL); #define NUM 5 pthread_t nums[NUM]; for(int i=0;i<NUM;i++) pthread_create(nums+i, NULL, Route, NULL); for(int i=0;i<NUM;i++) pthread_join(nums[i], NULL); pthread_mutex_destroy(&lock);//释放资源 return 0; }
通过锁间接完成了原子性
互斥锁如何保持原子性
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
下图解释,通过xchgb汇编命令,mutex的这个1相当于一个唯一的令牌,只有%al==1的线程才能拥有锁
⑦线程安全与可重入
线程安全
:描述的是线程之间的关系以及访问某些函数、数据、区域是否会引起线程问题)
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
可重入
:函数的状态,比如,是否可以被多个执行流同时进入,而且不会出现问题
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
联系:
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
区别:
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
⑧死锁(MARK一下)
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态
死锁的必要条件
互斥条件:
- 一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
方法:
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
算法
:死锁检测算法,银行家算法…
银行家算法实验
⑨线程同步
引入
存在一种情况,一个线程竞争锁的能力非常强,每次申请,都是它优先申请到锁,而其他线程没有机会获取锁,这个线程一直在申请锁,检测,释放锁,这样并不合理
如果我们规定某线程在释放锁之后如果要继续申请锁,就必须排到所有线程的尾部,这种排队的本质,其实是让我们获取锁在安全的情况下,按照某种顺序进行释放和申请,这叫做同步的过程
条件变量接口
条件变量是一个线程库提供的描述临界资源状态的对象,变量
场景:
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
定义一个条件变量
pthread_cond_t cond
#include <pthread.h>
动态分配:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
静态分配:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁
int pthread_cond_destroy(pthread_cond_t *cond);
类似pthread_mutex,分为静态和动态
功能
:初始化,销毁cond(注意静态分配的cond可以不用释放)
返回值
:同pthread_mutex_~
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能比较:
pthread_cond_wait 线程等待信号触发,如果没有信号触发,无限期等待下去 pthread_cond_timedwait 线程等待一定的时间,如果超时或有信号触发,线程唤醒 sleep 线程等待,等待期间线程无法唤醒
pthread_cond_wait
:当你挂起时自动释放锁,当你唤起时,自动获取锁
返回值:
在修改由mutex指定的mutex或由cond指定的条件变量的状态之前返回,成功0,失败错误码
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
broadcast功能
:将所有在cond条件变量下等待的线程全部唤醒
signal功能
:唤醒在指定条件变量cond等待下等待的一个线程
返回值
:成功0,失败错误码
为什么 pthread_cond_wait 需要互斥量
解释:
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护 没有互斥锁就无法安全的获取和修改共享数据
如下错误代码:
如果在wait前后手动加解锁
,解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过// 错误设计 pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_mutex_unlock(&mutex); //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过 pthread_cond_wait(&cond); pthread_mutex_lock(&mutex); } pthread_mutex_unlock(&mutex);
参考:下面基于阻塞队列的生产者消费者模型
条件变量使用规范
等待条件
pthread_mutex_lock(&mutex); while (/*条件为假*/) pthread_cond_wait(cond, mutex); //修改条件 pthread_mutex_unlock(&mutex);
给条件发送信号
pthread_mutex_lock(&mutex); //设置条件为真 pthread_cond_signal(cond); pthread_mutex_unlock(&mutex);
⑩生产者消费者模型
引入
举例
:供货商 店铺 买家
如果让买家和供货商直接通讯,回到上面条件变量的例子,买家就会不停的占用锁,检测(发现没有货)释放锁
而如果加上商店这个容器,当商店没货了就告诉买家不用来,当商店货满了就告诉供货商不要来
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的
优点总结:
- 解耦
- 支持并发
- 支持忙闲不均
生产者消费者模型存在三种关系
:生产者和生产者(竞争/互斥
),生产者和消费者(竞争/互斥
和同步
),消费者和消费者(竞争/互斥
)
生产者消费者模型存在两种角色
:生产者 消费者(线程或者进程)
生产者消费者模型存在一种交易场所
:一段内存空间(可以是自己定义的数组,集合,链表等)(管道通信)
基于阻塞队列的生产者消费者模型
阻塞队列
:队列有上限,当队列不满足生产(队列满)或消费条件(队列空)的时候,对应的线程,应该阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
代码实现一个基于阻塞队列的生产者消费者模型:
引入条件变量之前:
block_queue.hpp#pragma once #include <iostream> #include <queue> #include <pthread.h> #include <unistd.h> using namespace std; template<class T> class BlockQueue{ private: int _cap; pthread_mutex_t lock; queue<T> bq; private: bool isfull() { return bq.size()==_cap; } bool isempty() { return bq.size()==0; } public: BlockQueue(int cap) :_cap(cap) { pthread_mutex_init(&lock, nullptr); } ~BlockQueue() { pthread_mutex_destroy(&lock); } void Put(const T &in)//const T &输入型参数 { //生产 pthread_mutex_lock(&lock);//加锁 if(!isfull()) bq.push(in); else cout<<"is full put is blocked..."<<endl; pthread_mutex_unlock(&lock);//解锁 } void Get(T *out)//T *输出型参数 T &输入输出型参数 { //消费 pthread_mutex_lock(&lock);//加锁 if(!isempty()){ *out=bq.front(); bq.pop(); } else cout<<"is empty get is blocked..."<<endl; pthread_mutex_unlock(&lock);//解锁 } };
cp.cc
#include "block_queue.hpp" using namespace std; #define NUM 30 void *consumer(void *c) { BlockQueue<int> *bq=(BlockQueue<int>*)c; int out=0;//定义一个输出型参数 while(true){ bq->Get(&out); cout<<"consumer: "<<out<<endl; sleep(1); } return nullptr; } void *productor(void *p) { BlockQueue<int> *bq=(BlockQueue<int>*)p; int in=100; while(true){ bq->Put(in); cout<<"productor: "<<in<<endl; } return nullptr; } int main() { BlockQueue<int> *bq = new BlockQueue<int>(NUM); pthread_t c,p; pthread_create(&c, nullptr, consumer, bq); pthread_create(&p, nullptr, productor, bq); pthread_join(c,nullptr); pthread_join(p,nullptr); delete bq; return 0; }
结果如下:
分析:
- 由于在consumer操作里有sleep操作,消费相对生产慢,productor一直在生产,直到一百
- 然而我们可以看到一直在打印is full put is blocked…productor: 100,实际上并没有真正的生产,Put函数体内一直在执行申请锁,检测是否满,释放锁的操作,
这种轮询操作并不合理
引入条件变量:
block_queue.hppprivate: int _cap; pthread_mutex_t lock;//定义一个互斥锁 pthread_cond_t = have_space;//定义一个生产者条件变量 pthread_cond_t = have_data;//定义一个消费者条件变量 queue<T> bq;//临界资源 public: BlockQueue(int cap) :_cap(cap) { pthread_mutex_init(&lock, nullptr); pthread_cond_init(&have_space, nullptr); pthread_cond_init(&have_data, nullptr); } ~BlockQueue() { pthread_mutex_destroy(&lock); pthread_cond_destroy(&have_space); pthread_cond_destroy(&have_data); } void Put(const T &in)//const T &输入型参数 { //生产 pthread_mutex_lock(&lock);//加锁 if(isfull()){ pthread_cond_wait(&have_space, &lock); } bq.push(in); pthread_mutex_unlock(&lock);//解锁 pthread_cond_signal(&have_data);//唤醒Get } void Get(T *out)//T *输出型参数 T &输入输出型参数 { //消费 pthread_mutex_lock(&lock);//加锁 if(isempty()){ pthread_cond_wait(&have_data, &lock); } *out=bq.front(); bq.pop(); pthread_mutex_unlock(&lock);//解锁 pthread_cond_signal(&have_space); }
更改Put、Get函数
如果我们在判断isfull后直接pthread_cond_wait()
挂起时,由于我们在这条语句上文已经申请了锁且并没有释放锁,所以其他线程不能获取锁,Get操作也不能进行,block_queue队列永远是满的,这也就是为什么pthread_cond_wait的第二个参数为pthread_mutex_t *restrict mutex
当你挂起时自动释放锁,当你唤起时,自动获取锁
(注意,不能手动在wait前先解锁,wait后再加锁,有风险)
伪唤醒
对于以下语句:
if(isfull()){ pthread_cond_wait(&have_space, &lock); } bq.push(in);
有可能出现:
- 使用broadcast而不是signal唤醒,多个线程竞争同一把锁
- 处于某种情况提前唤醒线程,而队列已满,执行bq.push(in)出问题
所以我们建议都将if改为while循环,即使提前唤醒,仍可以达到不满足唤醒条件继续挂起的效果
while(isfull()){ pthread_cond_wait(&have_space, &lock); } bq.push(in);
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但
POSIX可以用于线程间同步
sem_t
:信号量
在semaphore.h中typedef union { char __size[__SIZEOF_SEM_T]; long int __align; } sem_t;
信号量函数
应用代码见基于环形队列的生产者消费者模型
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能
:初始化信号量
返回值
:成功0,失败-1同时errno设为错误码
pshared
: 0表示线程
间共享,非零表示进程
间共享
value
:信号量初始值
#include <semaphore.h>
int sem_destroy(sem_t *sem);
功能
:销毁信号量
返回值
:成功0,失败-1同时errno设为错误码
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能
:等待信号量,调用一次相当于对 sem 做了一次--,sem为0阻塞
返回值
:成功0,失败返回-1同时errno设为错误码,semaphore不变
struct timespec
: 是一个结构体,它指定自Epoch 以来的绝对超时(以秒和纳秒为单位)struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0 .. 999999999] */ };
#include <semaphore.h>
int sem_post(sem_t *sem);
功能
:发布信号量,相当于对于 sem 做了 ++ 操作
返回值
:成功0,失败返回-1同时errno设为错误码,semaphore不变
基于环形队列的生产者消费者模型
环形队列采用数组模拟,用模运算(%)来模拟环状特性
ring_queue.hpp#include <vector> #include <semaphore.h> #include <pthread.h> using namespace std; template <class T> class RingQueue{ private: int cap; vector<T> ring; int c_index; int p_index; //pthread_mutex_t c_lock;//多生产消费模型需要锁 //pthread_mutex_t p_lock; sem_t sem_space; sem_t sem_data; public: RingQueue(int _cap):cap(_cap), ring(_cap), c_index(0), p_index(0) { //pthread_mutex_init(&c_lock, nullptr);//初始化消费者互斥锁 //pthread_mutex_init(&p_lock, nullptr);//初始化生产者互斥锁 sem_init(&sem_space, 0, _cap);//初始化生产者信号量 sem_init(&sem_data, 0, 0);//初始化消费者信号量 } void Put(const T &in) { sem_wait(&sem_space); //多生产消费lock // ring[p_index] = in; sem_post(&sem_data); p_index++; p_index %= cap; //多生产消费unlock } void Get(T *out) { sem_wait(&sem_data); //多生产消费lock // *out = ring[c_index]; sem_post(&sem_space); c_index++; c_index %= cap; //多生产消费unlock } ~RingQueue() { //pthread_mutex_destroy(&c_lock); //pthread_mutex_destroy(&p_lock); sem_destroy(&sem_space); sem_destroy(&sem_data); } };
r_cp.cc
#include "ring_queue.hpp" #include "task.hpp" #include <pthread.h> #include <ctime> #include <cstdlib> #include <unistd.h> void *consumer(void *ring_queue) { RingQueue<int> *rq = (RingQueue<int>*)ring_queue; while(true){ sleep(1);//消费者等待一秒,生产者生产玩完0个处于阻塞再进行消费 int out=0; rq->Get(&out); std::cout << "消费者: hander Task Done , result: " << out << std::endl; } } void *producter(void *ring_queue) { RingQueue<int> *rq = (RingQueue<int>*)ring_queue; int count = 0; while(true){ sleep(1);//一秒生产一个,生产消费同步进行 int data=count; rq->Put(data); std::cout << "Producter data: " << count << std::endl; count++; } } int main() { RingQueue<int> *rq = new RingQueue<int>(10); pthread_t c, p; pthread_create(&c, nullptr, consumer, rq); pthread_create(&p, nullptr, producter, rq); pthread_join(c, nullptr); pthread_join(p, nullptr); return 0; }
多生产消费
注意
:
在多生产消费模型下需要互斥锁(多个生产者可以同时更改p_index,多个消费者可以同时更改c_index
),加在sem_wait和sem_post间sem_wait(&sem_space); //多生产消费lock // ring[p_index] = in; sem_post(&sem_data);
如果加在sem_wait前效率会更低,只能在等到可以申请到锁的时候再去申请信号量,而加在后面可以提前申请好信号量,申请到锁就可以直接执行
更改r_cp.cc实现生产者提供操作数,消费者进行1~n(n<=10)累加计算处理
实现一个task类:task.hpp
#include <iostream> class Task{ private: int top; //[1,top] public: Task():top(1){} Task(int _top):top(_top) {} int Handler() { int sum = 0; for(auto i= 0; i <= top; i++){ sum += i; } return sum; } void Show() { std::cout << "生产者: 这个任务是,累加数据从1~" << top << std::endl; } ~Task() {} };
修改r_cp.cc
#include "ring_queue.hpp" #include "task.hpp" #include <pthread.h> #include <ctime> #include <cstdlib> #include <unistd.h> void *consumer(void *ring_queue) { RingQueue<Task> *rq = (RingQueue<Task>*)ring_queue; while(true){ //sleep(1); //1. 消费任务 Task t; rq->Get(&t); //2. 处理任务 int result = t.Handler(); std::cout << "消费者: hander Task Done , result: " << result << std::endl; } } void *producter(void *ring_queue) { RingQueue<Task> *rq = (RingQueue<Task>*)ring_queue; while(true){ sleep(1); //1. 制造任务 int top = rand()%10+1; //[1, 10] Task t(top); t.Show(); //2. 生产任务 rq->Put(t); } } int main() { //srand((unsigned long)time(nullptr)); RingQueue<Task> *rq = new RingQueue<Task>(10); pthread_t c, p; pthread_create(&c, nullptr, consumer, rq); pthread_create(&p, nullptr, producter, rq); pthread_join(c, nullptr); pthread_join(p, nullptr); return 0; }