本章介绍如何编译和调试多线程程序。本章论述以下主题:
编译和链接多线程程序时,需要以下项。Solaris 软件应包括除 C 编译器以外的所有项。
标准 C 编译器
包括以下文件:
Solaris 线程库 (libthread)、POSIX 线程库 (libpthread),可能还有信号的 POSIX 实时库 (librt)(仅限于 Solaris 9 和以前的发行版)
MT 安全库,如 libc、libm、libw、libintl、libnsl、libsocket、libmalloc、libmapmalloc 等
某些函数在 POSIX 标准中的语义与在 Solaris 2.4 发行版中的语义是不同的,Solaris 2.4 发行版基于早期的 POSIX 草案。函数定义是在编译时选择的。有关预期参数和返回值中差异的说明,请参见手册页 section 3: Library Interfaces and Headers。以下是具有不同语义的函数:
asctime_r(3C)
ctime_r(3C)
ftrylockfile(3C)(新增)
getgrgid_r(3C)
getgrnam_r(3C)
getlogin_r(3C)
getpwnam_r(3C)
getpwuid_r(3C)
readdir_r(3C)
sigwait(2)
ttyname_r(3C)
在 Solaris 9 和以前的发行版中,Solaris fork(2) 函数可以复制所有的线程 fork-all 行为。POSIX fork(2) 函数仅复制调用线程 fork-one 行为,与 Solaris fork1() 函数是一样的。
从 Solaris 10 发行版开始,fork() 的行为在未链接到 -lpthreaad 时可能会发生更改,以与 POSIX 版本保持一致。需要特别指出的是,fork() 被重新定义为fork1()。因此,fork() 将复制子进程中的调用线程。所有 Solaris 发行版中都支持 fork1() 的行为。新函数 forkall() 可以针对需要将父进程的所有线程复制到子进程中的应用程序提供此行为。
包括文件 <thread.h> 可以编译与早期的 Solaris 软件发行版向上兼容的代码。此文件包含 Solaris 线程接口的声明。要使用 POSIX 线程调用 thr_setconcurrency(3C),程序需要包括 <thread.h>。
包括文件 <pthread.h>(与 -lpthread 库结合使用)可以编译符合 POSIX 标准定义的多线程接口的代码。为了与 POSIX 完全符合,应该将功能测试宏 _POSIX_C_SOURCE 的值 (long
) 设置为 ≥ 199506。请参见 standards(5) 手册页。
对于 1996 版 POSIX 标准:
cc89 -D_POSIX_C_SOURCE=199506L [flags] file
对于 2001 版 POSIX 标准:
cc99 -D_POSIX_C_SOURCE=200112L [flags] file ... [-l rt]
可以在同一个应用程序中混合使用 Solaris 线程与 POSIX 线程。请在应用程序中同时包括 <thread.h> 和 <pthread.h>。
如果二者混合使用,则当使用 -D_REENTRANT 编译时,将采用 Solaris 语义,而当使用 -D_POSIX_C_SOURCE 编译时,将采用 POSIX 语义。
对于 POSIX 行为,请使用 -D_POSIX_C_SOURCE 标志集 ≥ 199506L 来编译应用程序。对于 Solaris 行为,请使用 -D_REENTRANT 标志来编译多线程程序。这些编译器标志适用于应用程序的每个模块。
对于混合的应用程序,具有 POSIX 语义的 Solaris 线程使用 -D_REENTRANT 和 -D_POSIX_PTHREAD_SEMANTICS 标志进行编译。
要编译单线程应用程序,请不要定义 -D_REENTRANT 标志,也不要定义 -D_POSIX_C_SOURCE 标志。不存在这些标志时,errno、stdio 等的所有原有定义仍然生效。
请在不使用 -D_REENTRANT 标志的条件下编译单线程应用程序。使用这种方式编译单线程应用程序,以避免将宏(如 putc(3s))转换为可重复执行函数调用时引起的性能降低。
总之,定义 -D_POSIX_C_SOURCE 的 POSIX 应用程序将获取例程的 POSIX 语义。仅定义 -D_REENTRANT 的应用程序将获取这些例程的 Solaris 语义。定义 -D_POSIX_PTHREAD_SEMANTICS 的 Solaris 应用程序将获取这些例程的 POSIX 语义,但仍然可以使用 Solaris 线程接口。
同时定义 -D_POSIX_C_SOURCE 和 -D_REENTRANT 的应用程序将获取 POSIX 语义。
对于 POSIX 线程行为(在 Solaris 9 和以前的发行版中),请装入 libpthread 库。对于 Solaris 线程行为,请装入 libthread 库。有的 POSIX 程序员可能想使用 -lthread 进行链接,以保留 fork() 与 fork1() 之间的 Solaris 区别。-lpthread 库使 fork() 的行为方式与 Solaris fork1() 调用的行为方式相同。
在 Solaris 10 和后续发行版中,两个线程库都不再是必需的,但是仍然可以为了实现兼容而指定库。所有的线程功能都已被移入标准 C 库中。要使用 libthread,请在 ld 命令行的 lc 前面指定 -lthread,或在 cc 命令行的末尾指定 -lthread。
要使用 libthread,请在 ld 命令行的 -lc 前面指定 -lthread,或在 cc 命令行的末尾指定 -lthread。
要使用 libpthread,请在 ld 命令行的 -lc 前面指定 -lpthread,或在 cc 命令行的末尾指定 -lpthread。
在 Solaris 9 发行版之前,不应使用 -lthread 或 -lpthread 来链接非线程程序。这样做将在链接时建立在运行时启动的多线程机制。这些机制将使单线程应用程序的速度变慢,浪费系统资源,而且会在调试代码时产生误导性结果。
在 Solaris 9 和后续发行版中,使用 -lthread 或 -lpthread 链接非线程应用程序时不会为程序产生语义差异。也不会创建额外的线程或额外的 LWP。只有主线程会像传统的单线程进程一样执行操作。对程序的唯一影响就是使系统库锁定成为实际锁定,与伪函数调用相反。您必须为获取无竞争锁定付出代价。
在 Solaris 10 发行版之前,如果应用程序没有链接 -lthread 或 -lpthread,则对 libthread 和 libpthread 的所有调用都为空操作指令。运行时库 libc 具有许多预定义 libthread 和 libpthread 存根,这些存根都是空过程。当应用程序同时链接了 libc 和线程库时,将通过 libthread 或 libpthread 插入实际过程。
对于使用线程的 C++ 程序,请使用 -mt 选项(而不是 -lthread)来编译和链接应用程序。-mt 选项与 libthread 链接,并且能确保正确的库链接顺序。-lthread 可能会导致程序进行核心转储。
对于 1996 版 POSIX 标准,请使用以下选项来编译和链接应用程序:
cc89 -D_POSIX_C_SOURCE=199506L [flags] file ... [-l rt]
对于 2001 版 POSIX 标准,请使用以下选项来编译和链接应用程序:
cc99 -D_POSIX_C_SOURCE=200112L [flags] file ... [-l rt]
在 Solaris 线程环境中,请使用以下选项来编译和链接应用程序:
cc -D_REENTRANT -D POSIX_THREAD_SEMANTICS [flags] file ... [-l rt]
在混合环境中,请使用以下选项来编译和链接应用程序:
cc -D_REENTRANT [flags] file ... [-l rt]
在混合使用时,需要包括 thread.h 和 pthread.h。
Solaris 信号例程 sema_*(3C) 包含在标准的 C 库中。相对而言,您可以链接 -lrt 库,从而获取使用信号进行同步中所述的标准 sem_*(3R) POSIX 信号例程。
表 7–1 说明,在将多线程对象模块与原有对象模块链接时应格外小心。
表 7–1 使用或不使用 _REENTRANT 标志进行编译
文件类型 |
编译 |
参考 |
返回值 |
---|---|---|---|
原有对象文件(非线程)和新对象文件 |
不使用 _REENTRANT 或 _POSIX_C_SOURCE 标志 |
传统的 errno |
|
新对象文件 |
使用 _REENTRANT 或 _POSIX_C_SOURCE 标志 | ||
使用 _REENTRANT 或 _POSIX_C_SOURCE 标志(必需) |
线程的 t_errno 定义地址。 |
Solaris 8 发行版引入了备用的线程库实现,位于目录 /usr/lib/lwp (32 位) 和 /usr/lib/lwp/64 (64 位) 中。 在 Solaris 9 发行版中,此实现成为标准的线程实现(位于 /usr/lib 和 /usr/lib/64 中)。从 Solaris 10 发行版开始生效,所有的线程功能都已被移入 libc 中,不再需要任何单独的线程库。
以下列表指出了在多线程程序中可能导致错误的一些经常被疏忽的问题。
两个线程尝试轮流获取对同一对全局资源的权限时导致死锁。 其中一个线程控制第一种资源,另一个线程控制第二种资源。其中一个线程放弃之前,任何一个线程都无法继续操作。
在同步保护中创建隐藏的间隔。 如果受保护的代码段包含的函数释放了同步机制,而又在返回调用方之前重新获取了该同步机制,则将在保护中出现此间隔。结果具有误导性。对于调用方,表面上看全局数据已受到保护,而实际上未受到保护。
忘记已创建缺省线程 PTHREAD_CREATE_JOINABLE 并且必须使用 pthread_join(3C) 来进行回收。请注意,pthread_exit(3C) 不会释放其存储空间。
多线程程序(特别是那些包含错误的程序)经常在两次连续运行中的行为方式不同,即使输入相同也是如此。此行为是由线程调度顺序的差异所导致的。
一般情况下,多线程错误是统计得出的,不具有确定性。通常,与基于断点的调试相比,跟踪是用于查找执行顺序问题的一种更有效的方法。
请使用 TNF 实用程序跟踪、调试和收集应用程序和库中的性能分析信息。TNF 实用程序将内核以及多个用户进程和线程中的跟踪信息整合在一起。TNF 实用程序对于多线程代码特别有用。TNF 实用程序包括在 Solaris 软件中,是该软件的一部分。
使用 TNF 实用程序,可以轻松跟踪和调试多线程程序。有关使用 prex(1) 和 tnfdump(1) 的详细信息,请参见 TNF 手册页。
有关跟踪系统调用、信号和用户级别函数调用的信息,请参见 truss(1) 手册页。
有关 mdb 的信息,请参见《Solaris Modular Debugger Guide》。
如果目标为用户进程,则将列显有代表性的线程的 LWP ID。
如果目标为用户进程,则将列显目标中每个 LWP 的 LWP ID。
附加到编号为 pid 的进程。
释放以前附加的进程或核心转储文件。随后可以由 prun(1) 继续处理进程,或者可通过应用 MDB 或其他调试器来恢复进程。
上下文切换到指定进程。
这些用于设置条件断点的命令通常很有用。
在指定的位置设置断点。
删除包含给定 ID 编号的事件说明符。
使用 dbx 实用程序,可以调试和执行使用 C++、ANSI C 和 FORTRAN 编写的源代码程序。dbx 与调试器接受同样的命令,但使用标准的终端 (TTY) 接口。dbx 和调试器都支持调试多线程程序。有关如何启动 dbx 的说明,请参见 dbx(1) 手册页。有关 dbx 的概述,请参见《Debugging a Program With dbx》。调试器功能在 dbx 的调试器 GUI 的联机帮助中介绍。
表 7–2 中列出的所有 dbx 选项均可支持多线程应用程序。
表 7–2 MT 程序的 dbx 选项
选项 |
操作 |
---|---|
cont at line [-sig signo id] |
在包含信号 signo 的 line 中继续执行操作。id(如果存在)指定哪个线程或 LWP 继续操作。缺省值为 all。 |
lwp |
显示当前的 LWP。切换到给定 LWP [lwpid]。 |
lwps |
列出当前进程中所有的 LWP。 |
next ... tid |
单步执行给定线程。跳过函数调用时,所有的 LWP 都会在该函数调用期间隐式恢复。不能单步执行非活动线程。 |
next ... lid |
单步执行给定 LWP。跳过函数时不会隐式恢复所有 LWP。所含的给定线程处于活动状态的 LWP。跳过函数时不会隐式恢复所有 LWP。 |
step... tid |
单步执行给定线程。跳过函数调用时,所有的 LWP 都会在该函数调用期间隐式恢复。不能单步执行非活动线程。 |
step... lid |
单步执行给定 LWP。跳过函数时不会隐式恢复所有 LWP。 |
stepi... lid |
给定的 LWP。 |
stepi... tid |
所含的给定线程处于活动状态的 LWP。 |
thread |
显示当前线程。切换到线程 tid。在下面所有的变体中,可选的 tid 表示当前线程。 |
thread -info [ tid ] |
列显有关给定线程的所有已知信息。 |
thread -blocks [ tid ] |
列显阻塞其他线程的给定线程持有的所有锁定。 |
thread -suspend [ tid ] |
使给定线程进入暂停状态。 |
thread -resume [ tid ] |
取消暂停给定线程。 |
thread -hide [ tid ] |
隐藏给定线程或当前线程。该线程不会出现在通用的 threads 列表中。 |
thread -unhide [ tid ] |
取消隐藏给定线程或当前线程。 |
thread -unhide all |
取消隐藏所有线程。 |
threads |
列显所有已知线程的列表。 |
threads -all |
列显通常不会列显的线程(僵线程)。 |
threads -mode all|filter |
控制在缺省情况下,threads 是列显所有线程,还是过滤线程。 |
threads -mode auto|manual |
实现线程列表的自动更新。 |
threads -mode |