开源中文网

您的位置: 首页 > 编程开发 > C++语言编程 > 正文

多线程编程学习7-编译和调试

来源:  作者:

本章介绍如何编译和调试多线程程序。本章论述以下主题:
■    第 171 页中的 “编译多线程应用程序”
■    第 175 页中的 “调试多线程程序”
 
编译多线程应用程序
许多选项可用于头文件、定义标志和链接。
 
为编译做准备
编译和链接多线程程序时,需要以下项。Solaris 软件应包括除 C 编译器以外的所有项。
■    标准 C 编译器
■    包括以下文件:
■    <thread.h> 和 <pthread.h>
■    <errno.h>, <limits.h>, <signal.h> , <unistd.h>
■    常规 Solaris 链接程序 ln(1)
■    Solaris 线程库 (libthread)、POSIX 线程库 (libpthread),可能还有信号的 POSIX 实时库(librt)(仅限于 Solaris 9和以前的发行版)
■    MT 安全库,如 libc、libm、libw、libintl、libnsl、libsocket、libmalloc、libmapmalloc 等
 
选择 Solaris 语义或 POSIX 语义
某些函数在 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> 或 <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 [?ags]   ?le
 
对于 2001 版 POSIX 标准:
 
cc99 -D_POSIX_C_SOURCE=200112L [?ags]    ?le   ... [-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 标志。不存在这些标志时,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语义。
 
使用 libthread 或 libpthread 链接
对于 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 可能会导致程序
进行核心转储。
 
 
在 POSIX 环境中链接
对于 1996 版 POSIX 标准,请使用以下选项来编译和链接应用程序:
 
cc89 -D_POSIX_C_SOURCE=199506L [?ags]   ?le   ... [-l rt]
对于 2001 版 POSIX 标准,请使用以下选项来编译和链接应用程序:
 
cc99 -D_POSIX_C_SOURCE=200112L [?ags]    ?le   ... [-l rt]
 
 
在 Solaris 环境中链接
在 Solaris 线程环境中,请使用以下选项来编译和链接应用程序:
 
cc -D_REENTRANT -D POSIX_THREAD_SEMANTICS [?ags]      ?le   ... [-l rt]
 
在混合环境中链接
在混合环境中,请使用以下选项来编译和链接应用程序:
 
cc -D_REENTRANT [?ags]  ?le   ... [-l rt]
在混合使用时,需要包括 thread.h 和 pthread.h。
 
 
与 POSIX 信号的 -lrt 链接
Solaris 信号例程 sema_*(3C) 包含在标准的 C 库中。相对而言,您可以链接 -lrt 库,从而获取第 121 页中的 “使用信号进行同步”中所述的标准 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 中,不再需要任何单独的线程库。
 
 
调试多线程程序
下面论述的内容介绍了一些可能在多线程程序中导致错误的特征。
 
 
多线程程序中常见的疏忽性问题
以下列表指出了在多线程程序中可能导致错误的一些经常被疏忽的问题。
■    将指针作为新线程的参数传递给调用方栈。
■    在没有同步机制保护的情况下访问全局内存的共享可更改状态。
■    两个线程尝试轮流获取对同一对全局资源的权限时导致死锁。 其中一个线程控制第一种资源,另一个线程控制第二种资源。其中一个线程放弃之前,任何一个线程都无法继续
操作。
■    尝试重新获取已持有的锁(递归死锁)。
■    在同步保护中创建隐藏的间隔。 如果受保护的代码段包含的函数释放了同步机制,而又在返回调用方之前重新获取了该同步机制,则将在保护中出现此间隔。结果具有误导
性。对于调用方,表面上看全局数据已受到保护,而实际上未受到保护。
■    将 UNIX 信号与线程混合时,使用 sigwait(2) 模型来处理异步信号。
■    调用 setjmp(3C) 和 longjmp(3C),然后长时间跳跃,而不释放互斥锁。
■    从对 *_cond_wait() 或 *_cond_timedwait() 的调用中返回后无法重新评估条件。
■    忘记已创建缺省线程 PTHREAD_CREATE_JOINABLE 并且必须使用 pthread_join(3C) 来进行回收。请注意,pthread_exit(3C) 不会释放其存储空间。
■    执行深入嵌套、递归调用以及使用大型自动数组可能会导致问题,因为多线程程序与单
线程程序相比对栈大小的限制更多。
■    指定不适当的栈大小,或使用非缺省栈。
多线程程序(特别是那些包含错误的程序)经常在两次连续运行中的行为方式不同,即使
输入相同也是如此。此行为是由线程调度顺序的差异所导致的。
一般情况下,多线程错误是统计得出的,不具有确定性。通常,与基于断点的调试相比,
跟踪是用于查找执行顺序问题的一种更有效的方法。
 
使用 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] 在包含信号 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 回显当前模式。线程或 LWP ID 可以按照以前的任何形式来追溯指定实体。
 

Tags:线程
关于开源中文网 - 联系我们 - 广告服务 - 网站地图 - 版权声明