线程相关的知识点比较多,这里有两个模型(生产者消费者模型,以及读者写者模型)我会在后面的博客中写入!
一、线程的概念
1.什么是线程
在一个程序里的一个执行路线叫做线程(准确来说,线程是一个进程内部的控制序列),一切进程都至少有一个执行线程。
2.进程和线程
(1)区别与联系
1>进程是承担系统资源分配的基本单位;
2>线程是程序执行的最小单位(承担资源调度的基本单位);
3>线程共享进程的数据,但也拥有自己的一部分数据(如:线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级、上下文关系)。
(2)一个进程的多个线程共享
1>同一地址空间(也就是说,Text Segment、Data Segment都是共享的,如果定义了一个函数,在各线程中都可以调用,如果定义了一个全局变量,在各线程中都可以访问到)
2>文件描述符表
3>每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
4>当前工作目录
5>用户id和组id
(3)进程和线程的关系图
(4)线程的优点
1>创建一个新线程的代价要比创建一个新进程小得多
2>与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3>线程占用的资源比进程少很多
4>能充分利用多处理器的可并行数量
5>在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6>计算密集型应用,为了能在多处理器系统上运行,将将计算分解到多线程中实现
7>I/O密集型应用,为了提高性能,将I/O操作重叠(线程可以等待不同的I/O操作)
(5)线程的缺点
1>性能损失(一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变)
2>健壮性降低(编写多线程需要更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量二造成不良影响的可能性是很大的,换句话说,线程之间是缺乏保护的)
3>缺乏访问控制(进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响)
4>编程难度提高(编写与调试一个多线程比单线程程序困难得多)
二、线程控制
POSIX线程库
1>与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
2>要使用这些库函数,要通过引入头文件
3>链接这些线程函数库时要使用编译器命令的“-lpthread”选项
1.创建线程
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;失败返回错误码
2.进程ID和线程ID
(1)在Linux中,目前的线程实现是NPTL(Native POSIX Thread Libaray)。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)
(2)在没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核描述符一下子就变成了1:N的关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID。
这里我们来学一下一条命令:
ps命令中的-L选项,会显示如下信息
LWP:线程ID,即gettid()系统调用的返回值。
NLWP:线程组内线程的个数
可以看出:上面的a.out进程是单线程的,进程组ID为2599,线程ID为2599(也可以多线程)
(3)Linux提供了gettid系统调用来返回其线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来供程序员使用。如果确实需要获得线程ID,可以采用如下方法:
#include<sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
3.线程ID及进程地址空间布局
(1)pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和前面说的线程ID不是一回事。(之前说的线程ID属于进程的调度范畴,因为线程是轻量级进程,是操作系统调度器的最小单位;而这里的说的线程ID属于NPTL线程库的范畴)。
(2)线程库NPTL提供了pthread_self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
(3)注意:pthread_t 的类型取决于实现(对于Linux目前实现的NPTL实现而言,pthread_t 类型的线程ID,本质上就是一个进程地址空间上的一个地址)
4.线程终止
(1)如果只需要终止某个线程而不是整个进程,有以下三种操作:
1>从线程函数return。(该方法对于主线程不适用,从main函数return相当于调用exit);
2>线程可以调用pthread_exit函数来终止自己;
3>一个线程可以调用pthread_cancel终止同一进程中的另外一个线程。
(2)pthread_exit函数(线程终止)
void pthread_exit(void *value_ptr);
注意:pthread_ptr不能指向一个局部变量;该函数没有返回值
(3)pthread_cancel函数(取消一个执行中的线程)
int pthread_cancel(pthread_t thread);
注意:thread为线程ID;该函数调用成功返回0,失败返回错误码
三、线程等待与分离
1.线程等待
(1)需要线程等待有以下两个原因:
1>已经退出的线程,其空间没有被释放,仍然在进程的地址空间内;
2>创建新的线程不回复用刚才退出线程的地址空间。
(2)pthread_join函数(等待线程结束)
int pthread_join(pthread_t thread,void **value_ptr);
注意:value_ptr指向一个指针,或者指向线程的返回值;成功返回0,失败返回错误码
调用该函数的线程将挂起等待,直到ID为thread的线程终止。
2.分离线程
(1)默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄露;如果不关心线程的返回值,当线程退出时,自动释放线程资源。
(2)
int pthread_detach(pthread_t thread);
//可以是线程组内其他现场曾对目标线程进行分离,也可以是线程自己分离
pthread_detach(pthread_self());
//joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
四、线程同步与互斥
由于在线程中那个一些操作不是原子的,需要注意以下几点:
(1)代码必须要有互斥行为:当代吗进入临界区时,不允许其它线程进入该临界区;
(2)如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区;
(3)如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到以上三点,其本质上就是需要一把锁(Linux上提供的这把锁叫做互斥量)
1.互斥量的接口
(1)初始化互斥量
两种方法:
1>静态分配
pthread_mutex=PTHREAD_MUTEX_INITIALIZER
2>动态分配
int pthread_mutex_init(pthread_mutex_t *reatrict mutex,const pthread_mutexattr_t *restrict attr);
//mutex:要初始化的互斥量
//attr:NULL
(2)销毁互斥量
注意:
1>使用PTHREAD_MUTEX_INITALIZER初始化的互斥量不需要销毁;
2>不要销毁一个已经加锁的互斥量;
3>已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destory(pthread_mutex_ *mutex);
(3)互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutax);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
调用pthread_lock时,可能会遇到以下情况:
1>互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;
2>发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞,等待互斥量解锁。
下面,我们来编写一个售票系统(在我之前的博客中有这样一个图,关于售票系统的操作,必须要保证买票是原子的)
2.条件变量
当一个线程互斥的访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了。(例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中,这种情况需要用到条件变量)
(1)初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//cond:要初始化的条件变量
//attr:NULL
(2)销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
(3)等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//cond:要在这个条件变量上等待
//mutex:互斥量
(4)唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
我们来举一个例子:
当我们运行./a.out时,程序会连续出来“活动、活动、活动...”我们可以用Ctrl C终止掉
(5)条件变量的使用规范
1>等待条件代码
pthread_mutex_lock(&mutex);
while(条件为假)
pthread_cond_wait(cond,mutex);
修改条件
pthread_mutex_unlock(&mutex);
2>给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_singnal(cond);
pthread_mutex_unlock(&mutex);