linux 多线程编程指南(七)

5 篇文章 0 订阅
3 篇文章 0 订阅


5.4库的多线程安全等级
    所有可能被多线程程序的线程调用的函数都应当是MT-Safe的。
    这意味着过程可以同时正确地执行两个操作。所以,每一个被多线程程序
使用的接口都应是MT-Safe。
    并不是所有的库都是MT-Safe的。通常被使用的MT-Safe的库详列于表5-2中。

其他的库也将最终被改写成MT-Safe的。
        表5-2 一些MT-Safe库
------------------------------------
库                                      说明
------------------------------------
lib/libc                getXXbyYY接口(例如gethostbyname(3N))是MT-Safe的
lib/libdl_stubs         (支持static switch compiling)
lib/libintl
lib/libm                仅当为共享库编译时是MT-Safe的,但与文档库连接时
                        不是MT-Safe的
lib/libmalloc
lib/libmapmalloc
lib/libnsl              包括TLI接口,XDR,RPC客户方和服务方,netdir和
                        netselect。 GetXXbyYY是不安全的,但有线程安全版本
                        GetXXbyYY_r
lib/libresolv           支持因线程而异的错误码
lib/libsocket
lib/libw
lib/nametoaddr
lib/nametoaddr
lib/nsswitch
libX11
libC                    (不是Solaris系统的部分;可以分开购买)
------------------------------------

*不安全库
    如果库中的函数不是MT-Safe的,则只有在一个线程的调用时才是安全的。

