多线程 ****
1.线程概念:什么是线程,与进程的关系
进程就是一个运行中的程序,在操作系统中,一个程序运行起来,程序被加载到内存中,
操作系统创建一个进程描述符(进程控制块)PCB 对程序进行描述控制,因此进程就是pcb,
在Linux下就是task_struct结构体
Linux线程用进程pcb模拟,因此Linux的线程是一个轻量级进程,如果说pcb是线程,那么进程
就是线程组,一个进程中至少包含一个或多个线程
因为cpu调度的是pcb,因此线程是进程内部的一个执行流--线程是cpu调度的基本单位
因为进程是线程组-->进程是资源分配的基本单位
线程共享:
进程中所有线程共用一个虚拟地址空间,因此 共享进程 的代码段,数据段
因为线程数据的共享,因此线程之间进行通信将会变简单
文件描述符表
每种信号的处理方式(SIG_IGN、SIG_DFL或自定义信号处理函数)
当前工作目录
用户id和组id
线程独有:
因为每个线程都是pcb,是进程调度的基本单位,因此线程可以同时运行,但是不会造成调用栈
混乱,主要是因为每个线程都有自己的数据
栈
寄存器 (上下文数据)
信号屏蔽字
errno
线程标识符
多进程可以并行处理多任务,多进程也可以处理多任务--优缺点分析
多线程优点:因为多线程共享虚拟地址空间
进程间通信简单
线程创建/销毁成本更低
线程调度成本更低
线程的执行粒度更细
多线程缺点:
线程缺乏访问控制---exit 退出的是整个进程
健壮性较低---线程的某些错误会导致整个进程退出
IO密集型程序:大量的磁盘IO操作程序(频繁进行读写)
cpu密集型程序:大量的数据运算
适用场景: shell例子 处理客户端请求时,创建子进程来完成,而父进程只是接收请求
2.线程控制:线程创建,线程终止,线程等待,线程分离
(1)线程创建:
操作系统没有提供直接创建线程的系统调用接口(用户实现一个
进程的创建非常麻烦),因此实现了一套线程控制接口--封装了 线程库
提供用户使用,因此我们说创建的线程是一个用户态线程,但是在内核
对应有一个轻量级进程实现程序的调度运行
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//thread : 用于保存返回的线程ID
//attr : 线程属性,通常置空
//start_routine: 线程入口函数
//arg: 给线程传入的参数
//返回值: 0 失败:erron
pthread_t tid = pthread_self();
//返回调用线程的线程id
//exit(0); //exit退出的是整个进程
void pthread_exit(void * retval);
pthread_exit();//退出的是线程
// 退出调用线程,并且返回retval
// 线程退出,不仅可以判断终止场景,并且可以获取任务处理结果
ps aux -L 查看线程
线程ID:
task_struct->pid task_struct->tgid tid
LWP PID 线程地址空间首地址
(2)线程终止:
pthread_exit();//退出的是线程
// 退出调用线程,并且返回retval
// 线程退出,不仅可以判断终止场景,并且可以获取任务处理结果
在main函数中return会退出进程
在线程入口函数中return只会退出调用线程
int pthread_cancel(pthread_t thread);
//取消指定线程---让指定线程退出
// -1 PTHREAD_CANCELED
(3)线程等待:
等待指定线程退出--获取指定线程返回值,回收线程资源
线程退出也会形成僵尸线程
线程创建出来之后,默认有一个属性--joinable属性--线程必须被等待
因为线程退出后不会自动回收资源,造成内存泄漏
int pthread_join(pthread_t thread,void**retval);
// thread: 等待线程的ID
//retval: 用户获取线程的退出返回值
char* retval;
pthread_join(tid,(void**)&retval);
(4)线程分离:
分离一个线程(可在任意线程任意时间位置调用,包括分离自己),将线
程的joinable属性修改为detach,处于这个属性的线程,退出后将会自
动回收资源,处于这个属性的线程,不能被等待,否则报错
int pthread_detach(pthread_t thread);
//分离线程,被分离的线程退出后,自动回收资源,不能被等待
pthread_detach(tid);
3.线程安全概念:
线程安全:多个执行流之间对数据竞争操作--不安全
线程安全问题: 因为多个执行流之间对数据竞争操作,有可能造成数据二义性问题
如何实现线程安全: 互斥锁,条件变量,生产者消费者模型,信号量,读写锁,
(1)同步与互斥
同步:保证临界资源访问的时序可控性
互斥:保证临界资源的同一时间的唯一访问性
互斥的实现:
互斥锁:
int pthread_mutex_destory(pthread_mutex_t *mutex);
//pthread_mutex_t mutex 互斥锁变量
pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr)
//互斥锁初始化
//attr 互斥锁变量,通常置NULL
int pthread_mutex_lock(pthread_mutex_t *mutex);
//加锁(加锁之后要在任意可能退出线程的地方都要解锁)
pthread_mutex_lock(&mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//解锁
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
//互斥锁销毁
死锁:
产生场景:锁资源的竞争以及加锁/解锁顺序不当
死锁如何产生的:四个必要条件
互斥条件: 我操作的时候别人不能操作
不可剥夺条件: 我加的锁,别人不能解锁
请求与保持条件: 占用资源的同时申请新的资源,新资源申请不到,老资源也不释放
环路等待条件: 1占着A资源,申请B资源;同时2占着B资源,申请A资源,两边1和2都会进入等待
死锁的预防:
破坏必要条件(除互斥外)----如何破坏
死锁的避免:银行家算法,死锁检测算法
(2) 条件变量: 用于进程间同步--唤醒与等待
条件满足则唤醒等待的进程,条件不满足则等待
pthread_cond_t cond 定义
pthread_cond_init() 初始化
pthread_cond_wait(&cond,&mutex) 等待
pthread_cond_destory() 销毁
pthread_cond_signal() 唤醒至少一个等待的线程
pthread_cond_broadcast(&cond) 唤醒所有等待的进程
条件变量与互斥锁搭配使用:
因为条件变量并不具备操作条件判断的功能,对条件的判断是一个临界资源需要加锁
条件变量的pthread_cond_wait中集合了解锁+休眠+被唤醒后加锁的功能(原子操作)
如果对临界资源操作的线程有多种角色,需要分别等待到不同的条件变量上,分别进行唤醒
这样才不会造成唤醒角色错误的问题
(3) 生产者消费者模型:
多个生产者生产产品,放在队列中
多个消费者消费产品,从队列中获取
向队列中同时放数据或同时从队列中取数据就会出问题
生产者与消费者之间实现安全操作:
生产者与生产者之间是互斥关系
消费者与消费者之间是互斥关系
生产者与消费者之间是同步+互斥关系
实现生产者与消费者模型代码:C++
实现的主要是一个线程安全的队列
生产者生产数据,消费者获取数据处理;实现线程安全的数据操作
三种关系:生产者与生产者,消费者与消费者,生产者与消费者
一个场所,两类角色,三种关系
作用:解耦合,支持忙闲不均,支持并发
(4)posix标准信号量: 具有等待队列的计数器--主要实现线程/进程间的同步与互斥
原理: 可以初始化一个资源计数
当获取资源时,先判断计数;
若>0 ,表示有资源,则计数减一,直接返回,获取资源
若<0 ,表示没有资源,则阻塞等待
当资源产出,计数+1,唤醒等待队列上的进程/线程
通过自身计数的判断+等待+唤醒4
posix标准信号量接口操作步骤:
sem_init() 初始化信号量
sem_wait 计数-1 后小于0 ,则等待;否则直接返回
sem_post 若资源产出,计数+1操作,唤醒等待
sem_destroy 销毁
int sem_init(sem_t *sem,int pshared,unsigned int value);
// sem: 信号量
// pshared:
// 0 用于线程间同步与互斥
// !0 用于进程间同步与互斥
// value: 信号量的计数初值
int sem_wait(sem_t* sem);
// 计数判断,若<=0;则阻塞等待
int sem_trywait(sem_t* sem);
// 计数判断,若<=0;则报错返回
int sem_timedwait(sem_t* sem,struct timespec * abs_timeout)
//计数判断,若<=0;则限时等待
条件变量和信号量实现同步的区别:
条件变量是通过外部条件来判断是否等待;
信号量是内部计数器来判断是否等待
(5)读写锁:
写的时候,其他人即不能读也不能写
读的时候,可以同时读,但是不能写
写互斥,读共享
_write_count 若大于0 ,读锁和写锁都阻塞
_read_count 若大于0,则写锁阻塞,但是读锁可以加
读写锁加锁时的阻塞: 使用自旋锁实现---循环对资源进行判断
cpu消耗较大--自旋锁的使用场景是时间很短的情况
pthread_rwlock_t 读写锁定义
pthread_rwlock_init 读写锁初始化
pthread_rwlock_rdlock() 加读锁
pthread_rwlock_wrlock() 加写锁
pthread_rwlock_unlock 解锁
读写锁通常用于读者多,写者的情况少:这时候想要加写锁修改数据,
但是一直都有线程/进程加读锁;导致写饥饿问题;这是不合适的
因此读写锁是有优先级的:分为写优先和读优先
读写锁默认是读优先
int pthread_relock_setkind_np(pthread_,int pref)
//设置读写锁优先级
线程池:一堆线程(有最大数量上限)+线程安全的任务队列
作用: (1)避免大量线程创建/销毁的时间成本
(2)避免峰值压力,导致线程创建过多,资源耗尽,程序崩溃
(3)解耦合
(4)支持忙闲不均
(5)支持并发
线程池中的线程从任务队列中获取任务,然后进行处理
手撕线程池: 实现线程池类 实现任务类
(1)线程池中有最大数量限制---线程数量
(2)当前线程池中的线程数量
(3)线程安全的任务队列
(4)任务
STL中线程安全: 不安全
智能指针的线程安全:
线程安全的单例模式:
设计模式:针对经典应用场景,设计的解决方案
单例模式:资源只能被加载一次/一个对象只能被实例化一次
单例模式的实现:
饿汉模式:程序启动直接实例化对象(启动慢但运行流畅)
懒汉模式:用的时候才去实例化对象(启动快)
线程安全的单例模式(懒汉模式)
static data
lock
if(data==NULL){data=new[]}
unlock
注意:如果不加锁就不是线程安全的