APUE 4 - 线程

对传统的UNIX进程来讲,一个进程中只有一个线程,这就意味着一个进程在同一时刻只能做一件事(即使是多核CPU)。使用多线程技术, 我们可以设计程序使得一个进程在同一时刻做多件事。使用多线程编程具有以下优势:

  1. 我们可以以独立线程分别处理其对应事件类型事件的方式来处理异步事件,以此来简化我们的编码。每个线程中就可以以同步编程(顺序编程)的方式来编程了。同步(顺序)编程要比异步编程简单的多。
  2. 多进程程序必须使用操作系统提供的复杂机制来实现共享内存、共享文件描述符等,而相比而言,同一进程中的多线程在这个进程中具有相同的内存地址空间和文件描述符。
  3. 一些问题可以被分割开来以提高程序整体的性能。单线程程序在处理多任务时只能将任务序列化处理,因为它同一时刻只有一个线程在工作。使用多线程控件时,可以通过一个独立线程一个任务的方式来交叉处理多个独立任务。
  4. 同样的,对于交互式程序来说, 使用多线程技术可以大大提高程序的反应速度。

一个线程由一个进程中那些能代表当前执行上下文的所必要的信息组成。它包括一个用于标志线程的线程ID、一个寄存器值的集合、一个堆栈、一个优先级表、一个信号掩码、一个errno变量和一个指定线程数据(thread-specific data)。进程中的所有资源都被这个进程中的所有线程所共享,它包括程序执行上下文、程序的全局和堆内存、堆栈、文件描述符 。我们接下来要引述的线程接口来自 POSIX.1-2001。

线程ID

正如每个进程都有一个进程ID一样,每个线程都有一个线程ID,只不过一个进程的进程ID在系统中全局唯一,而线程ID只在线程所属的进程中有意义。线程ID使用数据类型pthread_t来表示。实现被允许使用结构体来代表pthread_t类型,因此好的实现不应将pthread_t当成整数来对待。因此,我们必须使用函数来比较两个线程ID:

 #include <pthread.h>

 /* Return:nozero if equal, 0 otherwise */
int pthread_equal(pthread_t tid1, pthread_t tid2);

Linux3.2.0使用用无符号长整型实现pthread_t。Solaris 10 使用无符号整形代表pthread_t。FreeBSD 8.0和Mac OS X 10.6.8 使用执行pthread结构体的指针来代表pthread_t。

获取线程ID

 #include <pthread.h>

 /* Return:the thread ID of the calling thread */
pthread_t pthread_self(void);

创建线程

使用pthread,在程序启动的时候一个进程也是仅有一个线程的,在程序运行的时候他与传统的进程没有什么区别, 直到他在进程中创建了更多的多线程。

 #include <pthread.h>

 /* Return: 0 if ok, error number on failure */
int pthread_create(
pthread_t* restrict tidp,
const pthread_attr_t* restrict attr,
void* (* start_rtn)(void*),
void* restrict arg);

tidp 用于获取线程成功创建后的线程Id;attr用户自定各种线程属性;start_rtn指定线程要执行的函数地址;arg为start_rtn指向函数的参数;

新创建的线程可以访问进程地址空间并继承调用线程的浮点环境(floating-point environment)和信号掩码,然而新线程的阻塞信号集是被清空的。注意pthread类函数在失败时通常返回一个错误码而不像其他POSIX函数那样设置errno。每个线程拥有一个errno副本仅仅是为了与现有使用errno的函数兼容。

终止线程

如果一个进程中任何一个线程调用了 exit、_exit或_Exit,那么整个进程会被中止。同样的,向一个线程一个默认处理方式是终止进程的信号会中止这个线程所在的进程。

