前言
本节为大家介绍多线程。
一,线程
1.1 什么是线程
-
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列” -
一切进程至少都有一个执行线程 -
线程在进程内部运行,本质是在进程地址空间内运行 -
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化 -
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程
执行流
1.2 线程的优点
-
创建一个新线程的代价要比创建一个新进程小得多 -
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多 -
线程占用的资源要比进程少很多 -
能充分利用多处理器的可并行数量 -
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务 -
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 -
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.3 线程的缺点
性能损失
健壮性降低
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
1.4 线程异常
进程内的所有线程也就随即退出
1.5 线程用途
多线程运行的一种表现)
二,进程与线程对比
-
进程是资源分配的基本单位 -
线程是调度的基本单位 -
线程共享进程数据,但也拥有自己的一部分数据: 线程ID 硬件上下文 一组寄存器 私有栈 errno 信号屏蔽字 调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
三,Linux线程控制
3.1 POSIX线程库
-
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的 -
要使用这些函数库,要通过引入头文<pthread.h> -
链接这些线程函数库时要使用编译器命令的“-lpthread”选项
功能:创建一个新的线程
原型
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;失败返回错误码
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返 回值返回。
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通 过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。
3.2 进程ID和线程ID
-
在Linux中,线程又被称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。 -
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况 发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的 进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID。为了解决此问题linux引入了线程组的概念。
ps命令中的-L选项,会显示如下信息:
- LWP:线程ID,既gettid()系统调用的返回值
- NLWP:线程组内线程的个数
注:强调一点,线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系
3.3 线程终止
终止某个线程而不终止某个进程,可以有三种方法:
- 1.从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 2.线程可以调用pthread_ exit终止自己。
- 3.一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数 value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数 thread:线程ID
返回值:成功返回0;失败返回错误码
3.4 线程等待
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
- 创建新的线程不会复用刚才退出线程的地址空间,会出现类似于僵尸进程的现象
功能:等待线程结束
原型 int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
3.5 分离线程
int pthread_detach(pthread_t thread);
四,linux线程互斥
进程线程间的互斥相关背景概念
-
临界资源:多线程执行流共享的资源就叫做临界资源 -
临界区:每个线程内部,访问临界自娱的代码,就叫做临界区 -
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用 -
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完 成
互斥量mutex
要解决问题,需要做到三点:
-
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区 -
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临 界区。 -
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
4.1 互斥量的接口
4.1.1 初始化互斥量
初始化互斥量有两种方法:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
4.1.2 销毁互斥量
销毁互斥量要注意:
- 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用pthread_ lock 时,可能会遇到以下情况:
-
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功 -
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
4.2 可重入与线程安全
-
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。 -
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
4.3 常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
4.4 常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
4.5 常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
4.6 可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
4.7 可重入与线程安全区别
五,死锁
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
破坏死锁的必要条件
避免死锁算法
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用
于线程间同步。
六,线程池
概念
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个
线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不
仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内
存、网络sockets等的数量。
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使
用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于
长时间的任务,比如一个远程登陆连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大
多了。
-
对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。 -
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没
有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程
可能使内存到达极限,出现错误.
线程池的种类:
线程池示例:
-
创建固定数量线程池,循环从任务队列中获取任务对象, -
获取到任务对象后,执行任务对象中的任务接口
总结
以上是本文内容,希望大家有所收获。
|