在一个程序里的一个执行路线为线程,即线程是一个进程内部的控制序列
一切进程至少都有一个执行路线
线程在进程内部运行,本质是在进程地址空间内运行
linux系统中,PCB比传统的进程更加轻量化
在linux下,线程以进程pcb模拟实现,也就是说linux下的pcb实际是一个线程,即linux的线程是一个轻量级进程,
linux下进程实际上是一个线程组-- 包括一个/多个线程
因为CPU调度程序运行时调度pcb,因此线程就是CPU调度的基本单位.
因为一个程序运行起来就会分配大量资源给线程组,因此进程是资源分配的基本单位
同一个进程的线程之间独有的数据
栈 寄存器 errno number 信号屏蔽字 线程ID(线程标识符)
同一个线程之间共享数据
数据段,代码段 文件描述符表 信号处理方式(信号动作) 工作路径 用户ID,组ID
线程的优点(相对于多进程)
- 创建新线程的代价小于创建新进程
- 线程之间的切换需要操作系统的工作少
- 线程占用的资源要比进程少
- 充分利用处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 密集型应用将分解到多个线程中实现
- I/O操作重叠,线程可以同时等待不同的I/O操作
线程的缺点
- 缺乏访问控制,健壮性低,一些系统调用和异常,针对整个进程产生效果
都可以并发/ 并行处理任务,提高处理效率但是同时对临界资源操作需要考虑较多.
CPU密集程序: 程序中都是大量的预算操作
IO密集程序: 程序中都是大量的IO操作
多进程场景: 对主进程安全度要求比较高的程序–shell ;
多进程任务处理: 将多个任务分解成多个程序(分解到多个进程中完成)
多线程同时处理任务( 并行 / 并发) | 多线程同时处理任务( 并发 / 并行 )
线程控制
线程创建:
Pathread_creat
功能:创建一个新的线程
原型
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;失败返回错误码
每一个进程都是一个pcb – task_struct 结构中都有一个pid, 但是用户使用ps命令查看进程的时候只有一个进程,也只有进程 pid,
LWP : task_struct ->pid
PID : task_struct -> tpid等于线程中的主线程pid
tid : 首地址
线程终止
- 线程入后函数中return ; main函数中return退出的是整个进程
- pthread_exit 退出调用线程
- pehread_cancel 取消一个指定的线程
主线程退出, 进程并不会退出
线程退出也会形成僵尸进程,因为线程退出也要保存自己的退出返回值
线程等待
获取指定退出线程的返回值, 并且允许操作系统回收线程资源
一个线程启动之后, 默认有一个属性是线程处于joinable状态
处于joinable状态的线程,退出后,不会自动释放资源; 需要被其他线程等待
线程默认的属性是joinable
一个线程被取消,返回值是多少?
(1)一个线程可以调用pthread_cancel来取消另一个线程。
(2)被取消的线程需要被join来释放资源。
(3)被取消的线程的返回值为PTHREAD_CANCELED
线程安全
多个线程同时操作临界资源而不会出现数据二义性
在线程中是否会面临资源进行了非原子操作
可重入 / 不可重入 : 多个执行流是否可以同时进入函数运行而不会出现问题
如何实现线程安全:
同步: 临界资源的合理访问
互斥: 临界资源同一时间唯一访问
互斥如何实现:
互斥锁: 一个1/0的计数器
1标识完成加锁,加锁就是计数-1;
操作完毕后要解锁, 解锁就是计数+1
0表示不可以加锁, 不能加锁则等待
互斥锁的操作步骤:
- 定义互斥锁变量 pthread_mutex_t
- 初始化互斥锁变量 pthread_mutex_init
- 加锁 pthread_mutex_lock
- 解锁 pthread_mutex_unlock
- 销毁互斥锁 pthread_mutex_destory
死锁:
产生: 对所资源的竞争以及进程/线程加锁的推进顺序不当
因为对一些无法加锁的锁进行加锁而导致程序卡死
死锁产生的四个必要条件:
- 互斥条件(我能操作别人不能操作)
- 不可剥夺操作(我的锁,别人不能解)
- 请求与保持条件(拿着碗里的,看着锅里的)
- 环路等待条件
避免死锁: 破坏必要条件
死锁处理:死锁检测算法 ,银行家算法
同步的实现: 临界资源访问合理性–生产出来才能使用–等待 + 唤醒
没有资源则等待( 死等) , 生产资源则等待
条件变量:
- 定义条件变量 pthread_cond_t
- 初始化条件变量 pthread_cond_init
- 等待 / 唤醒 pthread_cond_wait / pthread_cond_signal
- 销毁条件变量 pthread_cond_destroy
条件变量为什么要搭配互斥锁使用?
因为条件变量本身只提供等待与唤醒的功能,具体要什么时候等待需要用户来进行判断.这个条件的判断,通常涉及临界资源的操作(其他线程要通过修改条件,来促使条件满足), 而这个临界资源的操作应该受到保护.因此要搭配互斥锁一起使用.
加锁:
条件判断 – 不满足
解锁 pthread_mutex_nulock
等待 pthread_cond_wait
1.解锁 -> 2.休眠 -. 3.被唤醒后加锁
生产者与消费者模型
保证生产者与消费者的线程安全:
生产者与消费者之间应该具有互斥关系
消费者与消费者之间应该具有互斥关系
生产者与消费者之间应该具有同步 + 互斥关系
一个场所,两种角色,三种关系
信号量
计数器 + 等待队列 + 等待 + 唤醒
功能 : 实现线程 / 进程间的同步与互斥
计数器就是判断的条件 – 当计数只有0 / 1的时候那么就可以实现互斥
等待队列 + 等待 + 唤醒实现同步的基本功能
system V 信号量:
信号量原语 : P (-1 + 阻塞) / V ( +1 +唤醒) 操作
posix信号量:
定义:sem_t 信号量变量 第二个参数为0则控制线程
初始化 : sem_init
数据操作前资源计数判断 : sm_wait / timewait
计数 > 0 则计数-1,直接返回往下操作
计数 <= 0,则计数 +1, 阻塞等待
生产数据后则计数 +1 .
唤醒等待 : sem_post
销毁 : sem_destory
信号量与条件变量的区别:
信号量拥有资源计数的功能 ; 临界资源是否能够操作; 通过自身计数判断
条件变量是搭配互斥锁一起使用
信号量还可以实现互斥,计数仅为 0 / 1
RingQueue{
Std::vector<int> _queue(10)
Int _write_step;
Int _read_setp;
Sem_t_sem_data;//数据计数
Sem_t_sem_idle;//空闲空间计数
Sem_t_sem_lock
}
线程池
线程池维护着多个线程,等待分配可并发执行的任务
一堆固定数量 / 有最大数量限制的线程 + 任务队列 – 用于并发处理任务请求
线程池避免了频繁的线程创建销毁的时间成本
线程池避免峰值压力带来瞬间大量线程被创建资源耗尽, 程序崩溃的危险
应用场景
1.对性能要求苛刻,比如服务器对客户端的响应
2.接受突发性的大量请求, 因为短时间产生大量线程可能使内存达到极限, 出现错误
3.需要大量的线程来完成任务,且完成任务的事件比较短 . web服务器完成网页
线程安全的单利模式
单利模式是一种设计模式 : 一个对象只能被实例化一次
饿汉单利模式:
程序初始化时进行实例化,资源已经全部加载,因此运行速度快,流畅;缺点,初始化耗时较长
懒汉单利模式:
程序资源使用的时候进行加载,对象使用的时候再实例化,但是流畅度不够
懒汉模式需要注意线程安全问题