6 编译和调试 本章描述了怎样编译和调试多线程程序。 编译一个多线程应用程序 调试一个多线程应用程序 6.1编译一个多线程应用程序 6.1.1使用C编译器 确认你拥有如下软件,否则将无法正常编译和连接多线程程序 · 头文件:thread.h errno.h · 标准C编译器 · 标准Solaris连接器 · 线程库(libthread) · MT-Safe库(libc, libm, libw, libintl, libmalloc, libmapmalloc, libnsl, 等等) 6.1.2用替代(_REENTRANT)标志进行编译 在编译多线程程序时使用"-D _REENTRANT"标志。 这个标志必须在编译应用程序的每一个模块时都使用。如果没有这个标志,将 使用errno, stdio等等的旧的定义。如果要编译一个单线程应用程序,不要使用这 个标志。 *新旧连接需要小心 表6-1显示了多线程目标代码模块与旧的代码模块连接时需要非常慎重。 表6-1 在编译多线程程序时使用"-D _REENTRANT"标志 文件类型 编译 参考 返回 ---------------------------------------------------------------------- 旧的目标文件(非线程版) 和新的目标文件 没有 "- 静态 储存 传统的errno D _REENTRANT" 标志。 ---------------------------------------------------------------------- 新的目标文件 有 "-D _REENTRANT"标志。 __errno,新的二进制入口 ---------------------------------------------------------------------- 线程 定义errno的地址 ---------------------------------------------------------------------- 用libnsl 里的TLI编程 有 " -D _REENTRANT"标志(必须)。 __t_errno,一个新的入口 线程定义t_errno的地址 ---------------------------------------------------------------------- 6.1.3使用libthread 为了在连接时使用libthread,需要在ld命令行里,-lc参数之前,指定 -lthread,或者在cc 命令行的最后指定。 如果应用程序没有连接libthread,则对该库中的函数调用不产生实际操作。 Libc定义libthread为空过程。???真正的过程是在应用程序既连接libc也 连接libthread时由libthread加入的。 如果一个ld命令行包含了以下的字段:.o's ... -lc -lthread ...,则C函数 库的行为没有被定义。??? 不要在单线程程序中使用-lthread。这样做将在连接时建立多线程机制,在运 行时将被初始化。这样做不但浪费资源,而且在调试中会对运行结果有不正确的显 示。 6.1.4使用非C的编译器 线程库使用libc中的如下内容: · 系统调用包装器(system call wrappers) · 用来显示出错信息的调用(通常是printf) · 运行时的连接支持来解析符号(因为库是动态连接的) 你也可以写自己的系统调用包装器和自己的printf函数,并且在连接时(而不 是在运行时)进行符号解析,这样可以消除对libc的依赖。 如果线程使用应用程序提供的堆栈,则线程库不使用动态分配内存的办法。 Thr_create(3T)函数可以由应用程序指定自己的堆栈。 6.2调试多线程应用程序 6.2.1一般的疏漏 以下列出可以导致多线程出错的常见疏漏: · 给新线程传递参数时使用局部或全局变量 · 在没有同步机制的保护下访问全局内存 · 两个线程以不同的顺序去申请两个资源导致死锁(两个线程各自占有一个资源 并相执不下) · 在同步保护中有隐藏的漏洞。例如可能有如下情况:一个有同步机制(例如互斥 锁)保护的代码段包含一个先释放再重新获得同步机制的函数调用,结果是全局内存 实际上没有得到保护。 · 有隐匿的,重复或递归的大自动数组的使用可能导致问题,因为多线程程序的堆 栈容量比单线程程序有更多的限制。 · 指定的堆栈空间不够。 · 没有通过线程库的调用指定堆栈。 注意,多线程程序(特别是有错误的)经常在相同输入的情况下得到不同的结 果,因为线程调度的顺序不同。 一般的,多线程bug具有统计性,而不是确定性。在调试时,跟踪的办法将会比 设断点的办法好些。 6.2.2使用adb 如果你在一个多线程程序当中绑定所有线程,一个线程和一个LWP是同步的。 然 后你通过如下支持多线程编程的adb命令访问每一个线程。 表6-2 MT adb命令 ------------------------------------- pid:A 绑定在进程pid上,这将停止进程及其所有LWP :R 与进程分离,这将恢复进程及其所有LWP $L 显示在(停止的)进程中所有的活动的LWP n:l 将焦点切换到第n号LWP $l 显示当前焦点所在的LWP num:i 忽略信号码为num的信号 6.2.3使用dbx 使用dbx,可以调试和执行用C++, ANSI C, FORTRAN和PASCAL的源程序。Dbx使 用与SPARCworks? Debugger相同的命令,但使用标准终端(tty)接口。Dbx和 SPARCworks Debugger现在都支持多线程程序。 要得到dbx和Debugger的全面认识,请参考SunPro dbx(1) man page和 《Debugging a Program》用户指南。 以下的dbx选项支持多线程。 表6-3 给MT程序使用的dbx选项 Cont at line[sig signo id] 在信号signo发生时继续执行第line行。 参见dbx的命令语言的循环控制里的continue。 如果有id参数,则指定继续哪一个线程或LWP。 缺省设置为all。 Lwp 显示当前LWP。切换到给定LWP[lwpid] Lwps 列出当前进程的所有LWP Next … tid 单步执行指定线程。如果一个函数调用被跳过, 所有的LWP在该函数调用期间重新开始???非 活动线程不能被单步执行 Next … lid 单步执行指定LWP。在跳过函数时并不隐含地恢 复所有的LWP。在该LWP上的线程是活动的。 Step … tid 单步执行指定线程。如果一个函数调用被跳过, 所有的LWP在该函数调用期间重新开始???非 活动线程不能被单步执行 Step … lid 单步执行指定LWP。在跳过函数时并不隐含地恢 复所有的LWP。 Stepi … lid 指定的LWP Stepi … tid 在LWP上的线程是活动的。 Thread 显示当前线程。切换到线程tid。在以下情况中, 一个可选的tid指当前线程。 Thread -info[tid] 打印指定线程的所有已知情况。 Thread -locks[tid] 打印被指定线程控制的所有锁 Thread -suspend[tid] 把指定线程置于挂起状态。 Thread -continue[tid] 使指定线程退出挂起状态。 Thread -hide[tid] 隐藏指定(或当前)线程,在普通线程列表中 将不被显示出来 Thread -unhide [tid] 解除指定线程的隐藏状态 Allthread-unhide 解除所有线程的隐藏状态 Threads 打印已知线程的列表 Threads-all 打印所有线程(包括通常不被打印的,zombies) All|filterthreads-mode 控制threads命令打印所有线程还是有选择地列表 Auto|manualthreads-mode 使在GUI界面里线程监控器(Thread Inspector) 线程列表得以自动更新 Threads -mode 显示当前模式。Any of the previous forms can be followed by a thread or LWP ID to get the traceback for the specified entity.

