基本概念
线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
进程与线程
- 进程是资源竞争的基本单位
- 线程是程序执行的最小单位
- 线程共享进程数据,但也拥有自己的一部分数据
- 线程ID
- 程序计数器
- 寄存器组
- 栈
- errno
- 一个进程内部的线程可以共享资源
- 代码段
- 数据段
- 打开文件和信号
fork和创建新线程的区别
- 当一个进程执行一个fork调用的时候,会创建出进程的一个新拷贝,新进程将拥有它自己的变量和它自己的PID。这个新进程的运行空间是独立的,它在执行时几乎完全独立于创建它的进程
- 在进程里面创建一个新线程的时候,新的执行线程会拥有自己的栈(因此也就有自己的局部变量),但要与它的创建者共享全局变量、文件描述符、信号处理器和当前的工作目录状态
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多—-开销小
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量—-提高并发性
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。—-数据通信、共享数据方便
线程缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。—–线程是库函数实现的,不稳定 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多,因为GDB不支持 - 对信号支持不好
线程调度竞争范围
- 操作系统提供了各种线程模型,用来调度应用程序创建的线程。这些模型之间的主要不同是:在竞争系统资源(特别是CPU时间)时,线程调度竞争范围(thread-scheduling contention scope)不一样
- 进程竞争范围(process contention scope):各个线程在同一进程竞争“被调度的CPU时间”(但不直接和其他进程中的线程竞争)。—–进程级别
- 系统竞争范围(system contention scope):线程直接和“系统范围”内的其他线程竞争。—-系统级别
线程模型
N:1用户线程模型
- “线程实现”建立在“进程控制”机制之上,由用户空间的程序库来管理。OS内核完全不知道线程信息。这些线程称为用户空间线程。
这些线程都工作在“进程竞争范围”
在N:1线程模型中,内核不干涉线程的任何生命活动,也不干涉同一进程中的线程环境切换。
- 在N:1线程模型中,一个进程中的多个线程只能调度到一个CPU,这种约束限制了可用的并行总量。
- 第二个缺点是如果某个线程执行了一个“阻塞式”操作(如read),那么,进程中的所有线程都会阻塞,直至那个操作结束。为此,一些线程的实现是为这些阻塞式函数提供包装器,用非阻塞版本替换这些系统调用,以消除这种限制。
1:1核心线程模型
- 在1:1核心线程模型中,应用程序创建的每一个线程都由一个核心线程直接管理。
- OS内核将每一个核心线程都调到系统CPU上,因此,所有线程都工作在“系统竞争范围”。
- 这种线程的创建与调度由内核完成,因为这种线程的系统开销比较大(但一般来说,比进程开销小)
N:M混合线程模型
N:M混合线程模型提供了两级控制,将用户线程映射为系统的可调度体以实现并行,这个可调度体称为轻量级进程(LWP:lightweight process),LWP再一一映射到核心线程
总结:
(1)用户级线程
用户级线程主要解决的是上下文切换的问题,它的调度算法和调度过程全部由用户自行选择决定,在运行时不需要特定的内核支持。在这里,操作系统往往会提供一个用户空间的线程库,该线程库提供了线程的创建、调度和撤销等功能,而内核仍然仅对进程进行管理。如果一个进程中的某一个线程调用了一个阻塞的系统调用函数,那么该进程包括该进程中的其他所有线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势。—-只在本进程调度,还可能阻塞
(2)轻量级进程
轻量级进程是内核支持的用户线程,是内核线程的一种抽象对象。每个进程拥有一个或多个轻量级进程,而每个轻量级进程分别被绑定在一个内核线程上。
(3)内核线程
这种线程允许不同进程中的线程按照统一相对优先调度方法进行调度,这样就可以发挥多处理器的并发优势。
现在大多数系统都采用用户级线程与核心级线程并存的方法。一个用户级线程可以对应一个或几个核心级线程,也就是“一对一”或“多对一”模型。这样既可满足多处理机系统的需要,也可以最大限度地减少调度开销。
使用线程机制大大加快上下文切换速度而且节省很多资源。但是因为在用户态和内核态均要实现调度管理,所以会增加实现的复杂度和引起优先级翻转的可能性。一个多线程程序的同步设计与调试也会增加程序实现的难度
相关API
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文
<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项,在gcc 4.8以上,编译的时候加上-pthread选项就可以编译多线程程序了
pthread_create函数
- 功能:创建一个新的线程
- 原型
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;失败返回错误码
错误检查
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小
pthread_exit函数
- 功能:线程终止
- 原型
void pthread_exit(void *value_ptr);
- 参数
value_ptr:value_ptr不要指向一个局部变量。 - 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_join函数
- 功能:等待线程结束
- 原型
int pthread_join(pthread_t thread, void **value_ptr);
- 参数
- thread:线程ID
- value_ptr:它指向一个指针,指向线程的返回值
- 返回值:成功返回0;失败返回错误码
pthread_self函数
- 功能:返回线程ID
- 原型
pthread_t pthread_self(void);
- 返回值:成功返回0
pthread_cancel函数
- 功能:取消一个执行中的线程
- 原型
int pthread_cancel(pthread_t thread);
- 参数
thread:线程ID - 返回值:成功返回0;失败返回错误码
pthread_detach函数
- 功能:将一个线程分离
- 原型
int pthread_detach(pthread_t thread);
- 参数
thread:线程ID - 返回值:成功返回0;失败返回错误码
线程属性
线程属性
- 初始化与销毁属性
- int pthread_attr_init(pthread_attr_t *attr);
- int pthread_attr_destroy(pthread_attr_t *attr);
- 获取与设置分离属性
- int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
- int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
获取与设置栈大小
- int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
- int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
获取与设置栈溢出保护区大小
- int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
- int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);
- 获取与设置线程竞争范围
- int pthread_attr_getscope(const pthread_attr_t *attr,int *contentionscope);
- int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
获取与设置调度策略
- int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
- int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
获取与设置继承的调度策略
- int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
- int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
- 获取与设置调度参数
- int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
- int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
并发级别
- 获取与设置并发级别
- int pthread_setconcurrency(int new_level);
- int pthread_getconcurrency(void);
- 仅在N:M线程模型中有效,设置并发级别,给内核一个提提示:表示提供给定级别数量的核心线程来映射用户线程是高效的。
绑定属性
Linux中采用“一对一”的线程机制,也就是一个用户线程对应一个内核线程。绑定属性就是指一个用户线程固定地分配给一个内核线程,因为CPU时间片的调度是面向内核线程(也就是轻量级进程)的,因此具有绑定属性的线程可以保证在需要的时候总有一个内核线程与之对应。而与之对应的非绑定属性就是指用户线程和内核线程的关系不是始终固定的,而是由系统来控制分配的。
分离属性
分离属性是用来决定一个线程以什么样的方式来终止自己。在非分离情况下,当一个线程结束时,它所占用的系统资源并没有被释放,也就是没有真正的终止。只有当pthread_join()函数返回时,创建的线程才能释放自己占用的系统资源。而在分离属性情况下,一个线程结束时立即释放它所占有的系统资源。这里要注意的一点是,如果设置一个线程的分离属性,而这个线程运行又非常快,那么它很可能在pthread_create()函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用。