与进程(process)类似,线程(thread)是允许应用程序并发执行任务多个任务的一种机制。一个进程中可以包含多个线程。重点是 同一程序中的所有线程都会执行相同程序,它们共享同一份全局内存区域,其中包括了初始化数据段,未初始化数据段,以及堆内存段。可以这么说,传统UNIX进程只是多线程程序的一个特例,在一个进程中只有一个线程。
在说明线程的优点之前,先说说进程的缺点,毕竟一种新事物的出现,总是为了解决旧事物的一些问题的。以下为进程的一些缺点:
以上就是线程需要解决的问题,以下为线程的优点:
程序启动时,产生的进程只有单条线程,称之为初始或主线程。创建线程的函数如下:
#include
int pthread_create(pthread_t *thread,const pthread_attr *attr,void*(*start)(void*),void *arg);
以上参数的具体含义请参考函数手册。
值得注意的是,此函数调用后,应用程序无法确定系统接着会调度哪一个线程来使用CPU资源。
首先来总结下终止线程的方法:
下面来介绍下终止线程函数:
#include
int pthread_exit(void *retval);
此函数的执行相当于在线程的start函数中执行return语句,但是此函数可以在start函数中的任意函数中进行调用。
值得注意的是,若是主线程调用了pthread_exit(),而不是exit()或者执行return语句,那么其他线程将会继续运行。
线程id是一个很重要的标识。每一个线程都有唯一的标识,这个标识会通过pthread_create()来返回给调用者,线程使用如下的函数来获取到自己的线程id:
#include
int pthread_self(void);
线程id是非常有用的,理由如下:
要检查两个线程id是否相同,需要使用专有函数:
#include
int pthread_equal(pthread_t t1,pthread_t t2);
此函数的作用类似于进程中的wait函数,此函数可以等待返回线程的终止状态,若是没有进行连接,极有可能会出现僵尸线程。此函数声明如下:
#include
int pthread_self(pthread_t thread,void **retval);
默认情况下,线程是可以连接的,换句话说可以使用以上声明的函数来获取线程的终止状态。但是有一种场景,开发者不关心线程的返回状态,只是希望系统在线程终止时能够自动清理并移除。这时候就可以使用以下的函数来进程操作:
#include
int pthread_detach(pthread_t thread);
接下来介绍线程的同步,线程同步主要涉及到两个工具
其中互斥量可以用来帮助线程同步对共享资源的使用,以防止线程甲试图访问一共享变量的同时,线程乙却试图对该变量进行修改。而条件变量则是用来在线程间相互通知共享变量的状态发生了变化。
之前有提到过,线程的一个主要优势就是能够通过全局变量来共享信息,但是这种便捷是有代价的:
需要确保多个线程不会同时修改同一变量,或者说,某一线程不会读取正由其他线程修改的变量。
这里提下 临界区的概念:这是指某一共享资源的代码片段,且其为原子操作,也就是说同时访问同一共享资源的其他线程不会终端该片段的执行。
互斥量有两种状态:
任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将会导致线程阻塞,或者报错。
一旦线程锁定互斥量,随即就成为该互斥量的所有者,只有所有者才能给互斥量解锁。因为所有权的关系,有时候又用获取(acquire),和释放(release)来代替加锁和解锁。
每个线程在访问同一资源时,将遵循如下的协议:
若是多个线程执行这一代码块,那么执行流程如下图所示:
还有一点需要说明的是,互斥锁从来都是一种建议而非强制的,在使用互斥锁时,需要遵循既定的锁定规则。
## PThread API
互斥量可以像静态变量那样分配,也可以在运行时动态创建,首先介绍静态创建:
只需以下代码即可以创建:
#include
pthread_mutex mtx = PTHREAD_MUTEX_INTIALIZER;
调用以下函数来对互斥量进行加解锁:
#include
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都在等待对方释放互斥量,这样就产生了死锁。
在这里提供以下两种解决方法:
接下来介绍互斥量的动态初始化
其动态初始化,主要是使用了以下的函数:
#include
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
有创建。相对地就有销毁,其函数调用为:
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
值得注意的是,销毁必须要在互斥量处于未锁定状态,且后续也无任何线程企图去锁定它时,才能进行销毁
关于互斥量的类型,可以查看函数手册,不过需要记住以下几点规则:
上述的互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待,阻塞于这一通知。
条件变量总是结合互斥量来使用的。条件变量就共享变量的状态改变发出通知,而互斥量则提供对该共享变量访问的互斥。
其实现函数主要如下:
#include
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
条件变量的主要操作就是发送信号和等待。发送信号操作就是通知一个或者多个处于等待状态的线程,某个共享变量的状态已经改变了。等待操作是指在收到一个通知前一直处于阻塞状态。
如上所述的在代码层面上如下:
#include
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
int pthread_cond_init(pthread_cond_t *mutex,const pthread_condattr_t *attr);
同样地,成对出现的销毁操作:
#include
int pthread_cond_destroy(pthread_cond_t *mutex);
在通常情况下,程序中的多个线程会并发执行,每个线程各司其职,直至其决意退出,但是也存在着需要取消线程的情况。
其取消函数为;
#include
int pthread_cancel(pthread_t thread);
在调用此函数之中,目标线程如何去响应,这取决于其取消状态和类型。如果禁用线程的取消性状态,那么请求会保持挂起状态,直至将线程的取消性状态置为启用,如果启用取消性状态,那么线程何时响应请求则依赖于取消性类型。若类型为延时取消,则在线程下一次调用某个取消点(这个知识点很好理解,线程在执行之后,不可能立即取消掉,为了能确定其取消时间点,设置了取消点),取消发生,若是异步取消,取消动作则随时可能发生。
线程可以设置一个清理函数栈,其中的清理函数由开发人员来自己定义,当函数遭到取消时,会自动调用这些函数以执行清理工作,这里所说的清理工作为恢复共享变量状态,解锁互斥量等等。