线程概念
线程
- 一个程序里的一个执行路线就叫做线程(thread),线程是一个进程内部的控制序列
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间运行
- 在Linux系统中,在CPU中,看到的PCB都要比传统进程更加轻量化
- 通过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程的执行流。
在Linux中,站在CPU的角度,能否识别task_struct是进程还是线程?
答:不能也不需要识别,CPU只关心一个一个的独立执行流
页表映射
线程的优点
- 创建新线程代价比创建新进程小
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源比进程少
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高行能,将I/O操作重叠,线程可以同时等待不同 的I/O操作
ps:
计算密集型:执行流的大部分任务,主要以计算为主:加密解密,排序查找
I/O密集型:执行流的大部分任务,是以IO为主的:刷磁盘,访问数据库,访问网络
线程的缺点
- 行能缺失:一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失(增加了额外的同步和调度开销,而可用的资源不变)
- 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配的细微偏差或因共享了不该共享的变量而导致不良的影响(线程之间是缺乏保护的)
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
- 编程难度提高
线程异常
- 单个线程如果出现除0,野指针等问题导致线程崩溃,进程也会随之崩溃
- 线程是进程内部的执行分支,线程出现异常,就类似于进程出现异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也随即退出
线程用途
- 合理使用多线程,能提高CPU密集型程序的执行效率
- 合理使用多线程,能提高IO密集型程序的用户体验(一边下载视频,一边看视频)
进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也有自己的一部分数据:线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级
进程的多个线程共享
同一个地址空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到;线程hi哎共享以下进程资源和环境:文件描述符表,每种信号到处理方式(SIG_DEF,SIG_IGN,自定义的信号处理函数),当前工作目录,用户id和组id
Linux 线程控制
POSIX线程库
- 与线程有关的函数构成了一个完整的函数系列,大多数函数的名字以"pthread_"开头
- 头文件<pthread.h>
- 链接这些线程函数库时要使用编译器命令的"-lpthread"选项
创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// thread 返回线程id
//attr:设置线程的属性,attr为NULL表示使用默认属性
//start_routine:是一个函数地址,线程启动后要执行的函数
//arg:传给线程启动函数的参数
错误检查:
- 一些函数是成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误
- pthreads函数出错时不会设置全局函数errno,而是将错误代码通过返回值返回
- pthread提供了线程内的errno变量,以支持它使用的errno代码对于pthread函数的错误,建议通过返回值判定,因为读取返回值比读取线程内的errno变量的开销小
创建一个线程
Makefile
mythread:mythread.c
gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
rm -f mythread
mythread.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
while(1){
printf("%s : pid : %d,ppid:%d\n",msg,getpid(),getppid());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,Routine,(void*)"thread 1");
//主线程
while(1){
printf("main thread :pid:%d ppid:%d\n",getpid(),getppid());
sleep(2);
}
return 0;
}
-L 显示轻量级进程
创建多个线程
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
while(1){
printf("%s : pid : %d,ppid:%d\n",msg,getpid(),getppid());
sleep(1);
}
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char buffer[32];
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
}
//pthread_create(&tid,NULL,Routine,(void*)"thread 1");
//主线程
while(1){
printf("main thread :pid:%d ppid:%d\n",getpid(),getppid());
sleep(2);
}
return 0;
}
pthread_self()
获得自身线程id
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
while(1){
printf("%s : pid : %d,ppid:%d,tid :%lu\n",msg,getpid(),getppid(),pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char buffer[32];
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
printf("%s tid is %lu\n",buffer,tid[i]);
}
//pthread_create(&tid,NULL,Routine,(void*)"thread 1");
//主线程
while(1){
printf("main thread :pid:%d ppid:%d,tid :%lu\n",getpid(),getppid(),pthread_self());
sleep(2);
}
return 0;
}
线程等待
为什么需要线程等待
- 已经退出的进程,其空间没有被释放,仍在进程的地址空间内
- 创建新的进程不会复用刚才退出线程的地址空间
pthread_join()
等待线程结束
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//thread:需要等待线程的线程id
//retval:拿到被等待线程退出时对应的退出码
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
int count = 0;
while(count < 3){
printf("%s : pid : %d,ppid:%d,tid :%lu\n",msg,getpid(),getppid(),pthread_self());
sleep(1);
count++;
}
return (void*)0;
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char *buffer = (char*)malloc(20);
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
printf("%s tid is %lu\n",buffer,tid[i]);
}
//pthread_create(&tid,NULL,Routine,(void*)"thread 1");
//主线程
printf("main thread :pid:%d ppid:%d,tid :%lu\n",getpid(),getppid(),pthread_self());
for(i = 0;i < 5;i++){
pthread_join(tid[i],NULL);
printf("thread :%d[%lu] quit!\n",i,tid[i]);
}
return 0;
}
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
int count = 0;
while(count < 3){
printf("%s : pid : %d,ppid:%d,tid :%lu\n",msg,getpid(),getppid(),pthread_self());
sleep(1);
count++;
}
return (void*)10;
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char *buffer = (char*)malloc(20);
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
printf("%s tid is %lu\n",buffer,tid[i]);
}
//pthread_create(&tid,NULL,Routine,(void*)"thread 1");
//主线程
printf("main thread :pid:%d ppid:%d,tid :%lu\n",getpid(),getppid(),pthread_self());
for(i = 0;i < 5;i++){
void *ret = NULL;
pthread_join(tid[i],&ret);
printf("thread :%d[%lu] quit!,code :%d\n",i,tid[i],(int)ret);
}
return 0;
}
多线程需要考虑异常,但是做不到-》有异常直接退出
调用该函数的线程将挂起等待,直到id为thread的线程终止
- 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值
- 如果thread线程被别的线程调用pthread_cancel异常终止,retval所指向的单元里存放的是常数PTHREAD_CANCELED
- 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数
- retval可为NULL
pthread_t
pthread_t类型的id,本质是一个进程地址空间上的一个地址
线程终止
只讨论正常终止
终止某个线程而不终止整个:
- 从线程函数return
- 调用pthread_exit()终止自己
- 调用pthread_cancel()终止同一进程中的其他线程
pthread_exit()
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
int count = 0;
while(count < 3){
printf("%s : pid : %d,ppid:%d,tid :%lu\n",msg,getpid(),getppid(),pthread_self());
sleep(1);
count++;
}
pthread_exit((void*)11);
// exit(10);//终止进程
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char *buffer = (char*)malloc(20);
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
printf("%s tid is %lu\n",buffer,tid[i]);
}
//主线程
printf("main thread :pid:%d ppid:%d,tid :%lu\n",getpid(),getppid(),pthread_self());
for(i = 0;i < 5;i++){
void *ret = NULL;
pthread_join(tid[i],&ret);
printf("thread :%d[%lu] quit!,code :%d\n",i,tid[i],(int)ret);
}
return 0;
}
pthread_exit或return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了
pthread_cancel()
#include <pthread.h>
int pthread_cancel(pthread_t thread);
1、取消自己线程
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
int count = 0;
while(count < 3){
printf("%s : pid : %d,ppid:%d,tid :%lu\n",msg,getpid(),getppid(),pthread_self());
sleep(1);
count++;
}
int ret = pthread_cancel(pthread_self());//取消自己
printf("pthread_cancel ret:%d\n",ret);//成功返回0
//pthread_exit((void*)11);
// exit(10);//终止进程
//return (void*)10;
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char *buffer = (char*)malloc(20);
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
printf("%s tid is %lu\n",buffer,tid[i]);
}
//pthread_create(&tid,NULL,Routine,(void*)"thread 1");
//主线程
printf("main thread :pid:%d ppid:%d,tid :%lu\n",getpid(),getppid(),pthread_self());
for(i = 0;i < 5;i++){
void *ret = NULL;
pthread_join(tid[i],&ret);
printf("thread :%d[%lu] quit!,code :%d\n",i,tid[i],(int)ret);
}
return 0;
}
2、取消别人线程(一般是主线程取消其他线程)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void *Routine(void *arg)
{
//新线程
char *msg = (char*)arg;
int count = 0;
while(count < 3){
printf("%s : pid : %d,ppid:%d,tid :%lu\n",msg,getpid(),getppid(),pthread_self());
sleep(1);
count++;
}
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char *buffer = (char*)malloc(20);
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
printf("%s tid is %lu\n",buffer,tid[i]);
}
//主线程
pthread_cancel(tid[1]);
printf("main thread :pid:%d ppid:%d,tid :%lu\n",getpid(),getppid(),pthread_self());
for(i = 0;i < 5;i++){
void *ret = NULL;
pthread_join(tid[i],&ret);
printf("thread :%d[%lu] quit!,code :%d\n",i,tid[i],(int)ret);
}
return 0;
}
线程分离
- 默认情况下,新线程是jionable,线程退出后,需要对其进行pthread_join操作,否则将无法释放资源,从而造成系统泄漏
- 如果不关心进程的返回值,可以让系统当线程退出时,自动释放线程资源
pthread_detach()
#include <pthread.h>
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己进行分离
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void *Routine(void *arg)
{
//新线程
pthread_detach(pthread_self());
char *msg = (char*)arg;
int count = 0;
while(count < 3){
printf("%s : pid : %d,ppid:%d,tid :%lu\n",msg,getpid(),getppid(),pthread_self());
sleep(1);
count++;
}
pthread_exit((void*)11);
}
int main()
{
pthread_t tid[5];
int i;
for(i = 0;i < 5 ;i++){
char *buffer = (char*)malloc(20);
sprintf(buffer,"thread %d",i);//格式化控制
pthread_create(&tid[i],NULL,Routine,(void*)buffer);//Routine函数被重入
printf("%s tid is %lu\n",buffer,tid[i]);
}
//主线程
while(1){
printf("main thread :pid:%d ppid:%d,tid :%lu\n",getpid(),getppid(),pthread_self());
sleep(1);
}
return 0;
}
#Linux线程的互斥
进程线程间的互斥相关概念
- 临界资源:多线程执行流共享的资源叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码就叫做临界区
- 互斥:任何时刻互斥保证只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态:完成/未完成
互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量
- 有时,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
- 多个线程并发的操作共享变量,会有问题
多线程临界区问题
eg:抢票
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM 1000
int tickets = NUM;
void * GetTicket(void *arg)
{
int num = (int)arg;
while(1){
if(tickets > 0){
usleep(100);
printf("thread[%d] 抢票:%d\n",num,tickets--);
}
else{
break;
}
}
}
int main()
{
pthread_t thid[5];
int i;
for(i = 0;i < 5;i++){
pthread_create(&thid[i],NULL,GetTicket,(void*)i);
}
for(i = 0;i < 5;i++){
pthread_join(thid[i],NULL);
}
return 0;
}
出现错误!
保证临界区安全需做到:
- 当代码进入临界区执行时,不允许其他线程进入该临界区
- 如果多个线程同时要求执行临界区的代码,并且临界区没有其他线程在执行,那么只允许一个线程进入该临界区
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
要做到这三点,本质需要一把锁🔒。Linux上提供的这把🔒叫做互斥量
互斥量的接口
初始化互斥量
- 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//mutex:要初始化的互斥量
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
调用pthread_mutex_lock可能出现的情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
改进抢票
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM 1000
int tickets = NUM;
pthread_mutex_t lock;
void * GetTicket(void *arg)
{
int num = (int)arg;
while(1){
pthread_mutex_lock(&lock);
if(tickets > 0){
usleep(100);
printf("thread[%d] 抢票:%d\n",num,tickets--);
pthread_mutex_unlock(&lock);
}
else{
pthread_mutex_unlock(&lock);
break;
}
}
}
int main()
{
pthread_t thid[5];
pthread_mutex_init(&lock,NULL);
int i;
for(i = 0;i < 5;i++){
pthread_create(&thid[i],NULL,GetTicket,(void*)i);
}
for(i = 0;i < 5;i++){
pthread_join(thid[i],NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
互斥量实现原理探究
- 为实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和单元内存的数据相交换,由于只有一条指令,保证了原子性,即使是多处理平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令也只能等待总线周期。
锁原子性是如何实现的,lock和unlock具体过程是什么样的?
答:把共享的mutex通过exchange方案,原子性的交换到线程自己的上下文当中
可重入与线程安全
概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现问题
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,称之为重入。一个函数在重入的情况下,运行结果不会出现任何或不同的问题,则该函数被称为可重入函数
常见线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态变化发生的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见线程安全的情况
- 每个线程对全局变量或静态变量只有只读的权限额,而没有写入的权限
- 类或多接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致接口的执行结果存在二义性
常见不可重入函数的情况
- 调用malloc/free函数,因为malloc函数是用全局链表来管理的
- 调用I/O库函数,标准I/O库的有很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用静态的数据结构
常见可重入函数的情况
- 不使用全局变量或静态变量
- 不使用malloc/new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有的数据都由函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入函数与线程安全的联系
- 函数是可重入的,那就是线程安全的
- 线程安全不一定是不可重入的,而可重入函数则一定是线程安全的
- 如果将对临界资源的访问加上锁,则这个函数线程是安全的,但如果这个重入函数锁还未释放则会产生死锁,因此是不可重入的
常见锁
死锁
死锁是指一组进程中的各种进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
达成死锁的条件
- 互斥条件:一个资源只能每次只被一个执行流使用(尽可能少使用锁)
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。多执行流申请多个锁的时候,建议按照顺序申请
破坏死锁
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
避免死锁的算法
- 死锁检测算法
- 银行家算法
Linux线程同步
单纯的加锁有没有问题?
答:有,个别线程,竞争力很强,每次都能申请到锁,但是不办事,又可能导致其他线程长时间竞争不到锁,引起饥饿问题(低效)
为什么要存在同步?
条件变量
用来描述某种临界资源是否就绪的一种数据化描述
- 当一个线程互斥的访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了
- 例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个结点添加到队列中。这种情况需要用到条件变量
同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:因为时序问题,而导致程序异常,称之为竞态条件。
条件变量函数
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//cond:要初始化的条件变量
销毁
int pthread_cond_destroy(pthread_cond_t *cond);
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
//cond:在特定的条件变量下等待
//mutex:等待的时候是在临界区,mutex会自动释放;如果当前线程被唤醒,会自动获得对应的mutex
为什么pthread_cond_wait需要互斥量?
- 条件等待时线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某种操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
- 条件不会突然变得满足,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
案例
#include <iostream>
#include <pthread.h>
#include <cstdio>
pthread_mutex_t lock;
pthread_cond_t cond;
void *Run(void *arg)
{
pthread_detach(pthread_self());//将当前线程相分离
std::cout << (char*)arg << "run!" <<std::endl;
while(true){
pthread_cond_wait(&cond,&lock);//当前线程阻塞在这
std::cout << "thread:"<< pthread_self() << "活动"<< std::endl;
}
}
int main()
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&cond,nullptr);
pthread_t t1,t2,t3;
//pthread_create(&ctrl,nullptr,CtrlRoutinue,nullptr);
pthread_create(&t1,nullptr,Run,(void*)"thread 1");
pthread_create(&t2,nullptr,Run,(void*)"thread 2");
pthread_create(&t3,nullptr,Run,(void*)"thread 3");
//ctrl
while(true){
getchar();//键盘输入(eg:回车)让线程活动
pthread_cond_signal(&cond);//唤醒:在当前条件变量下等待的首个线程
}
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
#include <iostream>
#include <pthread.h>
#include <cstdio>
pthread_mutex_t lock;
pthread_cond_t cond;
void *Run(void *arg)
{
pthread_detach(pthread_self());//将当前线程相分离
std::cout << (char*)arg << "run!" <<std::endl;
while(true){
pthread_cond_wait(&cond,&lock);//当前线程阻塞在这
std::cout << "thread:"<< pthread_self() << "活动"<< std::endl;
}
}
int main()
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&cond,nullptr);
pthread_t t1,t2,t3;
//pthread_create(&ctrl,nullptr,CtrlRoutinue,nullptr);
pthread_create(&t1,nullptr,Run,(void*)"thread 1");
pthread_create(&t2,nullptr,Run,(void*)"thread 2");
pthread_create(&t3,nullptr,Run,(void*)"thread 3");
//ctrl
while(true){
getchar();//键盘输入eg:空格让线程活动
pthread_cond_broadcast(&cond);//将所有线程都唤醒
}
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
生产者消费者模型
生产者消费者模型模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不同通讯,而通过阻塞队列来进行通讯,所以生产者产生完数据后不用等待消费者处理,之间扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列本质就是来给生产者和消费者解耦的
生产者消费者模型的优点
- 解耦
- 支持并发
- 支持忙闲不均
基于BlockingQueue的生产者消费者mox
BlockingQueue
在多线程编程中的阻塞队列(Blocking queue)是一种常用于实现生产者和消费者模型的数据结构。与其普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素也会被阻塞,直到有元素从队列中被取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进操作时会被阻塞)
模拟阻塞队列的生产消费模型
单生产者,单消费者
Makefile
bq:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f bq
#pragma once
#include <unistd.h>
#include <queue>
#include <iostream>
#include <pthread.h>
#define NUM 32
template<typename T>
class BlockQueue
{
/*
private:
void Lock()
{
}
void unLock()
{
}*/
private:
bool IsFull()//是否满了
{
return q.size() == cap;
}
bool IsEmpty()
{
return q.empty();
}
public:
BlockQueue(int _cap = NUM):cap(_cap)
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&full,nullptr);
pthread_cond_init(&empty,nullptr);
}
void Push(const T& in)
{
pthread_mutex_lock(&lock);
while(IsFull()){
// 满了,生产者进行等待
//不能进行生产,等待q可以容纳新的数据
pthread_cond_wait(&full,&lock);//在等待的同时,自动释放该锁
}
q.push(in);
if(q.size() >= cap / 2){
std::cout << "data more! Customers come on!"<<std::endl;
pthread_cond_signal(&empty);
}
pthread_mutex_unlock(&lock);
}
void Pop(T& out)
{
pthread_mutex_lock(&lock);
while(IsEmpty()){
//如果为空,不能进行消费,等待q有新的数据时,才能消费
pthread_cond_wait(&empty,&lock);
}
out = q.front();
q.pop();
if(q.size() <= cap / 2){
std::cout <<"sapce enough! Producter come on!" <<std::endl;
pthread_cond_signal(&full);
}
pthread_mutex_unlock(&lock);
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&full);
pthread_cond_destroy(&empty);
}
private:
std::queue<T> q;//临界资源
int cap;
pthread_mutex_t lock;
pthread_cond_t full;
pthread_cond_t empty;
};
main.cc
#include "Blockqueue.hpp"
#include <pthread.h>
#include <cstdlib>
#include <ctime>
void *Consumer(void *arg)
{
auto bq = (BlockQueue<int>*)arg;
while(true){
int data = 0;
bq->Pop(data);//消费数据
std::cout << "Consumer:" << data << std::endl;
sleep(3);
}
}
void * Producter(void *arg)
{
auto bq = (BlockQueue<int>*)arg;
while(true){
int data = rand() % 100 + 1;
bq->Push(data);//生产数据
std::cout << "Producter" << data << std::endl;
sleep(5);
}
}
int main()
{
srand((unsigned long)time(nullptr));
BlockQueue<int> *bq = new BlockQueue<int>();
pthread_t c,p;
pthread_create(&c,nullptr,Consumer,bq);
pthread_create(&p,nullptr,Producter,bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
基于如上案例实现简单的计算器
BlockQueue.hpp
#pragma once
#include <unistd.h>
#include <queue>
#include <iostream>
#include <pthread.h>
#define NUM 32
template<typename T>
class BlockQueue
{
/*
private:
void Lock()
{
}
void unLock()
{
}*/
private:
bool IsFull()//是否满了
{
return q.size() == cap;
}
bool IsEmpty()
{
return q.empty();
}
public:
BlockQueue(int _cap = NUM):cap(_cap)
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&full,nullptr);
pthread_cond_init(&empty,nullptr);
}
void Push(const T& in)
{
pthread_mutex_lock(&lock);
while(IsFull()){
// 满了,生产者进行等待
//不能进行生产,等待q可以容纳新的数据
pthread_cond_wait(&full,&lock);//在等待的同时,自动释放该锁
}
q.push(in);
if(q.size() >= cap / 2){
std::cout << "data more! Customers come on!"<<std::endl;
pthread_cond_signal(&empty);
}
pthread_mutex_unlock(&lock);
}
void Pop(T& out)
{
pthread_mutex_lock(&lock);
while(IsEmpty()){
//如果为空,不能进行消费,等待q有新的数据时,才能消费
pthread_cond_wait(&empty,&lock);
}
out = q.front();
q.pop();
if(q.size() <= cap / 2){
std::cout <<"sapce enough! Producter come on!" <<std::endl;
pthread_cond_signal(&full);
}
pthread_mutex_unlock(&lock);
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&full);
pthread_cond_destroy(&empty);
}
private:
std::queue<T> q;//临界资源
int cap;
pthread_mutex_t lock;
pthread_cond_t full;
pthread_cond_t empty;
};
Task.hpp
#pragma once
#include <iostream>
class Task{
private:
int x;
int y;
char op;
public:
Task(int _x,int _y,char _op):x(_x),y(_y),op(_op)
{}
Task()
{}
void Run()
{
int result = 0;
switch(op){
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '*':
result = x * y;
break;
case '/':
if(y == 0){
std::cout<<"warning" << std::endl;
result = -1;
}
else{
result = x / y;
}
break;
default:
break;
}
std::cout<< x << op << y <<" = " << result <<std::endl;
}
~Task()
{}
};
main.cc
#include "Blockqueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <cstdlib>
#include <ctime>
void *Consumer(void *arg)
{
auto bq = (BlockQueue<Task>*)arg;
while(true){
sleep(1);
Task t;
bq->Pop(t);//消费数据
t.Run();
//std::cout << "Consumer:" << data << std::endl;
}
}
void * Producter(void *arg)
{
auto bq = (BlockQueue<Task>*)arg;
const char *_op = "+-*/";
while(true){
int x = rand() % 100 + 1;
int y = rand() % 50;
char op = _op[rand()%4];
Task t(x,y,op);
bq->Push(t);//生产数据
// std::cout << "Producter" << data << std::endl;
std::cout<<"product task" << std::endl;
}
}
int main()
{
srand((unsigned long)time(nullptr));
BlockQueue<Task> *bq = new BlockQueue<Task>();
pthread_t c,p;
pthread_create(&c,nullptr,Consumer,bq);
pthread_create(&p,nullptr,Producter,bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
POSIX信号量
信号量本质是一个计数器,描述临界资源中的资源数目的计数器
信号量存在的价值:
1、更细粒度的临界资源的管理
2、同步互斥
POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的。但POSIX可以用于线程间同步
初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
//pshared :0表示线程间共享,非0表示进程间共享
//value: 信号零零初始值
信号量是一个计数器,如果sem值为1-》基本等价于互斥锁
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
等待信号量,会将信号量的值减一
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
实现抢票系统
#include <iostream>
#include <semaphore.h>
#include <pthread.h>
#include <string>
#include <unistd.h>
class Sem{
//信号量封装
private:
sem_t sem;
public:
Sem(int num = 1)
{
sem_init(&sem,0,num);
}
void P()
{
sem_wait(&sem);
}
void V()
{
sem_post(&sem);
}
~Sem()
{
sem_destroy(&sem);
}
};
Sem sem(1);
int tickets = 1000;
void * GetTickets(void * arg)
{
std::string name = (char*) arg;
while(true){
sem.P();//申请
//采用二元信号量(0/1)
if(tickets > 0){
usleep(10000);
std::cout<< name << "get tickets!"<< tickets-- << std::endl;
sem.V();//释放
}
else{
sem.V();//释放
break;
}
}
std::cout << name << "quit!" << std::endl;
pthread_exit((void*)0);//线程退出
}
int main()
{
pthread_t t1,t2,t3;
pthread_t t4,t5,t6;
pthread_create(&t1,nullptr,GetTickets,(void*)"thread 1");//创建线程
pthread_create(&t2,nullptr,GetTickets,(void*)"thread 2");
pthread_create(&t3,nullptr,GetTickets,(void*)"thread 3");
pthread_create(&t4,nullptr,GetTickets,(void*)"thread 4");
pthread_create(&t5,nullptr,GetTickets,(void*)"thread 5");
pthread_create(&t6,nullptr,GetTickets,(void*)"thread 6");
pthread_join(t1,nullptr);//等待线程
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
pthread_join(t5,nullptr);
pthread_join(t6,nullptr);
return 0;
}
基于环形队列的生产消费模型
- 环形队列采用数组模拟,用模运算来模拟环状特性
Ring.hpp
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#define NUM 5
template<typename T>
class Ring{
private:
std::vector<T> q;
int cap;//容量
int c_pos;//生产位置
int p_pos;//消费位置
sem_t blank_sem;
sem_t data_sem;
private:
void P(sem_t &s)
{
sem_wait(&s);
}
void V(sem_t &s)
{
sem_post(&s);
}
public:
Ring(int _cap = NUM):cap(_cap),c_pos(0),p_pos(0)
{
q.resize(cap);
sem_init(&blank_sem,0,cap);//blank资源是cap个
sem_init(&data_sem,0,0);
}
void Push(const T& in)//生产者调用,生产资源,关系blank
{
P(blank_sem);
q[p_pos] = in;
V(data_sem);
p_pos++;
p_pos %= cap;
}
void Pop(T& out)//消费者调用,消费资源,关心data
{
P(data_sem);
out = q[c_pos];
V(blank_sem);
c_pos++;
c_pos %= cap;
}
~Ring()
{
sem_destroy(&blank_sem);
sem_destroy(&data_sem);
}
};
main.cc
#include "Ring.hpp"
#include <stdlib.h>
#include <unistd.h>
void *consume(void *arg)
{
Ring<int> *rq = (Ring<int>*)arg;
while(true){
sleep(2);
int x = 0;
rq->Pop(x);
std::cout<<"Consume ..." << x << std::endl;
}
}
void * product(void * arg)
{
Ring<int> *rq = (Ring<int>*)arg;
while(true){
int x = rand() % 100 + 1;
rq->Push(x);
std::cout << "Product ..." << x <<std::endl;
}
}
int main()
{
srand((unsigned long)time(nullptr));
Ring<int> *rq = new Ring<int>();
pthread_t c,p;
pthread_create(&c,nullptr,consume,rq);
pthread_create(&p,nullptr,product,rq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
线程池线程池
一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这避免了在处理短时间任务时创建于销毁线程的代价。而线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量
线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。
线程池 可以避免大量线程频繁创建或销毁所带来的时间成本,也可以避免在峰值压力下,系统资源耗尽的风险;并且可以统一对线程池中的线程进行管理,调度监控。
线程池的应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短。web服务器完成网页请求这样的任务,使用线程池技术是非常适合的。因为单个任务小,而任务数量巨大。但对于长时间的任务 eg:telnet连接请求,就不适合线程池,因为telnet会话时间比线程的创建时间多。
- 对性能要求苛刻的应用,eg:服务器迅速响应客户要求
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户的请求,在没有线程池情况下,生产大量线程,短时间内生产大量线程可能使内存达到极限,出现错误
线程池批量化处理任务
main.cc
#include "Threadpool.hpp"
#include <cstdlib>
#include <ctime>
#include <unistd.h>
#include "Task.hpp"
int main()
{
Threadpool<Task> *tp = new Threadpool<Task>();
tp->InitThreadpool();
srand((unsigned long)time(nullptr));
const char *op = "+-*/%";
while(true){
int x = rand() % 100 + 1;
int y = rand() % 100 + 1;
Task t(x,y,op[x % 5]);
tp->Push(t);
sleep(1);
}
return 0;
}
Task.hpp
#pragma once
#include <iostream>
#include <pthread.h>
//typedef int(*handler_t)(int,int char);
class Task{
private:
int x;
int y;
char op;
//handler_t handler;
public:
Task()
{}
Task(int _x,int _y,char _op):x(_x),y(_y),op(_op)
{}
void Run()
{
//handler(x,y,op);
int result = 0;
switch(op){
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '*':
result = x * y;
break;
case '/':
if(y == 0){
std::cerr<<"warning "<<std::endl;
}
else{
result = x / y;
}
break;
case '%':
if(y == 0){
std::cerr<< "warning"<<std::endl;
}
else{
result = x % y;
}
break;
default:
std::cout<<"operator error"<<std::endl;
break;
}
std::cout<<"thread:["<< pthread_self() << "]:" << x << op << y << " = " << result<< std::endl;
}
~Task()
{}
};
Threadpool.hpp
#pragma once
#define NUM 5
#include <iostream>
#include <queue>
#include <pthread.h>
template <typename T>
class Threadpool{
private:
int thread_num;
std::queue<T> task_queue;
pthread_mutex_t lock;
pthread_cond_t cond;
public:
Threadpool(int _num = NUM):thread_num(_num)
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&cond,nullptr);
}
void Lockqueue()
{
pthread_mutex_lock(&lock);
}
void Unlockqueue()
{
pthread_mutex_unlock(&lock);
}
bool IsqueueEmpty()
{
return task_queue.size() == 0 ? true : false;
}
void Wait()
{
pthread_cond_wait(&cond,&lock);
}
void Wakeup()
{
pthread_cond_signal(&cond);
}
static void *Routine(void *arg)
{
pthread_detach(pthread_self());//线程分离
Threadpool *self = (Threadpool*)arg;
while(true){
self->Lockqueue();
while(self->IsqueueEmpty()){
//wait
self->Wait();
}
//任务队列有任务
T t;
self->Pop(t);
self->Unlockqueue();
//处理任务
t.Run();
}
}
void Push(const T& in)
{
Lockqueue();
task_queue.push(in);
Unlockqueue();
Wakeup();
}
void Pop(T& out)
{
out = task_queue.front();
task_queue.pop();
}
void InitThreadpool()
{
pthread_t tid;
for(int i = 0;i < thread_num;i++){
pthread_create(&tid,nullptr,Routine,this);//无法访问static 修饰的函数,所以传 this
}
}
~Threadpool()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
};
线程安全的单例模式
单例模式使一种“经典的,常用的”设计模式
设计模式:针对一些经典的常见的场景,给定了一些对应 的解决方案
单例模式的特点
某些类,只应该具有一个对象,就称之为单例。
在很多服务器开发场景中,经常需要让服务器加载很多的数据到内存中,此时往往要用一个单例的类来管理这些数据
饿汉实现方式就懒汉实现方式
吃完饭,立即洗碗:饿汉方式
吃完饭,等到下一顿饭用到碗再洗碗-》懒汉方式
懒汉方式的核心思想是延时加载,从而能够优化服务器的启动速度
STL,智能指针和线程安全
STL的容器是否是线程安全的?
不是,STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁确保线程安全,会对性能造成巨大的影响,而且对于不同的容器,加锁的方式不同,性能也可能不同(eg:hash表的锁表和锁桶)。因此,STL默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全
智能指针是否是线程安全的?
- unique_ptr:只在当前代码块范围内生效,不涉及线程安全问题
- share_ptr:多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是基于原子操作(CAS)的方式保证share_ptr能够高效,原子的操作引用计数
其他常见的锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程更改,所以会在取数据前先加锁,当其他线程想要访问数据时,被阻塞挂起
- 乐观锁:每次取数据时,总是乐观的以为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机和CAS操作
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试
读写问题
多线程情况下,读写同时到来-》写者优先
读写锁
在编写多线程的时候,有些共有数据修改的机会较少。相比较改写,它们读的机会比较多。通常而言,在读的过程中,往往伴随着查找到操作,中间耗时比较长。给这种代码段加锁,会极大的降低效率。读写锁专门处理这种多读少写的情况
三个关系:读和读(没关系),读和写(互斥关系),写和写(互斥/同步关系)
读写锁接口
初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);