Linux环境编程---线程基础

在前面的章节中我们学习到进程,进程是程序的实例,可是对于进程来说进程间通信相对复杂,需要借助管道,消息队列等的帮助,并且进程相对来说占用资源较大,本章提出了线程的概念,线程是系统调用的最小单位,是进程中的一条执行流,同时我们要理解进程是资源调度的最小单位。在Linux下是用进程来模拟线程的一个进程也就是一个线程组,一个进程中所有的线程都可以访问进程的组成部分,如文件描述符和内存等。

线程概念

一个进程在某一个时刻只能干一件事情,有了线程后在程序设计的时候就可以让每个线程干独立的工作,一个进程可能包含多个线程,传统意义上的进程是多线程的一种特例,即进程只包含一个线程。
由于线程共享进程的资源,就会带来很多好处,比喻创建线程花费的时间少于创建进程花费的时间,终止线程花费的时间少于终止进程花费的时间,线程上下文之间切换的开销要小于进程间的上下文切换。线程之间的共享比进程之间共享简单。
不过由于线程可以共享很多临界资源,所以也会让进程变得脆弱,线程间安全就变得尤为重要。多线程的进程因为地址空间的共享让进程变得更加脆弱。线程模型作为一种并发的编程模型,效率没有想象的高,会出现复杂度高,易出错等问题。
对于多线程编程来说,可能会出现的频繁问题大致是死锁,饿死,活锁,静态条件

进程ID与线程ID

在NPTL中,每一个用户态的线程,在内核中都有一个相应的调度实体,也有自己的进程描述符,在没有线程之前,一个进程对应内核的一个进程描述符,对应一个ID但是引入了线程的概念后,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的描述符,进程和内核的进程描述符变成了1对N的关系,内核引入了我线程组的概念。
线程ID有两个说法,一个是用于进程调度范畴的,因为线程是轻量级进程,是操作系统调度的最小单位,所以需要一个数值来唯一标识该线程。这个进程ID用pid_t结构存储,由系统调用gettid查看,同样的该线程在内核中的进程描述符中的pid就是线程的线程ID,进程描述符中的tgid才是进程ID,他代表线程组ID,同时线程组的首线程与线程组ID相同。
当我们创建一个线程的时候,会产生一个线程ID,存放在第一个指向的地址中这个属于NPTL县城库的范畴,线程库的后续操作需要这个线程ID来操作。(其实我理解就是一个是进程ID的作用一个是文件描述符的作用)
我们可以用ps命令来查看进程以及线程信息,-L代表查看线程,LWP是线程ID,NLWP是该线程组的线程个数。
我们还需要了解的是进程与线程不同,进程是具有亲缘关系的,可以有父子进程等很明确的上下级关系,父进程可以fork产生一个子进程,而一个线程组中的所有线程都是平等的,最多只有主线程与其他线程之分,并不是只有主线程才可以创建线程,每一个线程都可以创建线程,并且线程组的所有现场都有一个group_leader指针指向主线程,主线程的指针指向自己。

线程控制

pthread库中有很多用于线程控制的函数,其中包括pthread_create,pthread_exit,pthread_self等,接下来我们将介绍这些接口。

线程的创建

首先先看线程创建的接口,pthread_create函数,在进程中的任意一个线程都可以调用,在开始的时候只有一个线程,是在我们创建进程的时候就存在的一条执行流,我们叫做主线程,主线程的线程ID与进程还有线程组的ID都相同。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
第一个参数是pthread的操作句柄,也就是上面说的另一种线程ID,第二个参数是定制线程的属性,指定栈的大小等,无要求置NULL。第三个参数是入口函数,相当于进程的main函数,最后一个参数是第三个函数参数的参数,要是有多个参数封装起来传结构体。
需要注意的是线程共享了进程的很多资源,比如代码段,比如堆,可是栈是不能共享的,一旦共享了的话多条执行流都会用栈从而导致栈混乱,所以在虚拟地址空间中堆与栈中间的共享映射区中存放线程的动态共享库与线程栈。

线程的退出

