多线程编程指南

第 7 章 编译和调试

本章介绍如何编译和调试多线程程序。本章论述以下主题:

编译多线程应用程序

许多选项可用于头文件、定义标志和链接。

为编译做准备

编译和链接多线程程序时,需要以下项。Solaris 软件应包括除 C 编译器以外的所有项。

选择 Solaris 语义或 POSIX 语义

某些函数在 POSIX 标准中的语义与在 Solaris 2.4 发行版中的语义是不同的,Solaris 2.4 发行版基于早期的 POSIX 草案。函数定义是在编译时选择的。有关预期参数和返回值中差异的说明,请参见手册页 section 3: Library Interfaces and Headers。以下是具有不同语义的函数:

在 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><pthread.h>

包括文件 <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 语义。

定义 _REENTRANT_POSIX_C_SOURCE

对于 POSIX 行为,请使用 -D_POSIX_C_SOURCE 标志集 ≥ 199506L 来编译应用程序。对于 Solaris 行为,请使用 -D_REENTRANT 标志来编译多线程程序。这些编译器标志适用于应用程序的每个模块。

对于混合的应用程序,具有 POSIX 语义的 Solaris 线程使用 -D_REENTRANT-D_POSIX_PTHREAD_SEMANTICS 标志进行编译。

要编译单线程应用程序,请不要定义 -D_REENTRANT 标志,也不要定义 -D_POSIX_C_SOURCE 标志。不存在这些标志时,errnostdio 等的所有原有定义仍然生效。


注 –

请在不使用 -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 语义。

使用 libthreadlibpthread 链接

对于 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,则对 libthreadlibpthread 的所有调用都为空操作指令。运行时库 libc 具有许多预定义 libthreadlibpthread 存根,这些存根都是空过程。当应用程序同时链接了 libc 和线程库时,将通过 libthreadlibpthread 插入实际过程。


注 –

对于使用线程的 C++ 程序,请使用 -mt 选项(而不是 -lthread)来编译和链接应用程序。-mt 选项与 libthread 链接,并且能确保正确的库链接顺序。-lthread 可能会导致程序进行核心转储。


在 POSIX 环境中链接

对于 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 环境中链接

在 Solaris 线程环境中,请使用以下选项来编译和链接应用程序:

cc -D_REENTRANT -D POSIX_THREAD_SEMANTICS [flags] file ... [-l rt]

在混合环境中链接

在混合环境中,请使用以下选项来编译和链接应用程序:

cc -D_REENTRANT [flags] file ... [-l rt]

在混合使用时,需要包括 thread.hpthread.h

与 POSIX 信号的 -lrt 链接

Solaris 信号例程 sema_*(3C) 包含在标准的 C 库中。相对而言,您可以链接 -lrt 库,从而获取使用信号进行同步中所述的标准 sem_*(3R) POSIX 信号例程。

将原有模块与新模块链接

表 7–1 说明,在将多线程对象模块与原有对象模块链接时应格外小心。

表 7–1 使用或不使用 _REENTRANT 标志进行编译

文件类型 

编译 

参考 

返回值 

原有对象文件(非线程)和新对象文件 

不使用 _REENTRANT_POSIX_C_SOURCE 标志

静态存储

传统的 errno

新对象文件 

使用 _REENTRANT_POSIX_C_SOURCE 标志

__errno,新的二进制入口点

线程的 errno 定义地址

libnsl 中使用 TLI 的程序要获取 TLI 全局错误变量,请包括 tiuser.h

使用 _REENTRANT_POSIX_C_SOURCE 标志(必需)

__t_errno,新的入口点

线程的 t_errno 定义地址。

备用线程库

Solaris 8 发行版引入了备用的线程库实现,位于目录 /usr/lib/lwp (32 位) 和 /usr/lib/lwp/64 (64 位) 中。 在 Solaris 9 发行版中,此实现成为标准的线程实现(位于 /usr/lib/usr/lib/64 中)。从 Solaris 10 发行版开始生效,所有的线程功能都已被移入 libc 中,不再需要任何单独的线程库。

调试多线程程序

下面论述的内容介绍了一些可能在多线程程序中导致错误的特征。

多线程程序中常见的疏忽性问题

以下列表指出了在多线程程序中可能导致错误的一些经常被疏忽的问题。

多线程程序(特别是那些包含错误的程序)经常在两次连续运行中的行为方式不同,即使输入相同也是如此。此行为是由线程调度顺序的差异所导致的。

一般情况下,多线程错误是统计得出的,不具有确定性。通常,与基于断点的调试相比,跟踪是用于查找执行顺序问题的一种更有效的方法。

使用 TNF 实用程序跟踪和调试

请使用 TNF 实用程序跟踪、调试和收集应用程序和库中的性能分析信息。TNF 实用程序将内核以及多个用户进程和线程中的跟踪信息整合在一起。TNF 实用程序对于多线程代码特别有用。TNF 实用程序包括在 Solaris 软件中,是该软件的一部分。

使用 TNF 实用程序,可以轻松跟踪和调试多线程程序。有关使用 prex(1)tnfdump(1) 的详细信息,请参见 TNF 手册页。

使用 truss

有关跟踪系统调用、信号和用户级别函数调用的信息,请参见 truss(1) 手册页。

使用 mdb

有关 mdb 的信息,请参见《Solaris Modular Debugger Guide》。

可以使用下面的 mdb 命令来访问多线程程序的 LWP。

$l

如果目标为用户进程,则将列显有代表性的线程的 LWP ID。

$L

如果目标为用户进程,则将列显目标中每个 LWP 的 LWP ID。

pid::attach

附加到编号为 pid 的进程。

::release

释放以前附加的进程或核心转储文件。随后可以由 prun(1) 继续处理进程,或者可通过应用 MDB 或其他调试器来恢复进程。

address ::context

上下文切换到指定进程。

这些用于设置条件断点的命令通常很有用。

[ addr ] ::bp [+/-dDestT] [-c cmd] [-n count] sym ...

在指定的位置设置断点。

addr ::delete [id | all]

删除包含给定 ID 编号的事件说明符。

使用 dbx

使用 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]

在包含信号 signoline 中继续执行操作。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

回显当前模式。线程或 LWP ID 可以按照以前的任何形式来追溯指定实体。