线程
概述
线程是 CPU 使用的基本单元,它由线程 ID
、程序计数器
、寄存器集合
和栈
组成。它与同属于统一进程的其他线程共享代码段
、数据段
和其他操作系统资源
(文件和信号)。
注意,寄存器是线程私有的。
多线程模型
有两种不同方法来提供线程支持:
1.用户层的用户线程。
2.内核层的内核线程。
用户线程受内核支持,而无须内核管理;而内核线程由操作系统直接支持和管理。用户线程和内核线程之间存在中某种关系,这种关系主要分为以下三种线程模型。
多对一模型
将许多用户级线程映射到一个内核线程。线程管理是由线程库在用户空间进行的,因而效率较高。但是如果一个线程执行了阻塞系统调用,那么整个进程会阻塞。而且,因为任一时刻只有一个线程能访问内核,多个线程不能并行运行在多处理器上。(并没有增加并发性)
一对一模型
将一个用户级线程映射到一个内核线程。该模型在一个线程执行阻塞系统调用时,能允许另一个线程继续执行。因此,它也允许多个线程并行地运行在多处理器系统上。这种模型地唯一缺点是每创建一个用户线程就需要创建一个内核线程,开销大,这会导致绝大多数实现都会限制系统支持的线程数量。
多对多模型
多路复用了许多用户线程到同样数量或更小数量的内核线程上。内核线程的数量与特定机器有关。相比多对一模型,和一对一模型,多对多模型既增加了并发性,同时资源开销较小。开发人员可创建任意多的用户线程,并且相应内核线程能在多处理器上并发执行。当一个线程执行阻塞系统调用时,内核能调度另一个线程来执行。
线程库 Pthread
Pthread 是由 POSIX 标准为线程创建和同步定义的 API。不同语言和不同操作系统会有不同的线程库,但对 C/C++ 和 Unix 操作系统来说,Pthread 应该是最常用的线程库了。
代码例子
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void* runner(void* param)
{
if((*(int *)param) == 1)
printf("Main thread\n");
else
{
printf("Created thread\n");
pthread_exit(0);
}
}
int main()
{
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
int type1 = 2;
int type2 = 1;
pthread_create(&tid, &attr, &runner, &type1);
runner(&type2);
pthread_join(tid, NULL);
exit(0);
}
多线程问题
系统调用 fork() 和 exec()
有的 UNIX 系统有两种形式的 fork(),一种复制所有线程,另一种只复制调用了 fork() 的线程。
如果调用了 exec() 那么所指定的程序会替换整个进程,包括所有线程。
取消
线程取消是在线程完成之前来终止线程的任务。
要取消的线程通常称为目标线程。目标线程的取消可在如下两种情况下发生:
1.异步取消(asynchronous cancellation),一个线程立即终止目标线程。
2.延迟取消(deferred cancellation),目标线程不断检查它是否应该终止,这匀速目标线程有机会以有序方式来终止自己。
信号处理
信号处理的大致内容可以查看这篇博文,链接: POSIX 信号处理,这里讲解多线程对于信号处理的应对。
对于多线程进程的信号发送通常有如下选择:
1.发送信号到信号所应用的线程。
2.发送信号到进程内的每个线程。
3.发送信号到进程内的某些固定线程
4.规定一个特定线程以接收进程的所有信号
大多数多线程版 Unix 允许线程描述它会接受什么信号和拒绝什么信号。因此,有时一个异步信号只能发送给那些不拒绝它的线程。不过,因为信号只能处理一次,所以信号通常发送到不拒绝它的第一个线程。
发送信号的函数如下,
Unix 函数:
kill(pid_t pid, int signal); // pid为进程号
POSIX Pthread 线程库函数:
pthread_kill(pthread_t tid, int signal); // tid为线程号,此函数允许发送信号到指定线程
线程池
为了避免进程无限制地创建线程而耗尽 CPU 时间和内存,人们提出了线程池(thread pool)的概念。
线程池的主要思想是在进程开始时创建一定数量的线程,并放入到池中等待工作。当服务器收到请求时,它会唤醒池中的一个线程,开始执行工作。工作完毕后,再将它放回到池中。如果池中没有可用线程,那么服务器就等待到有可用线程为止。
线程池有如下两个主要优点:
1.通常用现有线程处理请求要比等待创建新的线程要快。
2.线程池限制了在任何时候可用的线程数量。
线程特定数据
同属一个进程的线程共享进程数据。不过,在某些情况下,每个线程可能需要一定数据的自己的副本,这种数据称为县城特定数据(thread-specific-data)。绝大多数线程库,都提供了对线程特定数据的支持。