线程的正常退出方式有三种,下面三种方式线程会终止但是进程不会。

  1. 创建线程时候的start_routine函数执行了return,并且返回指定值
  2. 线程调用pthread_exit退出,退出的只有当前线程
  3. 其他线程调用了pthread_cancel取消了该线程

void pthread_exit(void *retval);
参数指向的是线程的退出信息,调用pthread_join函数接受这个地址获取推出信息。
需要注意的是我们不能将退出信息存放在线程的栈中,因为当推出后线程栈销毁,我们没办法获取推出信息,所以可以放在堆上。

线程的连接与分离

我们刚刚提到线程退出的时候是有返回值的的, 线程提供了pthread_join来获取线程的返回值,这种操作叫连接,当等待的线程还没有退出,pthread_join调用线程陷入阻塞,要是等待的现场已经推出那么会将线程的推出之存放在第二个参数指针指向的空间里。
其实这和进程的wait很像,不过还是不同,wait只能等待子进程的退出信息,pthread_join阻塞等待同一线程组指定的一个线程。
当然我们要是不关心线程的退出信息我们可以将线程设置为分离状态
int pthread_detach(pthread_t thread);
当将线程设置为分离状态的时候就不关心线程的退出状态。

线程取消

线程可以通过调用pthread_cancel来取消同一个进程中的线程,从编程的角度来说,不建议使用这个接口,因为它实现了一个似是而非的功能却带来了一堆的问题。下面进行具体介绍:

函数取消接口

Linux提供了下面的接口用于线程取消
int pthread_cancel(pthread_t thread);
一个线程可以通过调用该接口向同一个进程的线程发送取消请求,这个不是一个阻塞接口,发送请求后函数立刻返回,不会等待目标线程退出。
线程取消是一种在线程的外部强行终止线程的做法,由于无法预知目标线程的内部情况,会带来很严重的情况。目标线程可能持有互斥量,信号量或其他类型的锁,这时候乳沟收到取消请求,并且取消类型是异步取消,那么可能目标线程掌握的资源没来得及释放就被迫退出了。
即使执行异步取消也安然无恙的函数成为异步取消安全函数,所以对于开发人员来说要遵循下面的原则:

  1. 轻易不要调用pthread_cancel,在外部杀死是很糟糕的做法,线程退出有很多办法
  2. 如果不得不允许线程取消,那么在某些不容有失的代码区域,暂时将线程设置为不可取消状态,退出关键区之后再恢复。
  3. 在非关键区域,也要将线程设置为延迟取消,永远不要设置异步取消。

对于线程取消来说是很危险的,因为可能当前线程有锁,而外部取消之后锁子并没有释放,这样就可能在某个地方造成死锁。对于这种情况,内核提供了取消的清理函数
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
从名字可以看出,这是栈的风格,先入后出,后注册的函数会先执行,看他参数可以发现定义了一个操作函数,如果要清理就进入这个操作函数,那么什么时候触发清理函数呢

  1. 当县城的主函数是调用pthread_exit返回的时候,清理函数总是先被执行
  2. 当线程被其他线程调用pthread_cancel取消的时候,清理函数总是会被执行
  3. 当县城的主函数是通过return返回的时候,并且pop的唯一参数是0,不会被执行
  4. 通过return返回并且pop函数的参数非零,会执行一次

线程与信号

在信号的那篇博客最后已经大体说明白了多线程与信号的关系,在这里总结一下:

  1. 信号处理函数是进程层面的东西,线程组内所有线程共享信号的处理函数
  2. 对于发送给进程的信号,内核会任选一个线程来执行信号处理函数,执行完了后会将其挂起信号队列中去除,其他进程不会对一个信号重复响应
  3. 可以针对进程中谋和线程发送信号,只有该线程可以响应,知行相应的处理函数
  4. 信号掩码是线程层面的定西,信号处理函数是统一的,但是信号掩码是各自独立可配置的,各个线程独立配置自己要阻止或放行的信号集合
  5. 挂起信号即使针对进程又是针对线程,内核维护了两个挂起信号队列,一个是进程共享的挂起信号队列,一个是线程独有的挂起,调用函数sigpending返回的是两者的并集,对于线程来说,优先递送发给自身的信号。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值