linux多线程

Posted 银背欧尼酱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux多线程相关的知识,希望对你有一定的参考价值。


前言

本节为大家介绍多线程。


一,线程

1.1 什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”

  • 一切进程至少都有一个执行线程

  • 线程在进程内部运行,本质是在进程地址空间内运行

  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化

  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程

执行流

1.2 线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多

  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • 线程占用的资源要比进程少很多

  • 能充分利用多处理器的可并行数量

  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

1.3 线程的缺点

性能损失

  • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型

    线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的

    同步和调度开销,而可用的资源不变。

健壮性降低

  • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了

    不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

缺乏访问控制

  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

编程难度提高

  • 调试一个多线程程序比单线程程序困难得多

1.4 线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该

进程内的所有线程也就随即退出

1.5 线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率

  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是

多线程运行的一种表现)

二,进程与线程对比

  • 进程是资源分配的基本单位

  • 线程是调度的基本单位

  • 线程共享进程数据,但也拥有自己的一部分数据:

    线程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 分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资

    源,从而造成系统泄漏。

  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程

    资源。

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等的数量。

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使

用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于

长时间的任务,比如一个远程登陆连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大

多了。

  1. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

  2. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没

有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程

可能使内存到达极限,出现错误.

线程池的种类:

线程池示例:

  1. 创建固定数量线程池,循环从任务队列中获取任务对象,

  2. 获取到任务对象后,执行任务对象中的任务接口

总结

以上是本文内容,希望大家有所收获。

以上是关于linux多线程的主要内容,如果未能解决你的问题,请参考以下文章

线程学习知识点总结

多个请求是多线程吗

多线程

python小白学习记录 多线程爬取ts片段

LINUX操作系统知识:进程与线程详解

多线程编程