当前位置:Linux教程 - Linux - Solaris多线程编程(1)

Solaris多线程编程(1)



         1 多线程基础介绍多线程这个词可以被译为多控制线(multiple threads of control)或多控制流(multiple flows of control)。一个传统的UNIX进程只包含一条控制线,而多线程技术(MT)将一个进程分解成多条执行线程(execution thread),其中每一条都可以独立运行。在程序中使用多线程,你可以:改进程序的实时响应能力更有效的使用多处理器改进程序结构减少对系统资源的使用本章解释了一些多线程的术语,优点和概念。如果你希望立即开始使用多线程技术,可以从第2章开始阅读。本章包括:l 定义多线程涉及的术语l 多线程标准介绍l 多线程的优点l 理解多线程的基本概念定义多线程涉及的术语表1-1介绍了本书中的一些术语。表1-1 多线程术语术语定义进程(Process)用fork(2)系统调用创建的UNIX环境(包括文件描述符,用户标识符等等),程序在这个环境中运行线程(Thread)在某一进程的上下文(context)中执行的代码序列POSIX线程(POSIX threads)符合POSIX1003.1c标准的线程接口Solaris线程(Solaris threads)由SunSoft(tm)开发的线程接口,这个接口的开发比POSIX线程接口标准的制定要早,因此不符合POSIX标准单线程的(Single-threaded)只允许有一个线程多线程的(Multithreaded)允许有两个或多个线程用户层(或应用层)线程在用户(相对于内核而言)空间中的线程,这些线程由线程库中的例程管理轻进程(Lightweight processes)在内核中可见的线程,被用来执行核心代码和系统调用(也叫做LWPs)绑定线程(Bound threads)被固定的绑定在LWPs上的线程非绑定线程(Unbound threads)缺省的Solaris线程,环境切换快速,不需要核心支持的线程属性对象(Attribute object)包括不透明(不公开)的数据类型和相关函数。这些函数用来标准化对POSIX线程、互斥锁和条件变量可配置属性的操作互斥锁(Mutual exclusion locks)一组函数,被用来保护对共享数据的访问条件变量(Condition variables)一组函数,被用来阻塞线程,直到某种状态发生变化计数信号量(Counting variables)一种基于内存的同步机制并行(Parallelism)多个线程同时运行的情形并发(Concurrency)多个线程一起运行的情形。从广义上讲,并发的形式也包括利用时间片轮转的虚拟并行多线程标准介绍多线程程序设计的概念在六十年代被首次提出, UNIX系统中多线程机制的开发始于八十年代中期。尽管对于多线程的概念和特点大家都有共识,但各种系统中用于实现多线程的接口仍然有很大的差别。POSIX(可移植操作系统接口)1003.4a小组,多年来一直致力于多线程编程的标准化。他们制定的这一标准现在已经被批准了。本书正是基于这个POSIX标准:P1003.1b最终草案14(实时系统)和P1003.1c最终草案10(多线程)。本书既介绍了POSIX线程接口(pthreads)也介绍了Solaris自身特有的线程接口。Solaris的线程接口在Solaris 2.4中就已经有了,而且在功能上和POSIX线程标准没有区别。但是由于POSIX线程接口比Solaris线程接口有更好的可移植性,本书将主要介绍POSIX线程接口。关于Solaris线程接口的内容,我们将在第9章中集中阐述。多线程的优点改进程序的实时响应能力如果一个程序需要执行许多互不依赖的工作,那么可以重新设计程序,把这些互不依赖的工作分别设计成线程,这样它们就可以同时执行了。例如:多线程化的图形界面的用户在进行下一个行动之前不需要等待另一个工作的结束。更有效的使用多处理器一般来说,使用多线程的应用程序不需要考虑系统有多少个可用的处理器。应用程序的性能随着处理器的增多而提高。具有高度并行性的数值算法和应用程序(如矩阵乘法),若在多处理器系统中利用多线程往往会有更快的运行速度。改进程序结构对许多程序来说,如果把程序分成多个独立或半独立的执行单元,而不是把程序设计成一个单一复杂的流程,将使程序拥有更高效的结构。多线程程序比单线程程序更适应于用户的各种各样的要求。减少对系统资源的使用尽管利用多进程共享内存访问数据,程序也可以实现多控制线。但是,每个进程都有一个自己的地址空间和操作环境状态(operating environment state)。创建和维护一个进程所需要的花费,在时间和空间上都远比创建和维护一个线程多得多。而且,由于每个进程都有自己独立的地址空间,这使程序员必须用很复杂的方式实现进程间通讯和进程间同步。RPC(远地过程调用)和线程通过结合使用线程和远地过程调用包,你可以充分利用非共享内存的多处理器环境(例如通过局域网连接的多台工作站)。这种结合相对简便地将你的应用分布在多台工作站上,应用程序就像运行在一个多处理器的计算机上一样,这就提高了应用程序的效能。例如,一个主线程可以创建一系列子线程,每个子线程可以产生一个远地过程调用,调用另一台工作站上的过程。虽然主线程在一台工作站上创建了所有的子线程,但这些子线程是在其他计算机上执行的,这就使整个程序被分布在多台工作站并行执行。理解多线程的基本概念并发性和并行性在单个处理器上运行一个多线程的进程,系统将在多个线程之间切换处理器资源,即轮流为这些线程分配时间片,这就是并发执行。在共享内存的多处理器环境中运行同样的多线程进程,所有线程可以在不同的处理器上同时运行,这就是并行执行。当进程中的线程数小于或等于系统中的处理器个数时,系统和操作环境将保证每个线程都运行在不同的处理器上。例如,一个矩阵乘法程序有n个线程,这个程序运行在一个有n个处理器的计算机上,每一个线程(在每个处理器上)负责计算一行的结果。多线程结构其实传统的UNIX也支持线程这个概念,但是一个进程只包括一个线程。所以多进程程序设计也就是多线程程序设计,但一个进程就意味着一个独立的地址空间,创建一个进程就意味着创建一个新的地址空间。创建一个线程比创建一个进程要简单而且“便宜”(使用更少的时间资源和空间资源)。因为新建的线程使用当前进程的地址空间,同一进程内的线程间切换所用的时间要比进程间切换所用的时间短得多,部分原因是前者不需要切换地址空间。在同一进程内的线程之间进行通讯比较简单,这是因为同一进程内的线程共享所有资源,特别是地址空间,也就是说,一个线程产生的数据可以立即被同一进程中的其他线程所访问。线程支持接口是通过一个例程库提供的。对于POSIX线程接口,它就是libpthread。对Solaris线程接口是libthread。通过区分核心级资源和用户级资源,多线程技术变得更加灵活。用户级线程(相对应的是核心级线程,系统程序员更关心核心级线程,由于这本书是面向应用程序员的,所以本书中没有讨论核心级线程)线程是多线程程序设计的基本接口。用户级线程被放在用户空间中处理,以避免内核上下文切换造成的性能恶化。一个应用程序可以拥有多达上百个用户级线程而不会消耗太多的核心资源。应用程序使用多少核心资源由应用程序本身决定。用户级线程只在一定的进程中才是可见的,线程共享所在进程的资源,包括地址空间,打开的文件等等。但每一个线程都各自具有下列状态量:l 线程标识符l 寄存器状态(包括当前指令指针和栈指针)l 栈l 信号掩码l 优先级l 线程的私有内存因为线程共享所在进程的指令和大部分的进程数据,所以一个线程修改的共享数据可以被同一进程内的其他线程访问。当一个线程需要和同一进程内的其他线程通讯时,它可以不使用系统资源,而是通过进程的共享数据(如全局变量)来完成。缺省情况下,线程是很“轻”(即占用很少的系统资源)的。但是,为了对线程进行更多的控制(例如,控制线程的调度策略),应用程序可以绑定线程。当应用程序把线程和执行资源绑定在一起时,线程就变成了核心资源。总结一下,用户级线程:l 方便创建,不需要为它们创建地址空间,它们只是运行时在虚拟内存中分配的一小段内存。l 可以进行快速的同步,由于同步是在用户空间中完成的。l 可以由线程库进行简单的管理,相应的线程库是libpthread和libthread。轻进程线程库使用由内核支持的控制流程(称为轻进程)来完成其功能。我们可以把一个轻进程(即LWP)当作一个执行代码或系统调用的虚拟CPU。使用线程进行程序设计时,我们通常不需要考虑LWPs。这里对LWP的描述是为了使你有一个背景知识,使你在阅读 “进程域(非绑定线程)”时,能理解不同调度域的区别。注意:在Solaris 2和Solaris 7中的LWPs和SunOS(tm) 4.0 LWP库中的LWPs是不同的。后者在Solaris 2和Solaris 7中是不支持的。线程接口使用LWP接口,就像stdio库例程(比如fopen()和fread())要使用open()和read()一同调用一样。轻进程(LWPs)连接了用户层和核心层。每一个进程包括一个或多个LWP,每一个LWP运行一个或多个用户线程。创建线程的时候,通常创建一些线程环境,而不创建LWP。每一个LWP都是在某个内核池中的一个内核资源。当线程被创建、调度或终止时,系统会为线程分配(挂上)LWP,或释放(卸载)LWP。调度POSIX标准定义了三种调度策略:先进先出(SCHED_FIFO),循环(SCHED_RR)和自定义(SCHED_OTHER)。SCHED_FIFO是一种基于队列的调度算法,它为每一个优先级维护一个队列。SCHED_RR和先进先出法很相似,但每个线程都有一个执行时间的限额。SCHED_FIFO和SCHED_RR都是POSIX标准为实时系统定义的扩展机制。而SCHED_OTHER才是缺省的调度策略。如果你想深入了解SCHED_OTHER,SCHED_FIFO,SCHED_RR调度策略,请阅读 “LWPs 和调度类型”。系统中有两种调度域:非绑定线程所处的进程域和绑定线程所处的系统域。在一个系统中、甚至一个进程中都会有处在不同调度域的线程。线程所处的调度域决定了线程调度策略的有效范围。进程域(非绑定线程)创建非绑定线程时,请使用PTHREAD_SCOPE_PROCESS。这种线程在进程的用户空间中通过挂上和卸载LWP池中的LWPs来完成调度。池中的LWPs只能被这个进程中的线程使用;也就是说,线程在这些LWPs上进行调度。在大部分情况下,线程都应该是PTHREAD_SCOPE_PROCESS的。这就是说,线程可以在LWPs间移动,这大大改善了线程的性能(这等价于创建了一个为THR_UNBOUND状态的Solaris线程)。由线程库通过对LWPs的分配和释放来决定内核为哪个线程服务。系统域(绑定线程)创建绑定线程时,请使用PTHREAD_SCOPE_SYSTEM一个绑定线程和一个LWP固定的挂接在一起。每一个绑定线程一直被绑定在一个LWP上,一直到线程结束。这样的线程和被定义为THR_BOUND的Solaris线程是等价的。通过绑定线程,你可以为线程指定新的信号栈或为线程指定特殊的调度属性。对这类线程的调度由操作系统完成。注意:不管是绑定或非绑定线程,都不能被另一个进程直接访问或操作。退出线程退出机制允许一个线程终止本进程中的任何一个线程。被终止的线程可以先挂起终止请求,直到它完成了退出前必须的清理工作。POSIX线程机制允许对线程的异步终止(asynchronous termination)或延迟终止(deferedtermination)。异步终止可以发生在任何时间,而延迟终止只能发生在被终止线程中预先定义好的位置上。延迟终止是缺省的终止类型。同步同步允许你控制程序流程,控制在并发的多线程中对共享数据的访问。l 四个主要的同步方式是互斥锁,条件变量,信号量和读写锁。l 互斥锁允许在一段时间内只能有一个线程执行特定的代码或访问特定的数据。l 条件变量阻塞一个线程的运行,直到一个特定条件成立。l 计数信号量一般用来协调对资源的访问。信号量的计数值表示有多少个线程可以同时访问这个信号量。当计数达到了这个值,相应的信号量就被阻塞了。l 读写锁允许多个线程只读访问一个共享资源。如果需要修改这个资源,线程必须先写锁定这个读写锁。使用64位体系结构对应用程序的开发者来说,Solaris 64位操作系统和32位操作系统之间的最大差别在于它们使用的C语言数据类型模型。64位操作系统使用LP64模型,在LP64模型中long类型和指针类型是64位的。其他基本数据类型和32位的模型一样。32位数据类型使用ILP32模型,其中的int,long和指针类型都是32位的。下面列出了64位环境的主要特点和使用时需要考虑的问题。l 巨大的虚拟地址空间在64位环境中,一个进程可以有多达64位宽的虚拟地址空间,或18 exabytes(18*260字节)。这是32位环境中4G虚拟地址空间的四十亿倍。由于硬件的限制,有的64位平台无法完全支持64位的地址空间。大地址空间使得系统可以创建更多的线程,在32位平台上一个缺省的线程需要1M堆栈,在64位平台上一个缺省的线程需要2M堆栈。在32位平台上可以创建4000个缺省线程,在64位平台上可以创建8万亿个缺省线程。l 使用核心内存的程序由于系统核心内部也使用64位的数据结构,所以现存的程序,如果使用了libkvm,/dev/mem或/dev/kmem,将无法再在64位环境中运行。必须将这样的程序转变为64位的程序。l /proc的限制一个使用/proc的32位应用程序可以访问32位进程的属性,但无法访问一个64位进程的属性;现存的描述进程的接口和数据结构不能包含所涉及的64位的量。这种程序必须重新编译成64位应用程序,这样才能访问32位和64位进程的属性。l 64位库32位的应用程序必须和32位的库链接在一起,64位的应用程序必须和64位的库链接在一起。除了过时的库,所有的库都有32位和64位两种版本。但没有一个64位库是静态链接库。l 64位运算尽管在32位的Solaris系统中已经有64位运算了,但64位的实现为整数操作和参数传递提供了完全64位的机器寄存器。l 大文件如果一个程序只需要支持大文件,使用32位Solaris的大文件接口就可以了。但是为了充分的利用64位的优点,最好把程序转变为64位代码。
    发布人:gloomy 来自: