线程

发布时间 2023-09-13 08:54:16作者: 常羲和

线程

1. 线程的概念

每个进程都拥有自己的数据段、代码段和堆栈段,这就造成进程在进行创建、切换、撤销操作时,需要较大的系统开销。

为了减少(多进程)的系统开销,从进程中演化出了线程

线程存在于进程中,共享进程的资源

线程是进程中的独立控制流,由环境(包括寄存器组合程序计数器)和一系列的执行指令组成。

每个进程有一个地址空间和一个控制线程(主线程)

image-20230912144442109

线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。查看指定进程的LWP号:ps -Lf pid

线程和进程的比较

1)调度:
线程是CPU调度和分派的基本单位
进程是系统中程序执行和资源分配的基本单位
2)拥有资源:
线程自己一般不拥有资源(除了必不可少的程序计数器,一组寄存器和栈),但它可以去访问其所属进程的资源,如进程代码段,数据段以及系统资源(已打开的文件,I/O设备等)
3)系统开销:
同一个进程中的多个线程可共享同一地址空间,因此它们之间的同步和通信的实现也变得比较容易。
在进程切换时候,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置;而线程切换只需要保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作,从而能更有效地使用系统资源和提高系统的吞吐量。
4)并发性:
不仅进程间可以并发执行,而且在一个进程中的多个线程之间也可以并发执行。

2. 多线程优势

线程共享的资源

1)文件描述符表
2)每种信号的处理方式
3)当前工作目录
4)用户ID和组ID

线程非共享的

1)线程id
2)处理器现场和栈指针(内核栈)
3)独立的栈空间(用户空间栈)
4)errno变量
5)信号屏蔽字
6)调度优先级

线程的优缺点

优点:

1.提高程序并发性
2.开销小
3.数据通信、共享数据方便

缺点:

1.库函数,不稳定
2.调试、编写困难、gdb不支持
3.对信号支持不好 优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大

使用多线程的目的:

1)多任务程序的设计
2)并发程序设计
3)网络程序设计
4)数据共享

在多cpu系统中,实现真正的并行

3. 线程的创建

程序中一旦使用线程函数 必须加上线程库-lpthread

3.1 线程号

线程号只在它所属的进程环境中有效

线程号则用pthread_t数据类型来表示,Linux使用无符号长整数表示。

获取线程号

#include <pthread.h>
pthread_t pthread_self(void);

3.2 创建线程函数

#include <pthread.h>
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void*(*start_routine)(void*),void *arg);

参数:

thread:线程标识符地址。
attr:线程属性结构体地址,通常设置为NULL
start_routine:线程函数的入口地址
arg:传给线程函数的参数。

返回值:成功,0;失败,非0

与fork不同的是pthread_create创建的线程不与父线程在同一点开始运行,而是从指定的函数开始运行,该函数运行完后,该线程也就退出了。线程依赖进程存在的,如果创建线程的进程结束了,线程也就结束了。

子线程的执行由CPU调度,不确定顺序。

4. 线程等待

等待子线程结束,并回收子线程资源

如果等待的线程不结束,那么pthread_join阻塞

#include <pthread.h>
int pthread_join(pthread_t thread,void **retval);

功能:等待线程结束(此函数会阻塞),并回收线程资源,类似进程的wait()函数,如果线程已经结束,那么该函数会立即返回

参数

thread:被等待的线程号。

retval:用来存储线程退出状态的指针的地址

返回值:成功0,失败非0

5. 线程分离

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态位置。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它所占用的所有资源,而不保留终止状态。

不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误

#include <pthread.h>
int pthread_detach(pthread_t thread);

功能:

使调用线程与当前进程分离,分离后不代表此线程不依赖于当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。

返回值:成功0,失败非0

6. 线程的退出

在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流(子线程)

1.线程从执行函数中返回
2.线程调用pthread_exit退出线程
3.线程可以被同一进程中的其它线程取消

6.1 线程退出函数

#include <pthread.h>
void pthread_exit(void *retval);

retval:存储线程退出状态的指针

一个进程的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放

6.2 线程的取消

取消线程是指取消一个正在执行线程的操作

#include <pthread.h>
int pthread_cancel(pthread_t thread);

成功返回0,失败返回出错编号。

pthread_cancel函数的实质是发信号给目标线程thread,使目标线程退出

线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。类似于玩游戏存档,必须到达指定的场所(存档点)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。取消点:线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write...执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。可粗略认为一个系统调用(进入内核)即为一个取消点。

6.3 线程退出清理

和进程的退出清理一样,线程也可以注册它退出时要调用的函数,这样的函数称为线程清理处理程序(pthread cleanup handler).

【注意】线程可以建立多个清理处理程序。处理程序在栈中,故它们的执行顺序与它们注册时的顺序相反

注册清理函数:

将清除函数压栈,即注册清理函数

#include <pthread.h>
void pthread_cleanup_push(void(*routine)(void*),void *arg);

参数:

routine:线程清理函数的指针
arg:传给线程清理函数的参数

弹出清理函数

将清除函数弹栈,即删除清理函数

#include <pthread.h>
void pthread_cleanup_pop(int execute);

参数:

execute:线程清理函数执行标志位
非0,弹出清理函数,执行清理函数。
0,弹出清理函数,不执行清理函数

当线程执行以下动作时会调用清理函数:

1.调用pthread_exit 退出线程
2.响应其它线程的取消请求。
3.用非零execute调用pthread_cleanuo_pop。

无论哪种情况pthread_cleanup_pop都将删除上一次pthread_cleanup_push调用注册的清理处理函数

7. 线程的属性

Linux下线程的属性是可以根据实际项目需要,进行设置。

7.1 线程属性结构体

typedef struct
{
    int detachstate;//线程的分离状态
    int schedpolicy;//线程调度策略
    struct sched_param schedparam;//线程的调度参数
    int inheritsched;//线程的继承性
    int scope;//线程的作用域
    size_t guardsize;//线程栈末尾的警戒缓冲区大小
    int stackaddr_set;//线程的栈设置
    int *stacksize;//线程栈的位置
    size_t stacksize;//线程栈的大小
}pthread_attr_t;

线程属性主要包括如下:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级

主要结构体成员

1.线程分离状态

2.线程栈大小(默认平均分配)

3.线程栈警戒缓冲区大小(位于栈末尾)

4.线程栈最低地址属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create之前调用。之后须用pthread_attr_destory函数来释放资源

7.2 线程属性初始化及销毁函数

//线程属性初始化
int pthread_attr_init(pthread_attr_t *attr);
函数返回值:成功:0;失败:错误号

//线程属性资源销毁
int pthread_attr_destroy(pthread_attr_t *attr);
函数返回值:成功:0;失败:错误号

7.3 通过属性进行线程分离

先设置线程的分离属性、再去创建线程

int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);
获取属性,分离or非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate);
参数:
   attr:已初始化的线程属性
   detachstate:
       分离状态PTHREAD_CREATE_DETACHED(分离线程)
       PTHREAD_CREATE_JOINABLE(非分离线程)

7.4 设置线程的栈空间

//设置栈的地址
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize);
成功:0;失败:错误号

//得到栈的地址
int pthread_attr_getstack(pthread_attr_t *attr,void **stackaddr,size_t *stacksize);
成功:0;失败:错误号
参数:
    attr:指向一个线程属性的指针
    stackaddr:返回获取的栈地址
    stacksize:返回获取的栈大小

//设置线程所使用的栈空间大小
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);
成功:0;失败:错误号
//得到线程所使用的栈空间大小
int pthread_attr_getstacksize(pthread_attr_t *attr,size_t *stacksize);
成功:0;失败:错误号

参数:
    attr:指向一个线程属性的指针
    stacksize:返回线程的堆栈大小

7.5 设置线程的优先级

通过struct sched_param结构体设置

struct sched_param{
    int __sched_priority;//优先级
};

它的取值范围取决于操作系统和调度策略

在Linux系统中,常用的调度策略是基于优先级的实时调度策略(SCHED_FIFOSCHED_RR(Round-Robin Scheduling,轮询调度))。在这些调度策略下,sched_priority的取值范围通常是1到99,其中1表示最低优先级,99表示最高优先级

核心函数:

pthread_attr_getschedparam(pthread_attr_t *attr,sched_param *param);
pthread_attr_setschedparam(pthread_attr_t *attr,sched_param *param);