7 编程指南 本章给出线程编程的一些要点。特别强调了单线程和多线程编程方法的差别。 重新认识全局变量 提供静态局部变量 线程同步 避免死锁 一些基本的注意事项 用多处理器编程 在历史上,大多数代码以单线程的方式来编程。如果在C程序里调用库函数则 尤其是这样: · 如果你给全局变量赋值,并且在一会以后读该变量,则读的结果和写的是一样的。 · 对于非全局的,静态存储也是如此 · 不需要同步机制,因为没有什么可以同步的 在下面的几个多线程例子当中讨论了采用以上假设将会发生的问题,以及你 如何处理这些问题。 7.1重新认识全局变量 传统情况下,单线程C和UNIX有处理系统调用错误的传统。系统调用可以返回 任何值(例如,write()返回传输的字节数)作为功能性的返回值。然而,-1被保留, 它意味着出错。所以,如果一个系统调用返回-1,你就知道是失败了。 Code Example 7-1 全局变量和错误码errno Extern int errno; … if(write(file_desc,buffer,size)==-1){ /* the system call failed */ fprintf(stderr,"something went wrong, error code = %d/n", errno); exit(1); } … 函数并不直接返回错误码(将会和正常的返回值混淆),而是将错误码放入一个 名为errno的全局变量中。如果一个系统调用失败,你可以读errno来确定问题所在。 现在考虑在多线程程序中,两个线程几乎同时失败,但错误码不同。他们都希望 在errno中寻找问题,但一个errno不能存放两个值。这个全局变量不能被多线程程序 使用。 Solaris线程包通过一种在概念上全新的存储类型解决了这个问题--线程专有数据。 与全局变量类似,在线程运行时任何过程都可以访问这块内存。然而,它是线程私有 的--如果两个线程参考同名的线程专有存储区,它们实际上是两个存储区。 所以,如果使用线程,每个对errno的操作是线程专有的,因为每个线程有一个 errno的私有拷贝。 7.2提供给静态局部变量 示例7-2显示了一个类似与errno错误的问题,但涉及到静态存储,而不是全局存 储。Gethostbyname(3N)函数用计算机名称作为参数。返回值是一个指向结构的指针, 该结构包含通过网络访问指定计算机的必要信息。 Code Example 7-2 gethostbyname问题 Struct hostent *gethostbyname(char *name){ Static struct hostent result; /*lookup name in hosts database */ /*put answer in reault*/ return(&result); } 返回指向自动局部变量不是一个好的选择,尽管在这个例子是可以的,因为所指 定的变量是静态的。但是,如果两个线程用不同的计算机名同时访问这个区域,对静 态存储的使用就会发生冲突。 线程专有数据可以代替静态储存,就象在errno问题中那样,但是这涉及到动态 分配内存,并且增加了调用的开销。 一个更好的办法是调用者提供存放数据的存储区。这需要在函数调用中增加一个 参数,一个输出参数。即需要一个gethostbyname的新的版本。 在Solaris里,这种技术被用来处理很多类似问题。在大多数情况下,新接口的 名字都带有"_r"后缀,例如gethostbyname_r(3N)。 7.3线程同步 应用程序中的线程在处理共享数据和进程资源是必须使用同步机制。 在多个线程控制一个对象时会出现一个问题。在单线程世界里,对这些对象的同 步访问不是问题,但正如示例7-3所示,在多线程编程中需要注意。(注意 Solaris printf(3S)对多线程程序是安全的;此例说明如果printf不安全将会发生的问题 。) Code Example 7-3 printf()问题 /*thread 1*/ printf("go to statement reached"); /*thread 2*/ printf("hello world"); printed on display: go to hello 7.3.1单线程策略 一个办法是采用单一的,应用程序范围有效的互斥锁,在调用printf时必须使用 互斥锁保护。因为每次只有一个线程可以访问共享数据,每个线程所见的内存是一致 的。 Because this is effectively a single-threaded program, very little is gained bythis strategy. 7.3.2重入(reentrant)函数 更好的办法是采用模块化和数据封装的思想。一个替代函数被提供,在被几个线 程同时调用时是安全的。写一个替代函数的关键是搞清楚什么样的操作是"正确的"。 可以被几个线程调用的函数一定是重入的。这也许需要改变函数接口的实现。 访问全局状态的函数,例如内存和文件,都存在重入问题。这些函数需要用 Solaris线程提供的正确的同步机制来保护自己访问全局状态。 两个保证函数重入的基本策略是代码锁和数据锁。 7.3.2.1代码锁 代码锁是函数调用级的策略,它保证函数完全在锁的保护下运行。该策略假设对 数据的所有访问都是通过函数。共享数据的函数应当在同一个锁的保护下执行。 有些并行编程语言提供一种名为监视器(monitor)的机制,在monitor的内部, 函数的代码被隐含地用所来保护。一个monitor也可以用互斥锁实现 7.3.2.2数据锁 数据锁保证对数据集合(collection of data)维护的一致性。对于数据锁,仍 然有代码锁的概念,但代码锁是仅仅围绕访问共享数据进行。对于一个互斥锁协议, 仅有一个线程来操作每一个数据集合。??? 在多读单写协议当中,几个读操作或一个写操作可以被允许。在操作不同的数据 集合,或者在同一个数据集合上不违反多读单写的协议的前提下,一个模块中的多个 线程可以同时执行。所以,数据锁比代码锁提供了更多的同时性。 如果你需要使用锁的话,你要用哪一种(互斥锁,条件变量,信号量)呢?你需 要尝试只在必要时加锁来允许更多的并发呢(fine-grained locking 细纹锁),还 是使锁在相当一段时间内有效来避免加锁和释放锁的额外开销呢(coarse-grained locking 粗纹锁)? 锁的纹理(可以理解成加锁和释放锁的频率,频率越高则纹理越细--译者注)依 赖于所保护的数据量。一个粗纹锁可以是一个保护所有数据的单一的锁。把数据由适 当数量的锁分开来保护是很重要的。如果纹理过细可能会影响性能,过多的加锁和解 锁操作会累计到相当的程度。 普通的做法是:用一个粗纹锁开始,找到限制性能的瓶颈,然后在需要时加入细 纹锁来减缓瓶颈。看上去这是一个合理的办法,但你需要自己判断来达到最好效果。 7.3.2.3不变量 不论是代码锁还是数据锁,不变量对于控制锁的复杂度都具有重要的意义。一个 不变量是一个永真的条件或关系。 这个定义在应用在同时执行时需要进行一定的修改:一个不变量是一个永真的条 件或关系,如果相关的锁尚未设置。一旦锁被设置,不变量就可能为假。然而,拥有 锁的代码在释放所前一定要重新建立不变量。 一个不变量也可以是永真的条件或关系,如果锁尚未设置。条件变量可以被认为 拥有一个不变量,那就是它的条件。 Code Example7-4 用assert(3X)来测试不变量 mutex_lock(&lock); while(condition) cond_wait(&cv); assert((condition)==TRUE); . . . mutex_unlock(); Assert()命令是测试不变量的。Cond_wait()函数不保护不变量,所以线程返回 时一定要重新估价不变量。 另外一个例子是一个控制双链表元素的模块。对链表中每一个组件,一个好的不 变量是指向前项的指针,以及指向其后项的指针。 假设这个模块使用代码锁,即仅仅用一个全局互斥锁进行保护。如果某一项被删 除或者某一项被添加,将会对指针进行正确操作,然后释放互斥锁。显然,在操作指 针的某种意义上不变量为假,但在互斥锁被释放之前不变量会被重新建立。
7.4避免死锁 死锁是一系列线程竞争一系列资源产生的永久阻塞。某些线程可以运行并不说明 其它线程没有死锁。 导致死锁的最常见的错误是自死锁(self deadlock)或者递归死锁(recursive deadlock):一个线程在拥有一个锁的情况下试图再次获得该锁。递归死锁是编程 时非常容易发生的错误。 例如,如果一个代码监视器在调用期间让每一个模块的函数都去获得互斥锁,那 么任何在被互斥锁保护的模块之间调用的函数都将立即导致死锁。如果一个函数调用 模块以外的一些代码,而这些代码通过一个复杂或简单的路径,又反过来调用该模块 内部被同一互斥锁保护的函数,也会发生死锁。 解决这种死锁的办法是避免调用模块以外的函数,如果你并不知道它们是否会在 不重建不变量的情况下回调本模块并且在调用之前丢弃所有已获得的锁。当然,在调 用完成后锁会重新获得,一定要检查状态以确定想要进行的操作仍然合法。 死锁的另外一种情况是,线程1和线程2分别获得互斥锁A和互斥锁B。这时线程1 想获得互斥锁B,而同时线程2想获得互斥锁A。结果,线程1阻塞等待B,而线程2阻塞 等待A,造成死锁。 这类死锁可以通过为互斥锁编排顺序来避免(锁的等级 lock hierarchy)。如 果所有线程通过指定顺序申请互斥锁,死锁就不会发生。 为锁定义顺序并非最优的做法。如果线程2在拥有互斥锁B时对于模块的状态有很 多的假设,则放弃互斥锁B来申请互斥锁A,然后再按照顺序重新申请互斥锁B将导致 这些假设失去意义,而不得不重新估价模块的状态。 阻塞同步原语通常有不阻塞的版本,例如mutex_trylock()。它允许线程在没有 竞争时打破锁等级。如果有竞争,已经获得的锁通常要释放,然后按照顺序来申请。 7.4.1死锁调度 因为锁的获得没有顺序的保证,一个线程编程的普遍问题是一个特定线程永远不 会得到一个锁(通常是条件变量),即使它看上去应当获得。 这通常发生在拥有互斥锁的线程释放了锁,在一段时间之后又重新获得了这个锁。 因为锁被释放了,似乎其他线程会获得这个锁。但是因为没有谁能阻塞这个已经获得 了锁的线程,它就继续执行到重新获得互斥锁的时候,这样其他线程无法进行。 通常可以用在重新获得锁之前调用thr_yield(3T)来解决这一类型的问题。它允 许其它线程运行并获得锁。 因为应用程序需要的时间片变化很大,线程库不能做强制规定。只有调用 thr_yield()来保证线程按你需要的那样共享资源。 7.4.2加锁的注意事项 以下是有关锁的一些简单的注意事项。 · 在长时间的操作(例如I/O)上尽量不要加锁,这样会对性能造成负影响。 · 在调用可能重新进入本模块的函数时不要加锁。 · 不要尝试极端的处理器同时性。在不涉及系统调用和I/O操作的情况下,锁通常只 被线程占有很短的时间,冲突是很少发生的。只有在确知竞争情况时,才能长时间占 有一个锁。 · 如果使用多个锁,用锁等级来防止死锁。 7.5遵循基本的注意事项 · 搞清你引入的内容以及它们是否安全。 一个线程程序不能任意进入非线程代码。 · 只有在初始线程中线程代码才可以调用非安全代码。 这保证了与初始线程关联的静态存储只能被该线程使用。 · Sun提供的库如果没有明确地标识为unsafe,则被定义为safe。 如果man page不声称函数是MT-Safe的,则它是安全的。所有的MT-unsafe函 数都在man page里明确标出。 · 使用编译标志来控制二进制不兼容的源代码改变。 在编译时指定-D_REENTRANT或者保证在头文件中定义_REENTRANT。 · 如果一个库是多线程安全的,不要将全局进程操作线程化。??? 不要把全局操作(或者有可能影响全局的操作)改成线程风格。例如,如果 文件的I/O操作被设置为线程级的操作,多个线程将不能正确访问文件。 对于线程级的操作,或者thread cognizant的操作,要使用线程工具。例如,如 果main()函数仅仅终止正在退出main函数的那个线程,则main()函数的结尾应当为 thr_exit(); /*not reached */ 7.5.1创建线程 Solaris线程包对线程数据结构,堆栈以及LWP设置缓存,这样重复创建非绑定线 程开销会降低。 非绑定线程的创建比其进程创建或者绑定线程的创建来开销都要小的多。实际上, 这种开销和从一个线程切换到另外一个线程的开销是相当的。 所以,在需要时不断地创建和清除线程,比起维护一个等待独立任务的线程池通 常要划算一些。 一个好的例子是,一个RPC服务器的工作方式是为监听到的每一个请求创建一个 线程,并且在提供服务后清除这个线程,而不是维护很多线程来提供服务。 虽然线程创建比进程创建开销要小,但是比起几条指令来代价并不低。因此仅在 需要执行几千条以上机器指令时才考虑创建线程。 7.5.2线程同时性 在缺省状态下,Solaris线程通过对非绑定线程调整执行资源(LWP)来实现对实 际活动的线程数的匹配。如果说Solaris线程包不能进行完美的调度,它至少可以保证 进程继续运行。 如果你需要让一定数量的线程同时处于活动状态(执行代码或系统调用),需要 通过thr_setconcurrency(3T)来通知线程库。 例如: · 如果一个数据库服务器给每一个用户开设一个服务线程的话,它应当把期望得到的 用户数目告诉操作系统Solaris。 · 如果一个窗口服务器给每一个客户开设一个线程,它应当把期望的活动客户端的 数目通知Solaris。 · 一个文件拷贝程序拥有一个读线程和一个线程,它应当通知Solaris它的同时性等级 为2。 或者,同时性等级可以在创建线程时使用THR_NEW_LWP标志来增加。 在计算线程的同时性时,需要把因为进程间的同步变量而处于阻塞状态的线程考虑 进来。 7.5.3效率 用thr_create(3T)创建一个新线程比重新启动一个线程花费的时间少。这意味 着,在需要时创建一个线程并且在任务结束后用thr_exit(3T)立刻杀掉它,比起维护一 大堆的空闲线程并且在它们中间切换要划算的多。 7.5.4绑定线程 绑定线程比起非绑定线程的开销要大。因为绑定线程可以改变它所在的LWP的属性, LWP在绑定线程退出后不会被缓存,在新的绑定线程生成时,操作系统将提供一个新的 LWP。 仅仅在线程需要只有在所在的LWP内可用的资源时(例如虚拟的定时器或者一个指定 的堆栈),或者为了实现实时调度而必须使线程对于内核可见的场合下,才需要使用绑 定线程。 即使在你希望所有的线程都同时活动时,你也应当使用非绑定线程。因为非绑定线程 允许Solaris高效率地分配系统资源。 7.5.5线程创建指南 在使用线程时有如下简单的注意事项: · 在有多个执行大量任务的操作时,使用多线程编程。 · 使用线程来更好地利用CPU的同时性。 · 只有在不得已的情况下再使用绑定线程,就是说,需要LWP的特殊支持的时候。 使用thr_setconcurrency(3T)来告诉Solaris你希望有多少个线程同时执行。 7.6关于多处理器 Solaris线程包使你能够充分利用多处理器。在很多情况下,程序员必须关心程序 是在单处理器还是在多处理器的环境下运行。 这样的情况下涉及到多处理器的内存模型。你不能够假设一个处理器对内存所做的 改变可以被另一个处理器立即看到。 另一个与多处理器有关的问题是如何实现"多个线程在到达同一点后再向下执行"的 有效同步。 -------------------------------------- 注意-如果同步原语已经被应用于共享的内存,这里讨论的问题将不重要。 -------------------------------------- 7.6.1基本建构 如果多个线程对共享存储区的访问使用的是Solaris的线程同步函数,那么程序在 多处理器的环境和单处理器的环境下的运行效果是一样的。 然而,在很多情况下,有些程序员希望更充分地发挥多处理器的优势,希望使用一 些"巧妙"的办法避开线程同步函数。如示例7-5和7-6所示,这样的办法是危险的。 了解一般的多处理器结构支持的内存模型有助于了解这种危险。 主要的多处理器组件是: 处理器本身 CPU缓冲区(Store buffers),它连接处理器和其高速缓存(caches) 高速缓存(caches),保存最近访问和修改过的存储地址 内存(memory),主要存储器,被所有的处理器共享 在简单的传统模型里,多处理器的操作就象是直接与内存打交道:一个处理器A 向一个内存单元写数后,另一个处理器B立刻读取该单元,取出的数一定是处理器A刚 刚写入的。高速缓存可以被用来加快平均的内存访问速度,如果高速缓存之间保持一 致的话,的确可以达到期望的效果。 这种简单方法的一个问题在于,处理器必须有一定的延迟来保证期望的语义效果 实现。许多新的多处理器结构使用各种办法来减少这种延迟,结果不得不改变了内存 模型的语义。在如下的两个例子当中,我们会解释两个这种技术和它们的效果。 7.6.1. 1"共享内存"的多处理器系统 考虑示例7-5的生产者/消费者解决方案。尽管这个程序在现在的SPARC多处理器 系统上是可行的,但它假定了所有的多处理器系统都有高度有序的内存,因而是不 可移植的。 示例7-5 生产者/消费者问题--共享内存的多处理器 char buffer[size]; unsigned int in=0; unsigned int out=0; void producer(char item){ do ;/*nothing*/ while (in - out == BSIZE); buffer[in%BSIZE] = item; in++; } char consumer(void){ char item; do ;/*nothing*/ while (in - out == 0); item = buffer[out%BSIZE]; out ++; } 如果这个程序仅有一个生产者和一个消费者,并且运行在一个共享内存的多 处理器系统当中,它似乎是正确的。In和out的差别是缓冲区中的产品数。生产者 等待直到有空位置,消费者等待直到缓冲区中有产品。 对于高度有序的内存(例如,一个处理器对内存的改变立刻对另一个处理器 生效),这种解决是正确的(即使考虑到in和out将最终溢出,程序仍然是正确的, 因为BSIZE比word型数据能表示的最大整数要小)。 共享内存的多处理器系统不一定拥有高度有序的内存。一个处理器对于内存 的改变未必会立刻通知其他处理器。如果一个处理器改变了两处内存,其他处理 器不一定看到两处改变按照预期的顺序发生,因为对内存的改变不是立即执行的。 写操作首先保存在CPU缓冲区里,对高速缓存是不可见的。处理器自己对这些缓冲 数据的维护是可靠的,但它对其它的处理器不可见,所以在数据写到高速缓存之 前,其他处理器认为该写操作没有发生。 Solaris同步原语(见第三章)使用特殊的指令将数据从CPU缓冲区写入高速 缓存。这样,在共享数据的访问前后加上锁的保护,就可以保证内存的一致性。 如果内存的顺序保护非常松散,在示例7-5中,消费者看到in变量被生产者 增加时,也许产品item还没有放入产品缓冲区。这种情况被称为weak ordering (弱顺序),因为一个处理器的操作在另一个处理器看来是打乱次序的(但内存对 同一个处理器总是保持一致)。解决这个问题的办法是使用互斥锁来强制更新高 速缓存。 现在的处理器趋向于"弱顺序"。因此,程序员必须在操作全局或共享内存时 使用锁。象在示例7-5和7-6中那样,锁是必不可少的。 7.6.1.2彼得森算法(Peterson's Algorithm) 示例7-6是Peterson's Algorithm的一个实现,它控制两个线程的互斥。这段 代码试图保证一个时间最多只有一个线程在执行关键代码,进而当一个线程调用 mut_excl()时,它在很"近"的一个时刻进入该关键代码。 假定线程进入关键代码后很快退出。 示例7-6 两个线程的互斥? Void mut_excl(int me /*0 or 1*/){ Static int loser; Static int interested[2]={0,0}; Int other;/* local variable */ Other = 1-me; Interested[me]=1; Loser=me; While (loser == me && interested[other]); /* critical section */ interested[me]; } 这个算法在多处理器有高度有序的内存时,可能是可以正确运行的。 一些多处理器系统,包括一些SPARC系统,都有CPU缓冲区。如果一个线程发出 一条存储指令,数据被放入CPU缓冲。这些数据最终都被送往高速缓存,但不是立刻 (注意高速缓存对其它的处理器来说是可见的,一致的,但数据并非立刻写入高速 缓存)。 如果同时写入多个内存地址,这些改变将按顺序到达高速缓存和内存,但有一 定延迟。拥有这种属性的SPARC多处理器系统被称为全存储顺序 (TSO:Total Store Order)。 如果某个时间一个处理器向A地址写,然后读取B地址,而另一个处理器向B地址 写,然后读取A地址,其预期结果是或者第一个处理器得到B的新值,或者第二个处 理器得到B的新值,或者二者都得到,但不会发生两个处理器都得到旧值的情形。 但是,由于从CPU缓冲区存取的延迟存在,这种不可能的事情就会发生。 在Peterson's Algorithm算法中可能发生的是:两个线程分别在两个处理器上 运行,数据都保存在CPU缓冲区中,然后读取另外一个,他们看到的都是旧的值( 0), 表示另外的线程当前不在关键代码中,所以他们共同进入关键代码(注意,这种问 题在你测试的时候可能不会表示出来,但它是可能发生的)。 如果你用线程同步原语,就可以避免这个问题。同步原语强制地将CPU缓冲区 的数据写入高速缓存。 7.6.1.3在共享内存的并行计算机里并行一个循环 在很多应用程序中,特别是数值计算,算法的某些部分可以并行,而另外一 些部分必须顺序执行(如示例7-7所示) Code Example7-7多线程合作(Barrier同步) While(a_great_many_iterations){ Sequential_computation Parallel_computation } 例如,你也许通过严格的线形计算获得了一个矩阵,然后对这个矩阵进行并 行计算,再用运行结果创建另外一个矩阵,然后再进行并行计算,等等等等。 此类计算的并行算法的特点是在计算时不需要太多同步,但使用结果时必须保 证结果都已得出。 如果执行并行计算的时间远比创建和同步线程的时间长,那么线程的创建和 同步不成问题。但是如果运算时间不是很长,线程的创建和同步时间就显得非常重 要。 7.2小结 这个编程指南包含了线程编程的基本注意事项。参看附录A"示例应用程序",可 以看到很多讨论过的特点和风格。 推荐读物: Algorithms for Mutual Exclusion by Michel Raynal (MIT Press, 1986) Concurrent Programming by Alan Burns & Geoff Davies(Addison-Wesley, 1993) Distributed Algorithms and Protocols by Michel Raynal (Wiley, 1988) Operating System Concepts by Silberschatz, Peterson, & Galvin (Addison-Wesley, 1991) Principles of Concurrent Programming by M. Ben-Ari (Prentice-Hall, 1982) Mccartney

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值