线程
每个线程是CPU使用的基本单元;他包括线程ID,程序计数器,寄存器和堆栈。它与统一进程的其他线程共享代码段,数据段和其他操作系统资源,如打开文件和信号。
每个传统的进程只有一个控制线程。如果一个进程拥有多个控制线程,那么它能同时执行多个任务。下图说明了传统单线程和多线程进程的差异。
线程模型
在线程运行的过程中,一般分为** 用户线程** 和** 内核线程**。
用户线程位于内核之上,它的管理无需内核支持。
内核线程由操作系统来直接支持和管理。几乎所有的现代操作系统都是支持内核线程的。
在实现的方式上,用户线程和内核线程存在某种关系。一般分为三种:一对一模型,多对一模型,多对多模型。
多对一模型
多对一模型映射多个用户线程到一个内核线程。线程管理是由用户控件的线程库来完成的,因此效率更高。但是,一个线程执行阻塞系统调用,那么整个进程将会被阻塞。在着,在任意时间只能有一个线程可以访问内核。万一内核线程也在同步等待其他事件的发生,那么此时,进程的其他线程也就得不到执行了。
一对一模型
一对一模型: 每个用户线程映射到一个线程。这个模型,在一个线程执行系统调用的时候,能够允许另一个线程继续执行。它也允许多个线程并行运行在多核处理器上。这个模型的缺点就是:创建一个用户线程就要创建一个内核线程,内核线程的创建开销影响程序的性能。所以在这个模型中,系统对支持的线程的数量是有限制的。
多对多模型
多对多模型多路复用多个用户线程到同样数量或者数量更小的内核线程上。这种模型结合了前两种的方式。没有了前两种的缺点。
开发人员可以创建任意多的用户线程,并且相应内核线程能在多处理上进行并发执行。而且当一个用户执行系统调用的时候,内核可以调度另外的内核线程来执行其他线程的操作。
线程库
线程库为程序员提供了创建和管理线程的API。
在展开对线程的API的整理之前,首先了解两个概念:异步线程和同步线程。
异步线程:一旦父线程创建了一个子线程,父线程继续自身的执行,不用管子线程何时终止,这个父子线程会并发执行,独立运行。由于各自独立运行,他们通常会有很少的数据共享。
同步线程:如果父线程创建一个子线程或者多个子线程后,那么在回复执行之前,它需要等待所有子线程的终止,就出现了同步线程。由父线程创建的子线程并发执行,但是父线程没有办法继续工作。一旦有线程终止,它就会与父线程连接。所有子线程完成后,父线程才继续工作,这种方式叫做 分叉-连接策略。
Pthreads
Pthreads库是Posix 标准定义的线程创建与同步API。它是线程的行为规范,而不是实现。大多数的操作系统都实现了这个线程规范。
#include
#include
#include
#include
int sum; // 这个数据是被线程共享的数据
void *runner(void* param) {
printf("son thread is here.\n");
int i = 0;
int upper = atoi((char*)param);
sum = 0;
for (i = 0; i <= upper; ++i) {
sum += i;
}
pthread_exit(0);
}
int main(int argc, char* argv[]) {
pthread_t tid;
pthread_attr_t attr; // 设置线程属性
if (argc != 2) {
fprintf(stderr, "usage: a.cout \n");
return -1;
}
if (atoi(argv[1]) < 0) {
fprintf(stderr, "%d must be >=0\n", atoi(argv[1]));
return -1;
}
// 获取默认的线程属性
pthread_attr_init(&attr);
// 创建线程
pthread_create(&tid, &attr, runner, argv[1]);
// 等待线程退出
pthread_join(tid, NULL);
printf(" parent thread is here.\n");
printf(" sum = %d\n", sum);
return 0;
}
根据上面的代码来大概讲解下创建线程的API。
pthread_t tid; 声明了线程的线程标识符。每个线程都有一个唯一的线程标识符。
每个线程都有一组属性,包括堆栈大小和调度信息。pthread_attr_t attr; 表示线程属性。
通过调用pthread_attr_init(&attr) 可以设置这些属性。由于没有任何属性,这里为默认属性。
pthread_create(); 可以创建一个单独的线程。除了传递线程的标识符和属性外,还要传递一个函数名,这里为runnner。以便线程从哪个函数开始执行,最后一个需要传递命令行参数。
这里程序里有两个线程。一个为main的主线程 ,另外一个线程从runner()开始执行。根据上面的分叉-连接策略:主线程通过pthread_join()来等待runner()完成。 runner() 线程在pthread_exit()之后就会终止。
线程池
为了说明线程池,先来看一个多线程的web服务器。
在web服务中,每当服务器收到一个请求时,他就会创建一个单独的线程来处理请求。虽然创建一个线程的代价比创建一个进程的代价要小,但是任然存在问题。
- 线程在创建需要的时间是多少,处理完这个请求后,线程还是会被注销。
- 如果允许所有并发请求都通过新线程来处理,那么系统的资源和内存需要受到限制,因为不可能无限制的创建新的线程。
解决方法就是线程池。
线程池的主要思想就是:在进程开始的时候就创建一定量的线程。并加载到线程池中等待工作。当服务器受到请求是,它会唤醒池内的一个线程去处理请求。一旦线程完成了服务,它在回到池中等待再次被唤醒。如果池内没有可用线程,服务器会等待,知道有新的空线程为止。
多线程的问题。
1. 系统调用fork() 和exec()
如果程序内的某个线程调用了fork(),那么新的进程是复制所有线程呢,还是复制只调用fork()的线程?
Unix 两种都支持,但是具体的实现还是根据是否要调用exec()来决定。
如果分支后直接执行exec(),那么没有必要全部复制。因为exec()会把整个进程换掉。如果分支后不调用exec(),新进程应该重复所有进程。
2. 信号处理
Unix信号用于通知某个进程特定的事件已经发生。比如,I/O就绪,或者用户键盘输入了 ctrl+c等。
这里在了解下同步信号和异步信号。
和以前的概念一样,同步表示一直在等待信号的处理。异步表示发完信号就可以执行其他操作。
一般进程街道信号后,可以有两种处理方法:
- 缺省的信号处理程序。(操作系统定义的默认处理)
- 用户自定义的信号处理程序。(比如异常捕捉等)。
这里就会产生一个问题。
- 信号发给所有的使用的线程。(不同进程的线程)
- 信号发给某个进程内所有线程。
- 信号发给某个进程内的某些线程。
- 规定一个特性线程只处理,接收外部的所有信号。
Unix 传递信号的系统调用为:
kill(pid_t pid, int signal);
发送给某个进程的 signal信号。
pthread_kill(pthread_t tid, int signal);
发给指定的线程signal信号。
线程的撤销
线程撤销是在线程完成之前终止线程。
例子: 用户按下网页浏览器上的按钮,以停止进一步的加载网页。通常加载网页可能是多个线程,每个图像可能都是一个单独的线程在加载,当用户点下停止按钮,所有的网页加载线程都要被撤销。
需要撤销的线程称为目标线程。目标线程的终止一般分为
- 异步撤销: 一个线程立即终止目标线程。
- 延迟撤销: 目标线程需要检查它自身是否可以终止,比如它有依赖其他线程的终止,自己才能终止。
所以问题来了:
当系统为进程分配资源的时候,分配到了已撤销的线程怎么办? 比如 线程退出了,它的子线程和他在并行执行,但是资源分配到了父线程。还有就是已经撤销的线程正在更新和其他线程共享的数据,撤销会有问题。
操作系统收回撤销线程的系统资源,不会收回所有的资源。所以在最后线程撤销的时候,最后有可能资源泄露。
pthread_cancel() 可以发起线程额撤销。这个是Pthread线程库提供的API。但是它并不能解决上面的问题。
调度程序激活
多线程编程最后的一个问题就是线程库与内核之间的通信。
许多系统在实现多对多的模型是,都在用户和内核线程之间加了一个中间数据结构** 轻量级进程 LWP(LightWeight process)**。
用户线程库与内核之间的一种通信方案称为调度器激活。
内核提供一组虚拟处理器LWP给应用程序,而应用程序可以调度用户线程到任意一个虚拟处理器上。此外,内核应将有关特事件通知应用程序。这个步骤 称为 回调。它由线程库通过回调处理程序来处理。
当一个应用程序的线程要阻塞时,它会触发一个回调的事件。内核向应用程序发出一个回调,通知它有一个线程将会阻塞并标识特定线程。然后分配一个新的虚拟处理器给应用线程。应用程序在这个新的虚拟处理器上进行回调处理程序,保存它的阻塞状态,并释放阻塞线程运行的虚拟处理器。接着,回调处理函数调度一个新的线程到虚拟处理器上。当阻塞线程等待的事件发生时,内核向线程库在发出另一个回调,通知线程库,之前等待的事件发生了。该回调也需要虚拟处理器,内核可能分配一个新的虚拟处理器,或者抢占一个用户线程再次执行会回调处理程序。
....
未完待续