单个线程可以有以下三种退出方式:

  1. 简单的从启动例程中返回。返回值就是线程的退出码。
  2. 线程可以被所属进程中的另一个线程取消掉。
  3. 线程可以通过调用pthread_exit退出
 #include <pthread.h>

 /*
终止线程并通过rval_ptr返回一个值,
rval_ptr可以被同一进程中调用pthread_join
方法的线程获取到
*/
void pthread_exit(void* rval_ptr); /*
等待thread线程结束,thread必须是joinable的。
如果rval_ptr不为空,它会复制目标线程的退出码(
如目标线程在pthread_exit中提供的值)到rval_ptr
指向的位置。如果目标线程被取消PTHREAD_CANCELED
会被放置到rval_ptr指向的位置
*/
void pthread_join(pthread_t thread, void** rval_ptr);

传递给pthread_exit 和 pthread_create的无类型指针可用于传输复杂类型数据。通过pthread_jion方法我们可以将我们等待的线程置于检测状态(detached state),而此时调用线程就可以发现(discover)被等待线程的资源。应当注意的是,当pthread_exit调用结束时,他的rval_ptr的值仍是有效的。这就意味着如果rval_ptr指向的内存在调用线程的堆栈(Stack)上分配,那么rval_ptr在被使用的时候它指向的内存的内容可能已经改变。举例来说,如果一个线程在它的堆栈上给一个struct结构分配了一块内存,并将struct结构作为参数传递给了pthread_exit函数,那么当pthread_join的调用线程 在使用rval_ptr时,这个结构可能已经被销毁而他指向的内存可能已经用于他处。为了避免这种情况,我们应当使用全局变量或者在堆(Heap)上给结构分配内存。

一个线程可以通过pthread_cancel方法请求取消同一进程中另一线程的执行:

 #include <pthread.h>

 /* Return: 0 if OK,error number on failuer */
int pthread_cancel(pthread_t tid);

默认情况下,调用pthread_cancel 函数会使tid线程的行为就像它自己使用PTHREAD_CANCELED参数调用了pthread_exit一样。线程可以选择忽略或其他的处理方式来处理cancel请求。pthread_cancel不会等待线程结束,它几乎只是发送cancel请求。

线程可以安排在它退出时需要执行的函数,这些函数一般是一些线程清理句柄(thread cleanup handlers) 。一个线程可以建立多个清理句柄,这些句柄存储在堆栈(stack)中,即他们会按注册时的顺序逆序执行。

 #include <pthread.h>

 /* 注册清理函数 */
void pthread_cleanup_push(void (*rtn) (void* ), void* arg); /* 移除栈顶的清理函数,如果excute不是0,将执行清理函数 */
void pthread_cleanup_pop(int excute);

pthread_cleanup_push注册的函数在以下三种情况下会被调用:

  1. 线程调用了 pthread_exit 函数
  2. 线程响应cancel请求
  3. 线程使用非0参数调用了pthread_cleanup_pop函数。

无论pthread_cleanup_pop函数再被调用时是否使用了非0参数,他都会将栈顶的pthread_cleanup_push注册的清理函数移除掉。注意, return并不会执行注册的清理函数,我们不应该在pthread_cleanup_push和pthread_cleanup_pop之间使用return,唯一可行的办法是在他们之间调用pthread_exit。

默认情况下,一个线程的退出状态会一直保留,除非我们对这个线程调用pthread_join函数(调用后线程处于detached 状态)。如果一个线程被distach了,那么当它退出时它的底层存储会被立即回收;可以使用thread_detach函数来detach一个线程:

 #include <pthread.h>

 /* Return: 0 if OK, error number if failure */
int pthread_depatch(pthread_t tid);

线程与进程函数对照表:

进程主要函数 线程主要函数 描述
fork pthread_create 创建新的实例
exit pthread_exit 退出
waitpid pthread_join 等待实例结束并获取结束码
atexit   pthread_cleanup_push 注册推出前要执行的函数
getpid pthread_self 获取实例ID
abort pthread_cancel 请求终止实例
上一篇:Spring(二)__bean的装配


下一篇:monkeyrunner之eclipse中运行monkeyrunner脚本之环境搭建(四)