基本概念
索引:
1.线程
多线程这个词可以被译为多控制线(multiple threads of control)或多控制流(multiple flows of control)。一个传统的UNIX进程只包含一条控制线,而多线程技术(MT)将一个进程分解成多条执行线程(execution thread),其中每一条都可以独立运行。
线程共享所在进程的资源,包括地址空间,打开的文件等等。但每一个线程都各自具有下列状态量:
- 线程标识符;
- 寄存器状态(包括当前指令指针和栈指针);
- 栈;
- 信号掩码;
- 优先级;
- 线程的私有内存。
2.用户级线程
用户级线程被放在用户空间中处理,以避免内核上下文切换造成的性能恶化。
3.轻进程
线程库使用由内核支持的控制流程(称为轻进程)来完成其功能。我们可以把一个轻进程(即LWP)当作一个执行代码或系统调用的虚拟CPU。
轻进程(LWPs)连接了用户层和核心层。每一个进程包括一个或多个LWP,每一个LWP运行一个或多个用户线程。
每一个LWP都是在某个内核池中的一个内核资源。当线程被创建、调度或终止时,系统会为线程分配(挂上)LWP,或释放(卸载)LWP。
4.非绑定线程(进程域)
这种线程在进程的用户空间中通过挂上和卸载LWP池中的LWPs来完成调度。线程可以在LWPs间移动,这大大改善了线程的性能。
5.绑定线程(系统域)
一个绑定线程和一个LWP固定的挂接在一起。每一个绑定线程一直被绑定在一个LWP上,一直到线程结束。对这类线程的调度由操作系统完成。
6.属性对象
属性对象是被用来指定线程的属性的。当创建一个线程或初始化一个同步变量时,都需要使用属性对象。
只能在创建线程的时候指定线程的属性,不能在线程运行时改变它的属性。
7.线程数据
单线程的C程序有两种基本数据:局部变量和全局变量。对于多线程C程序还有第三种数据:线程数据(TSD,thread-specific data)。它像全局变量,能被某个线程调用的所有函数访问,但却是线程私有的,只能被特定线程访问。
线程数据是定义和引用线程私有数据的唯一方法。每一个线程数据项都有一个键和它相关联,这个键对所有的线程都是一样的,但在不同的线程中使用这个键可以得到不同的值(被不同的线程绑定为不同的内存指针,指向不同的内存)。
8.线程的终止
一个线程可以用以下方式终止自身的运行:
- 从线程的第一个函数,即入口函数返回;
- 调用pthread_exit()函数;
- 用退出函数pthread_cancel()。
线程缺省为非分离线程,在终止后不会立即消失。
但有一个重要的例外,即初始线程(以main()函数为入口函数的线程)从main函数返回或调用exit()函数退出线程时,整个进程将被终止,包括进程中所有的线程。
当初始线程,也叫主线程调用pthread_exit()终止时,它仅仅终止自身的运行,并不终止其他线程的运行,也不会终止当前进程(当进程中所有线程都终止时,进程才会终止)。
9.退出线程
允许一个线程终止相同进程中其他线程的运行,当不再需要某些线程时,可以退出这些线程。
退出线程存在在危险,要小心处理。
退出可以在三种不同的环境下发生:
- 异步的;
- 由线程库定义的一系列退出点上;
- 由应用程序定义的一系列退出点上。
在缺省情况下,退出只发生在POSIX标准定义好的退出点上。
在所有情况下,要必须保证:资源占用情况和运行状态都和线程的起点一样。
POSIX标准定义了几个退出点:
- 由程序调用pthread_testcancel()建立的退出点;
- 调用pthread_cond_wait()或pthread_cond_timedwait()函数等待一个特定条件的线程;
- 调用了pthread_join()等待另一个线程结束的线程;
- 被阻塞在sigwait()上的线程;
- 一些标准函数,这些函数会阻塞线程。
缺省情况下,线程都是可退出的。当程序禁止退出时,所有的退出请求将会被推迟到重新允许退出为止。
可以用善后处理函数来恢复被退出线程的状态,使线程恢复原始状态。如释放内存,恢复运行状态等。善后处理函数在一段特定的代码中被压入堆栈并被弹出堆栈,它们必须配对,否则编译出错。
10.堆栈保护区
堆栈保护区被用来在堆栈指针越界的情况下提供保护。如果一个线程具有堆栈保护的特性,那么系统在创建线程堆栈时会在堆栈的末尾多分配一块内存,用来防止指针访问堆栈时,溢出堆栈的边界。如果一个应用程序访问堆栈时溢出到堆栈保护区将会引发一个错误(往往是当前线程收到一个SIGSEGV信号)。
提供堆栈保护区属性有两个原因:
- 首先,堆栈保护会引起系统资源浪费。如果一个应用程序创建了大量线程,而且确保这些线程不会越界访问堆栈,那么这个应用程序可以通过取消堆栈保护区来节省系统资源。
- 当线程在堆栈中存放大的数据结构时,有可能需要一个大的堆栈保护区。
11.调度策略
POSIX标准定义的调度策略有:SCHED_FIFO(先入先出)、SCHED_RR(循环)、SCHED_OTHER(由不同版本的POSIX线程库定义的缺省调度策略)。
SCHED_FIFO先入先出
如果不被高优先级的线程打断,正在运行的线程将一直运行下去直到结束。处在系统域的线程(PTHREAD_SCOPE_SYSTEM)将属于实时调度类型,且当前进程的有效用户标识符必须为0;处在进程域的线程(PTHREAD_SCOPE_PROCESS)将属于分时调度类型。
SCHED_RR循环
如果不被高优先级的线程打断,正在运行的线程将运行一段时间,这段时间的长短将由系统决定。处在系统域的线程(PTHREAD_SCOPE_SYSTEM)将属于实时调度类型,且当前进程的有效用户标识符必须为0;处在进程域的线程(PTHREAD_SCOPE_PROCESS)将属于分时调度类型。
SCHED_OTHER
线程根据优先级调度,线程保持运行,直到更高优先级的线程强占了处理器资源,或是线程被阻塞,或是线程主动出让运行权。
12.堆栈
一般来说,线程堆栈都从某个页的边界上开始,在页的边界上结束,也就是说堆栈的大小必须是页的大小的整数倍。在堆栈的顶端会多加一个无访问权的页,这样当程序越界访问堆栈时,往往会访问到这个区间从而引发一个SIGSEGV事件(发给溢出访问的线程)。由应用程序分配的堆栈也应该有一个这样的保护页。
当在程序中指定堆栈时,线程应该是PTHREAD_CREATE_JOINABLE的。这是因为由程序指定的堆栈必须由程序在线程终止后释放,而在程序中我们只能通过调用pthread_join函数来判断线程是否终止。
一般来说,应用程序不需要为线程分配堆栈。线程库将会为每一个线程分配1M不预留交换空间的虚拟内存作为堆栈(线程库使用mmap()函数的MAP_NORESERVE选项来分配内存)。
很少需要由程序指定线程的堆栈和它的大小。甚至对专家来说,准确的预测程序所需堆栈的大小都是困难的。这是因为,即使是一个兼容ABI标准的程序都无法静态的决定程序所需的堆栈大小。堆栈的大小依赖于程序运行时所处的环境。
但是,当你所需要的堆栈和缺省的堆栈有所不同,比如你需要更大的堆栈,或你需要的堆栈比缺省堆栈小,而你需要创建成千的线程(这时你需要限制堆栈的大小以节省内存资源)。需要建立自己的堆栈。
指定线程堆栈的大小时,一定要算上调用函数所需的空间,即计算中应该包括调用函数时需压入栈的返回地址,局部变量等。
对堆栈大小的限制是:最大不能大于系统中连续的可用虚存的大小,最小不能小于所有函数调用需要使用的堆栈框架,局部变量等的容量。你可以用宏PTHREAD_STACK_MIN来取对堆栈容量的最小限制。这样大小的堆栈只够一个执行空函数的线程使用。有用的线程所需要的堆栈容量比这个值大的多。
当指定自己的栈时,注意用mprotect()为它增加一个保护区。
13.同步原语
当前在线程中有这样几种同步对象可供使用:
- 互斥锁;
- 条件变量;
- 信号量;
- 读写锁。
它们的比较如下:
- 在线程中基本的同步原语是互斥锁原语,也是最节省内存和运行时间的原语。一般用互斥锁来保证对一个资源的顺序访问。
- 其次有效的是条件变量。一般,条件变量被用来在状态发生变化时阻塞线程。记住在等待条件变量前必须锁定一个互斥锁,而从pthread_cond_wait()函数返回后必须解锁这个互斥锁。从改变条件直到调用pthread_cond_signal()函数,也需要一直保持互斥锁的锁定。
- 信号量比条件变量使用更多的内存。但它们的功能更强大。和锁不同的是,信号量没有所有者,当有线程将信号量计数减为0之后,任何线程都可以继续操作信号量。
- 读写锁允许并发的读操作和独享的写操作以保护资源。读写锁实际上是一个有读写两种状态的锁,修改数据前必须先将读写锁写锁定,写锁定读写锁之前必须解开所有对该读写锁的读锁定。