时间是一个伟大的作者,它会给每个人写出完美的结局来!
9. Linux多线程Thread
9.1 线程初始
9.1.1 线程的基本概念
(1)线程是一个执行流,用于运行代码以及处理数据;
(2)Linux下的线程:用户态线程+轻量级进程;
用户态线程:使用库函数实现创建的线程,这个用户态线程在内核中使用一个轻量级进程实现调度;
9.1.2 线程与进程
- 线程
(1)Linux下线程是CPU调度的基本单位——因为Linux是线程; - 进程
(1)Linux下进程是资源分配的基本单位——因为程序运行时资源势分配给整个线程组(进程)的;
(2)传统操作系统中使用pcb来描述一个程序的运行——pcb就是进程,LInux下使用pcb来模拟实现线程,因此Linux下的pcb实际上是一个轻量级进程LWP(轻量级进程是因为公用大部分进程资源,相较于传统进程更加轻量化)。
9.1.3 线程的优缺点
- 线程的优点
(1)创建一个新线程的代价要比创建一个新进程小得多;
(2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多;
(3)线程占用的资源要比进程少很多;
(4)能充分利用多处理器的可并行数量;
(5)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务;
(6)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现;
(7)I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。 - 线程的缺点
(1)性能损失(增加了额外的同步和调度开销,而可用的资源不变);
(2)健壮性降低(线程之间是缺乏保护的);
(3)缺乏访问控制(进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个
进程造成影响);
(4)编程难度提高;
(5)编写与调试一个多线程程序比单线程程序困难得多。
9.1.4 线程之间资源的独有与共享
- 独有
栈、寄存器、、信号屏蔽字、errno、进程ID、调度优先级; - 共享
共享虚拟地址空间、文件描述符表、信号处理方式、当前的工作路径、用户ID、组ID。
9.2 线程控制
9.2.1 线程创建
(1)功能
创建一个新的线程
(2)原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(start_routine)(void), void *arg);
(3)参数
thread: 返回线程ID
attr: 设置线程的属性,attr为NULL表示使用默认属性
start_routine: 是个函数地址,线程启动后要执行的函数
arg: 传给线程启动函数的参数
(4)返回值
成功返回0;失败返回错误码。
9.2.2 线程终止
1. pthread_exit
(1)功能
线程终止;
(2)原型
void pthread_exit(void *value_ptr);
(3)参数
value_ptr:value_ptr不要指向一个局部变量;
(4)返回值
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
2. pthread_cancel
(1)功能
取消一个执行中的线程;
(2)原型
int pthread_cancel(pthread_t thread);
(3)参数
thread:线程ID
(4)返回值
返回值:成功返回0;失败返回错误码。
3. 线程终止的三种方法
- 从线程函数return;这种方法对主线程不适用,从main函数return相当于调用exit;
- 线程可以调用pthread_exit终止自己;
- 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
- 线程退出之后,默认不会自动释放资源(保存自己的退出结果在线程的地址空间中),会造成资源泄露;
- 主线程退出之后,其他线程依然可以正常运行。
9.2.3 线程等待
(1)功能
阻塞等待指定线程退出,通过retval获取返回值;
(2)原型
int pthread_join(pthread_t thread, void **value_ptr);
(3)参数
thread:线程ID;
value_ptr:它指向一个指针,后者指向线程的返回值;
(4)返回值
成功返回0;失败返回错误码。
(5)pthread线程一不同的方法终止,通过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参数。
(6)说明
一个线程创建出来之后,默认有一个joinable属性,处于joinable属性的线程退出之后,不会自动释放资源,需要被其他线程等待,才能释放资源;处于joinable属性的线程必须等待,否则造成资源泄露。
9.2.4 线程分离
(1)功能
将线程joinable属性修改为detach属性
(2)原型
int pthread_detach(pthread_t thread);
(3)说明
- 线程若属于detach属性,则线程将退出后将自动回收资源;
- 这个线程将不再需要等待(因为线程退出,返回值占用的空间已经被回收了);
(4)应用场景
可以在任意线程中实现(对线程的返回值并不关心)。
9.2.5 说明
(1)Linux下操作系统并没有提供线程的控制系统调用接口;
(2)因此大佬们封装了一个线程控制接口库;
(3)Linux的线程:用户态线程+轻量级进程。
(4)要使用这些函数库,要通过引入头文<pthread.h>;
(5)链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
9.2.6 线程中ID的讨论
(1)tid——线程地址空间的首地址;
(2)pcb——pid(轻量级进程Id,LWP);
(3)pcb——tgid(线程、进程组id,默认等于首线程id)。
9.3 线程安全
9.3.1 概念
线程安全 : 多个线程同时对临界资源进行访问而不会造成数据二义性.
重入 : 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入.
可重入函数 : 一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题;
9.3.2 线程安全的实现
(1) 同步
- 概念
保证对临界资源访问的时序合理性 - 实现 (等待+唤醒)
条件变量
线程在对临界资源进行访问之前, 首先判断是否能够操作,若可以操作则线程直接操作; 若不可以操作, 则条件变量提供等待功能, 让pcb等待在等待队列上, 其他线程促使操作条件满足, 然后唤醒条件变量等待队列上的线程.
条件变量 = 等待 + 唤醒 +等待队列
信号量
等待与唤醒的功能,不具备操作条件的时候则等待,其它线程促使条件满足后,唤醒等待的线程。
(2) 互斥
-
概念
保证对临界资源同一时间访问的安全性(唯一性). -
实现 (互斥锁)
互斥锁 -
原理
通过对一个只有0/1的计数器的原子判断
死锁 -
概念
指在一组进程中, 多个线程对多个锁资源进行争抢操作,但是因为推进
顺序不当,造成环路等待导致程序流程无法推进. -
产生的必要条件
- 互斥条件
一个资源每次只能被一个执行流使用 - 不可剥夺条件
一个执行流已获得的资源,在末使用完之前,不能强行剥夺 - 请求与保持条件
一个执行流因请求资源而阻塞时,对已获得的资源保持不放 - 环路等待条件
若干执行流之间形成一种头尾相接的循环等待资源的关系
预防死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
避免死锁算法
银行家算法
死锁检测算法 - 互斥条件
信号量
- 具体实现
1. 定义互斥锁变量
pthread_mutex_t *mutex;
2. 对互斥锁变量进行初始化
静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态分配
int pthread_mutex_init(pthread_mutex_t restrict mutex,
const pthread_mutexattr_t restrict attr);
参数:mutex:要初始化的互斥量
attr:NULL
3. 对临界资源操作之前先加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
4. 对临界资源操作完毕后进行解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
5. 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用PTHREAD MUTEX_ INITIALIZER初始化的互斥量不需要销毁;
不要销毁一个已经加锁的互斥量;
已经销毁的互斥量,要确保后面不会有线程再尝试加锁;
9.3.3 生产者与消费者模型(321)
- 生产者消费模型优点
- 解耦和
缓冲区作为中间过渡作用 - 支持忙闲不均
缓存数据, 缓慢处理 - 支持并发
多个生产者可同时向里面放置数据
- 实现
- 一个场所
缓冲区 (需要保证安全) - 两类角色
生产者, 消费者 - 三种关系
生产则与消费者之间具有同步与互斥关系;
生产者与生产者之间具有互斥关系;
消费者与消费者之间具有互斥关系;
9.4 线程池
9.4.1 概念
(1) 一堆线程+任务队列 (缓冲任务)
(2) 在程序初始化时, 创建固定数量的线程 (最大数量限制),从任务队列中获取任务, 进行处理.
(3) 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池
维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
9.4.2 作用
(1) 避免为大量请求创建线程, 导致资源耗尽程序奔溃的问题;
(2) 避免大量线程频繁创建以及销毁所带来的时间成本;
9.4.3 实现
线程创建 + 线程安全的任务队列实现
9.4.4 线程池的销毁条件
所有线程退出之后
9.4.4 线程池的应用场景
(1) 需要大量的线程来完成任务,且完成任务的时间比较短。WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了,因为Telnet会话时间比线程的创建时间大多了。
(2) 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
(3) 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请
求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
9.5 线程安全的单例模式
9.5.1 设计模式
(1) 定义
大佬们对典型的应用场景设计出的解决方案
(2) 分类
(3) 使用场景
9.5.2 单例模式
是设计模式中的一种常见模式, 一个对象只能被实例化一次 (一个资源只能被加载一次).
9.5.3 实现
饿汉模式
懒汉模式
资源在使用的时候才加载, 在多个执行流中就可能被加载多次, 因此资源的加载/对象的实例化过程需要保护,线程安全的单例模式主要针对的是懒汉方式的实现.
非线程安全+volatile
- 实现线程安全
class info{
static T* _data;
T* get_instance(){
lock();
if(NULL == _data){
_data = new T();
}
unlock();
return _data;
}
}
避免锁冲突进行二次判断
9.6 常见的锁
9.6.1 常见的锁的种类
(1) 悲观锁
在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
(2) 乐观锁
每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据
前,会判断其他数据在更新前有没有对数据进行修改。
(3) 互斥锁
(4) 自旋锁
(5) 读写锁