Linux线程简述

来源:互联网 发布:h5活动报名系统源码 编辑:程序博客网 时间:2024/05/12 17:28

线程概述

与进程(process)类似,线程(thread)是允许应用程序并发执行任务多个任务的一种机制。一个进程中可以包含多个线程。重点是 同一程序中的所有线程都会执行相同程序,它们共享同一份全局内存区域,其中包括了初始化数据段,未初始化数据段,以及堆内存段。可以这么说,传统UNIX进程只是多线程程序的一个特例,在一个进程中只有一个线程。

线程的优点

在说明线程的优点之前,先说说进程的缺点,毕竟一种新事物的出现,总是为了解决旧事物的一些问题的。以下为进程的一些缺点:

  • 进程间的信息难以共享。这很好理解,进程之间是不同的内存区域的,彼此隔离,若是需要共享信息,需要使用IPC进程间通信。
  • 调用fork()来创建进程的代价相对来说较高,需要复制诸如内存页表,文件描述符表等内容。

以上就是线程需要解决的问题,以下为线程的优点:

  • 线程之间能够方便,快速地共享信息,不过相对地需要考虑线程间竞争的问题,这是同步需要解决的问题
  • 创建线程比创建进程通常要快得多,因为它无需复制各种内容。

Linux调用PThreads API

创建线程

程序启动时,产生的进程只有单条线程,称之为初始或主线程。创建线程的函数如下:

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

以上参数的具体含义请参考函数手册。

值得注意的是,此函数调用后,应用程序无法确定系统接着会调度哪一个线程来使用CPU资源。

终止线程

首先来总结下终止线程的方法:

  • 线程的start函数执行return语句,并返回指定值
  • 线程调用pthread_exit()
  • 调用pthread_cancel()取消线程
  • 任意线程调用了exit(),或者主线程在main函数中执行了return语句,这会导致进程中所有线程都立即终止。

下面来介绍下终止线程函数:

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

此函数的执行相当于在线程的start函数中执行return语句,但是此函数可以在start函数中的任意函数中进行调用。
值得注意的是,若是主线程调用了pthread_exit(),而不是exit()或者执行return语句,那么其他线程将会继续运行。

线程ID

线程id是一个很重要的标识。每一个线程都有唯一的标识,这个标识会通过pthread_create()来返回给调用者,线程使用如下的函数来获取到自己的线程id:

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

线程id是非常有用的,理由如下:

  • 不同的线程函数利用线程id来标识要操作的目标线程。
  • 在一些应用程序中,通过特定的线程的线程id来作为动态数据结构的标签

要检查两个线程id是否相同,需要使用专有函数:

#include <pthread.h>int pthread_equal(pthread_t t1,pthread_t t2);

连接(joining)已终止的线程

此函数的作用类似于进程中的wait函数,此函数可以等待返回线程的终止状态,若是没有进行连接,极有可能会出现僵尸线程。此函数声明如下:

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

线程的分离

默认情况下,线程是可以连接的,换句话说可以使用以上声明的函数来获取线程的终止状态。但是有一种场景,开发者不关心线程的返回状态,只是希望系统在线程终止时能够自动清理并移除。这时候就可以使用以下的函数来进程操作:

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

线程同步

接下来介绍线程的同步,线程同步主要涉及到两个工具

  • 互斥量(mutexe)
  • 条件变量(condition)

其中互斥量可以用来帮助线程同步对共享资源的使用,以防止线程甲试图访问一共享变量的同时,线程乙却试图对该变量进行修改。而条件变量则是用来在线程间相互通知共享变量的状态发生了变化。

之前有提到过,线程的一个主要优势就是能够通过全局变量来共享信息,但是这种便捷是有代价的:

需要确保多个线程不会同时修改同一变量,或者说,某一线程不会读取正由其他线程修改的变量。

这里提下 临界区的概念:这是指某一共享资源的代码片段,且其为原子操作,也就是说同时访问同一共享资源的其他线程不会终端该片段的执行。

互斥量有两种状态:

  • 已锁定(locked)
  • 未锁定(unlocked)

任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将会导致线程阻塞,或者报错。

一旦线程锁定互斥量,随即就成为该互斥量的所有者,只有所有者才能给互斥量解锁。因为所有权的关系,有时候又用获取(acquire),和释放(release)来代替加锁和解锁。

每个线程在访问同一资源时,将遵循如下的协议:

  • 针对共享资源锁定资源
  • 访问共享资源
  • 对互斥量解锁

若是多个线程执行这一代码块,那么执行流程如下图所示:

这里写图片描述

还有一点需要说明的是,互斥锁从来都是一种建议而非强制的,在使用互斥锁时,需要遵循既定的锁定规则。

## PThread API

互斥量可以像静态变量那样分配,也可以在运行时动态创建,首先介绍静态创建:

只需以下代码即可以创建:

