Duangw

基本概念

索引:

  1. 线程
  2. 用户级线程
  3. 轻进程
  4. 非绑定线程(进程域)
  5. 绑定线程(系统域)
  6. 属性对象
  7. 线程数据
  8. 线程的终止
  9. 退出线程
  10. 堆栈保护区
  11. 调度策略
  12. 堆栈
  13. 同步原语

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.线程的终止

一个线程可以用以下方式终止自身的运行:

  1. 从线程的第一个函数,即入口函数返回;
  2. 调用pthread_exit()函数;
  3. 用退出函数pthread_cancel()。

线程缺省为非分离线程,在终止后不会立即消失。

但有一个重要的例外,即初始线程(以main()函数为入口函数的线程)从main函数返回或调用exit()函数退出线程时,整个进程将被终止,包括进程中所有的线程。

当初始线程,也叫主线程调用pthread_exit()终止时,它仅仅终止自身的运行,并不终止其他线程的运行,也不会终止当前进程(当进程中所有线程都终止时,进程才会终止)。

 

9.退出线程

允许一个线程终止相同进程中其他线程的运行,当不再需要某些线程时,可以退出这些线程。

退出线程存在在危险,要小心处理。

退出可以在三种不同的环境下发生:

  1. 异步的;
  2. 由线程库定义的一系列退出点上;
  3. 由应用程序定义的一系列退出点上。

在缺省情况下,退出只发生在POSIX标准定义好的退出点上。

在所有情况下,要必须保证:资源占用情况和运行状态都和线程的起点一样。

POSIX标准定义了几个退出点:

  1. 由程序调用pthread_testcancel()建立的退出点;
  2. 调用pthread_cond_wait()或pthread_cond_timedwait()函数等待一个特定条件的线程;
  3. 调用了pthread_join()等待另一个线程结束的线程;
  4. 被阻塞在sigwait()上的线程;
  5. 一些标准函数,这些函数会阻塞线程。

缺省情况下,线程都是可退出的。当程序禁止退出时,所有的退出请求将会被推迟到重新允许退出为止。

可以用善后处理函数来恢复被退出线程的状态,使线程恢复原始状态。如释放内存,恢复运行状态等。善后处理函数在一段特定的代码中被压入堆栈并被弹出堆栈,它们必须配对,否则编译出错。

 

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.同步原语

当前在线程中有这样几种同步对象可供使用:

它们的比较如下: