深度下潜――多线程篇
时间:2010-10-19 来源:aaron_xueli
计算机系统变得越来越复杂,多线程机制给我们带来了能够继续管理它们的希望。
――Andrew Koening and Barbara Moo
一、线程的基本概念
进程(process)和 文件(files)是UNIX/Linux操作系统两个最基本的抽象。进程是处于执行期的程序和它所包含的资源的总和,也就是说一个进 程就是处于执行期的程序。一个线程(thread)就是运行在一个进程上下文中的一个逻辑流,不难 看出,线程是进程中最基本的活动对象。
在传统的系统中,一个进程只包含一个线程。但 在现代操作系统中,允许一个进程里面可以同时运行多个线程,这类程序就被称为多线程程序。所有的程序都有一个主线程(main thread), 主线程是进程的控制流或执行线程,见图1。在多线程程序中,主线程可以创建一个或多个对等线程(peer thread),从这个时间 点开始,这些线程就开始并发执行,见图2。主线程和对等线程的区别仅在于主线程总是进程中第一个运行的线程。从某种程度上看,线程可以看作是轻量级的进程 (lightweight process)。在Linux操作系统中,内核调度的基本对象是线程,而不是进程,所以进程中的多个线程 将由内核自动调度。
每个线程都拥有独立的线程上下文(thread context),线程ID(Thread ID,TID),程序计数器(pc), 线程栈(stack),一组寄存器(register)和条件码。其中,内核正是通过线程ID(TID)来识别线 程,进行线程调度的。
图 1多线程进程的控制流
图 2并发线程执行模型
二、线程与进程的异同点
线程和进程在很多方面是相似的。相同点主要表 现在如下几方面:
1) 比如都具有ID,一组寄存器,状态,优先级以及所要遵循的调度策略。
2) 每个进程都有一个进程控制块,线程也拥有一个线程控制块(在Linux内核,线程控制块与进程控制块用同一 个结构体描述,即struct task_struct),这个控制块包含线程的一些属性信息,操作系统使用这些属性信息来描述线程。
3) 线程和子进程共享父进程中的资源。
4) 线程和子进程独立于它们的父进程,竞争使用处理器资源。
5) 线程和子进程的创建者可以在线程和子进程上实行某些控制,比如,创建者可以取消、挂起、继续和修改线程和子 进程的优先级。
6) 线程和子进程可以改变其属性并创建新的资源
除了这些相同点,在很多方面也 存在着差异:
1) 主要区别:每个进程都拥有自己的地址空间,但线程没有自己独立的地址空间,而是运行在一个进程里的所有线程 共享该进程的整个虚拟地址空间
2) 线程的上下文切换时间开销比进程上下文切换时间开销要小的多
3) 线程的创建开销远远小于进程的创建
4) 子进程拥有自己的地址空间和数据段的拷贝,因此当子进程修改它的变量和数据时,它不会影响父进程中的数据, 但线程可以直接访问它进程中的数据段
5) 进程之间通讯必须使用进程间通讯机制,但线程可以与进程中的其他线程直接通讯
6) 线程可以对同一进程中的其他线程实施大量控制,但进程只能对子进程实施控制
7) 改变主线程的属性可能影响进程中其他的线程,但对父进程的修改不影响子进程
三、深入剖析线程
3.1 线程优缺点
线程的优点 |
线程的缺点 |
上下文切换需要更少的系统资 源 |
并发读/写需要同步 |
增加了应用程序的吞吐量 |
很容易破坏它进程的地址空间 |
任务间的通讯不需要特殊机制 |
只存在于单个进程中,因此不 能被重用 |
简化了程序结构 |
|
表 1线程优缺点对比表
3.1 线程属性
POSIX线程库定义了线程 属性对象,它封装了线程的创建者可以访问和修改的线程属性。线程属性主要包括如下属性:
1) 作用域(scope)
2) 栈尺寸(stack size)
3) 栈地址(stack address)
4) 优先级(priority)
5) 分离的状态(detached state)
6) 调度策略和参数(scheduling policy and parameters)
线程属性对象可以与一个线程或多个线程相关 联。当使用线程属性对象时,它是对线程和线程组行为的配置。使用属性对象的所有线程都将具有由属性对象所定义的所有属性。虽然它们共享属性对象,但它们维 护各自独立的线程ID和寄存器。
线程可以在两种竞争域内竞争资源:
1) 进程域(process scope):与同一进程内的其他线程
2) 系统域(system scope):与系统中的所有线程
作用域属性描述特定线程将与哪些线程竞争资 源。一个具有系统域的线程将与整个系统中所有具有系统域的线程按照优先级竞争处理器资源,进行调度。
分离线程是指不需要和进程中其他线程同步的线 程。也就是说,没有线程会等待分离线程退出系统。因此,一旦该线程退出,它的资源(如线程ID)可以立即被重用。
线程的布局嵌入在进程的布局中。进程有代码 段、数据段和栈段,而线程与进程中的其他线程共享代码段和数据段,每个线程都有自己的栈段,这个栈段在进程地址空间的栈段中进行分配。线程栈的尺寸在线程 创建时设置。如果在创建时没有设置,那么系统将会指定一个默认值,缺省值的大小依赖于具体的系统。
POSIX线程属性对象中可设置的线程属性 及其含义参见表二:
函数 |
属性 |
含义 |
int pthread_attr_setdetachstate (pthread_attr_t* attr ,int detachstate) |
detachstate |
detachstate属性 控制一个线程是否是可分离的 |
int pthread_attr_setguardsize (pthread_attr_t* attr ,size_t guardsize) |
guardsize |
guardsize属性设置 新创建线程栈的保护区大小 |
int pthread_attr_setinheritsched (pthread_attr_t* attr, int inheritsched) |
inheritsched |
inheritsched决 定怎样设置新创建线程的调度属性 |
int pthread_attr_setschedparam (pthread_attr_t* attr , const struct sched_param* restrict param) |
param |
param用来设置新创建线 程的优先级 |
int pthread_attr_setschedpolicy (pthread_attr_t* attr, int policy) |
policy |
Policy用来设置先创建 线程的调度策略 |
int pthread_attr_setscope (pthread_attr_t* attr , int contentionscope) |
contentionscope |
contentionscope 用于设置新创建线程的作用域 |
int pthread_attr_setstack (pthread_attr_t* attr, void* stackader, size_t stacksize) |
stackader stacksize |
两者共同决定了线程栈的基地 址以及堆栈的最小尺寸(以字节为单位) |
int pthread_attr_setstackaddr (pthread_attr_t* attr , void* stackader) |
stackader |
stackader决定了新 创建线程的栈的基地址 |
int pthread_attr_setstacksize (pthread_attr_t* attr, size_t stacksize) |
stacksize |
stacksize决定了新 创建线程的栈的最小尺寸(以字节为单位) |
表 2线 程属性及其含义
3.2 线程调度和优先级
进程的调度策略和优先级属于主线程,换句话说 就是设置进程的调度策略和优先级只会影响主线程的调度策略和优先级,而不会改变对等线程的调度策略和优先级(注:这句话不完全正确)。每个对等线程可以拥 有它自己的独立于主线程的调度策略和优先级。
在Linux系统中,进程有三种调度策 略:SCHED_FIFO、SCHED_RR和SCHED_OTHER,线程也不例外,也具有这三种策略。
在pthred库中,提供了一个函数,用来设 置被创建的线程的调度属性:是从创建者线程继承调度属性,还是从属性对象设置调度属性。该函数就是:
int pthread_attr_setinheritsched (pthread_attr_t *__attr, int __inherit) 其中,inherit的值为 下列值中的其一: enum { PTHREAD_INHERIT_SCHED, // 线程调度属性从创建者线程继承 PTHREAD_EXPLICIT_SCHED // 线程调度属性设置为attr设置的属性 }; |
如果在创建新的线程时,调用该函数将参数设置 为PTHREAD_INHERIT_SCHED时,那么当修改进程的优先级时,该进程中继承这个优先级并且还没有改变其优先级的所有线程也将会跟着改变优 先级(现在应该明白我刚才为什么说那句话部正确的原因了吧)。
3.3 线程状态
线程具有进程所拥有的相同状态 和状态转换。如图3所示:
图 3线 程状态及其转换(摘自《pthread prime》)
四线程模型
线程模型
|
含义
|
委托模型
|
Boss线程创建其他线程 (worker线程),并给每个worker线程分配任务。Boss线程可能在每个线程完成它们的任务之前一直等待。
|
对等模型
|
所有的线程都具有相同的工作 状态。对等线程创建执行任务所需要的所有线程,但不执行委托职责。对等线程可以从单个输入流处理请求,这些输入流被所有线程共享,或者每个线程都有其自己 的输入流。
|
管道模型
|
类似于装配线流程,分阶段处 理输入流,然后传给下一个线程进行处理
|
生-消模型
|
生产者线程生产数据给消费者 线程使用,数据存储在生产者和消费者共享的存储块中。
|