#include <pthread.h>pthread_mutex mtx = PTHREAD_MUTEX_INTIALIZER;

加锁解锁互斥量

调用以下函数来对互斥量进行加解锁:

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);

在锁定互斥量之前,需要指定互斥量,这里存在几种情况:

  • 该互斥量处于未锁定状态,那么调用会锁定互斥量,并立即返回
  • 该互斥量处于锁定状态,那么调用会一直阻塞,直至该互斥量被解锁,之后锁定并返回
  • 若是调用程序自身已然将目标互斥量锁定,那么会产生死锁或者是调用失败,这要视情况决定

如果有不止一个线程在等待unlock调用的互斥量,那么无法肯定判断哪个线程会如愿以偿。

互斥量的死锁

通常情况下,一个线程会同时访问两个或者更多不同的共享资源,每个资源都是由不同的互斥量来进行管理的,当超过一个线程加锁同一组互斥量时,就有可能发生死锁。举个例子,如下图:

这里写图片描述

在此图中,线程A锁定了mutex1,试图去获取锁定mutex2,而线程B则是锁定了mutex2,试图去获取锁定mutex1,。这样的话,线程A,B都在等待对方释放互斥量,这样就产生了死锁。

如何避免?

在这里提供以下两种解决方法:

  • 最简单的就是定义互斥量的层级关系,当多个线程对一组互斥量操作时,总是以相同的顺序对该组互斥量竞争锁定,在上面的例子中,若是两个线程总是执行先锁定mutex1,再锁定mutex2,死锁就不会出现。
  • 另一种方法是,”尝试一下,然后恢复“。具体来说,线程先锁定第一个互斥量,然后调用trylock函数来锁定其他的互斥量,如实任一trylock调用失败,那么该线程直接释放其所有的互斥量,然后再依次去获取。这种方法再某种程度上是可以规避死锁的情况。

接下来介绍互斥量的动态初始化

其动态初始化,主要是使用了以下的函数:

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t  *attr);

有创建。相对地就有销毁,其函数调用为:

#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);

值得注意的是,销毁必须要在互斥量处于未锁定状态,且后续也无任何线程企图去锁定它时,才能进行销毁

关于互斥量的类型,可以查看函数手册,不过需要记住以下几点规则:

  • 同一线程不应对同一互斥量加锁两次
  • 线程不应对不为自己所拥有的互斥量解锁
  • 线程不应对尚未锁定的互斥量做解锁动作

通知状态的改变 条件变量

上述的互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待,阻塞于这一通知。

条件变量总是结合互斥量来使用的。条件变量就共享变量的状态改变发出通知,而互斥量则提供对该共享变量访问的互斥。

静态分配的条件变量

其实现函数主要如下:

#include <pthread.h>pthread_cond_t   cond = PTHREAD_COND_INITIALIZER;

通知和等待条件变量

条件变量的主要操作就是发送信号和等待。发送信号操作就是通知一个或者多个处于等待状态的线程,某个共享变量的状态已经改变了。等待操作是指在收到一个通知前一直处于阻塞状态。

如上所述的在代码层面上如下:

#include <pthread.h>int pthread_cond_signal(pthread_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond);int pthread_cond_wait(pthread_cond_t  *cond,pthread_mutex_t *mutex);

条件变量与互斥量之间的关系如此紧密,以至于它们有如下的天然联系:

  • 线程在准备检查共享变量状态时锁定互斥量
  • 检查共享变量的状态
  • 如果共享变量未处于预期状态,线程应在等待条件变量并进入休眠前解锁互斥量
  • 当线程因为条件变量的通知而被再度唤醒时,必须对互斥量再次加锁,因为在通常情况下,线程会立即访问共享变量

既然条件变量具有静态创建,同样拥有动态创建,如下:

#include <pthread.h>int pthread_cond_init(pthread_cond_t *mutex,const pthread_condattr_t  *attr);

同样地,成对出现的销毁操作:

#include <pthread.h>int pthread_cond_destroy(pthread_cond_t *mutex);

线程取消

在通常情况下,程序中的多个线程会并发执行,每个线程各司其职,直至其决意退出,但是也存在着需要取消线程的情况。

其取消函数为;

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

在调用此函数之中,目标线程如何去响应,这取决于其取消状态和类型。如果禁用线程的取消性状态,那么请求会保持挂起状态,直至将线程的取消性状态置为启用,如果启用取消性状态,那么线程何时响应请求则依赖于取消性类型。若类型为延时取消,则在线程下一次调用某个取消点(这个知识点很好理解,线程在执行之后,不可能立即取消掉,为了能确定其取消时间点,设置了取消点),取消发生,若是异步取消,取消动作则随时可能发生。

线程可以设置一个清理函数栈,其中的清理函数由开发人员来自己定义,当函数遭到取消时,会自动调用这些函数以执行清理工作,这里所说的清理工作为恢复共享变量状态,解锁互斥量等等。

原创粉丝点击