如同进程有一个进程 ID,每个线程也有一个线程 ID。不过进程 ID 在整个系统中是唯一的,而线程 ID 只有在它所属的进程上下文中才有意义。线程 ID 使用数据类型 pthread_t 来表示,因为这种类型在不同的实现中可能不同(有的为整型,有的为结构),所以为了移植性提供了一个比较函数 pthread_equal。另外,还提供了 pthread_self 函数来获取自身的线程 ID。
而要创建一个线程,可以使用 pthread_create 函数。
当该函数成功返回时,新创建的线程 ID 会被保存在 tidp 指向的内存中。attr 参数用于定制不同的线程属性,当为 NULL 时表示使用默认的属性。新创建的线程从 start_rtm 函数的地址开始运行,所需的参数放到无类型指针参数 arg 所指的地址中。如果有多个参数,可将其放到一个结构中,然后把该结构的地址作为 arg 参数传入(不过要保证该结构所使用的内存在调用者完成调用以后仍然有效,否则该结构的值可能会被其他使用了同一块栈内存的函数隐式地改变)。
线程创建时并不能保证是新创建的线程还是调用线程先运行。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。
注意,pthread 类函数在调用失败时通常会返回错误码,而不是直接修改全局变量 errno,以便把错误的范围限制在引起出错的函数中。
如果进程中的任意线程调用了 exit、_exit 或者 _Exit,再或者收到默认动作是终止进程的信号都会终止整个进程。要使单个线程在不终止整个进程的情况下停止它的控制流,可以通过以下 3 种方式。
(1)从启动例程中返回,返回值是线程的退出码。
(2)被同一进程中的其他线程取消。
(3)调用 pthread_exit 函数。
pthread_exit 中的无类型指针参数 rval_ptr 用法同传给启动例程的单个参数类似。进程中的其他线程可以通过调用 pthread_join 函数访问到这个指针。pthread_join 会使调用线程一直阻塞,直到指定的线程调用 pthread_exit、从启动例程返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr 就包含返回码;如果线程被取消,由 rval_ptr 指定的内存单元就设置为 PTHREAD_CANCELED。如果对线程的返回值不感兴趣,可将 rval_ptr 设置为 NULL,这样调用 pthread_join 就只等待指定的线程终止而不获取线程的终止状态。
pthread_detach 函数可以分离线程。默认情况下,线程的终止状态会保存直到对该线程调用 pthread_join。如果线程已经被分离,线程的底层存储资源就可以在线程终止时立即被收回。线程分离后再调用 pthread_join 将会产生未定义行为。
虽然不能可移植的打印线程 ID,但可以写一个小的测试程序来完成这个任务。下面这个程序打印了进程 ID、新线程 ID 以及初始线程 ID。
在 Linux 上运行该程序,得到:
可见线程是运行在同一进程中,而这里的线程 ID 数据类型很像是一个指针(然而事实是 Linux 上的 pthread_t 类型是用无符号长整型来表示的)。
线程可以调用 pthread_cannel 函数来请求取消同一进程中的其他线程(如同进程中调用了 abort),也可以通过 pthread_cleanup_push 函数安排在线程退出时需要调用的线程清理处理程序(如同进程中的 atexit 函数),它们的执行顺序与注册顺序相反。
默认情况下,pthread_cancel 函数会使得线程 tid 表现得如同调用了参数为 PTHREAD_CANCELED 的 pthread_exit 函数,但线程可以选择忽略取消或者控制如何被取消。注意该函数并不等待线程终止,它仅仅提出请求。
当线程执行以下动作时,pthread_cleanup_push 函数就会调用清理函数 rtn,调用时只有一个参数 arg:
* 调用 pthread_exit 时;
* 相应取消请求时;
* 用非零 execute 参数调用 pthread_cleanup_pop 时。如果 execute 为 0,清理函数将不被调用。
不管发生上述哪种情况,pthread_cleanup_pop 都将删除上次 pthread_cleanup_push 调用建立的清理处理程序。注意,pthread_cleanup_pop 必须和 pthread_cleanup_push 在与线程相同的作用域中以匹配对的形式使用,因为它们可能实现为宏。
下面这个程序给出了一个如何使用线程清理处理程序的例子。这里为了匹配 pthread_cleanup_push,我们调用了参数为 0 的 pthread_cleanup_pop 函数。
Linux 上的运行结果:
从输出可以看出只有第二个线程的清理处理程序被调用了,因为第一个线程是使用 return 返回的,而第二个是调用 pthread_exit 返回的。另外,清理处理程序是按照与安装时的相反顺序被调用的。
如果在 FreeBSD 或者 Mac OS 上运行该程序将会出现段异常并产生 core 文件。因为这两个平台上的 pthread_cleanup_push 是用宏实现的,而宏把某些上下文存放在栈上。当线程 1 在调用 pthread_cleanup_push 和调用 pthread_cleanup_pop 之间返回时,栈已被改写,而这两个平台在调用清理程序时就用了这个被改写的上下文。唯一的可移植的方法是调用 pthread_exit。
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
/* 返回值:若相等,返回非 0;否则,返回 0 */
pthread_t pthread_self(void); /* 返回值:调用线程的线程 ID */
而要创建一个线程,可以使用 pthread_create 函数。
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,
void* (*start_rtm)(void *), void *restrict arg);
/* 返回值:若成功,返回 0;否则,返回错误编号 */
当该函数成功返回时,新创建的线程 ID 会被保存在 tidp 指向的内存中。attr 参数用于定制不同的线程属性,当为 NULL 时表示使用默认的属性。新创建的线程从 start_rtm 函数的地址开始运行,所需的参数放到无类型指针参数 arg 所指的地址中。如果有多个参数,可将其放到一个结构中,然后把该结构的地址作为 arg 参数传入(不过要保证该结构所使用的内存在调用者完成调用以后仍然有效,否则该结构的值可能会被其他使用了同一块栈内存的函数隐式地改变)。
线程创建时并不能保证是新创建的线程还是调用线程先运行。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。
注意,pthread 类函数在调用失败时通常会返回错误码,而不是直接修改全局变量 errno,以便把错误的范围限制在引起出错的函数中。
如果进程中的任意线程调用了 exit、_exit 或者 _Exit,再或者收到默认动作是终止进程的信号都会终止整个进程。要使单个线程在不终止整个进程的情况下停止它的控制流,可以通过以下 3 种方式。
(1)从启动例程中返回,返回值是线程的退出码。
(2)被同一进程中的其他线程取消。
(3)调用 pthread_exit 函数。
#include <pthread.h>
void pthread_exit(void *rval_ptr);
int pthread_join(pthread_t tid, void **rval_ptr);
int pthread_detach(pthread_t tid);
/* 两个函数返回值:若成功,返回 0;否则,返回错误编码 */
pthread_exit 中的无类型指针参数 rval_ptr 用法同传给启动例程的单个参数类似。进程中的其他线程可以通过调用 pthread_join 函数访问到这个指针。pthread_join 会使调用线程一直阻塞,直到指定的线程调用 pthread_exit、从启动例程返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr 就包含返回码;如果线程被取消,由 rval_ptr 指定的内存单元就设置为 PTHREAD_CANCELED。如果对线程的返回值不感兴趣,可将 rval_ptr 设置为 NULL,这样调用 pthread_join 就只等待指定的线程终止而不获取线程的终止状态。
pthread_detach 函数可以分离线程。默认情况下,线程的终止状态会保存直到对该线程调用 pthread_join。如果线程已经被分离,线程的底层存储资源就可以在线程终止时立即被收回。线程分离后再调用 pthread_join 将会产生未定义行为。
虽然不能可移植的打印线程 ID,但可以写一个小的测试程序来完成这个任务。下面这个程序打印了进程 ID、新线程 ID 以及初始线程 ID。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void printtid(const char *s){
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid %lu, tid %lu (0x%lx)\n", s, (unsigned long)pid,
(unsigned long)tid, (unsigned long)tid );
}
void *tfunc(void *arg){
printtid("new thread:");
pthread_exit((void *)1); // or return (void *)1;
}
int main(void){
pthread_t ntid;
if(pthread_create(&ntid, NULL, tfunc, NULL) != 0){
printf("can't create thread\n");
exit(1);
}
printtid("main thread:");
void *tret;
if(pthread_join(ntid, &tret) != 0){
printf("pthread_join error\n");
exit(1);
}
printf("thread exit code: %ld\n", (long)tret);
exit(0);
}
在 Linux 上运行该程序,得到:
$ ./print_tid.out
main thread: pid 29340, tid 140178125190912 (0x7f7dc35b7700)
new thread: pid 29340, tid 140178125182720 (0x7f7dc35b5700)
thread exit code: 1
可见线程是运行在同一进程中,而这里的线程 ID 数据类型很像是一个指针(然而事实是 Linux 上的 pthread_t 类型是用无符号长整型来表示的)。
线程可以调用 pthread_cannel 函数来请求取消同一进程中的其他线程(如同进程中调用了 abort),也可以通过 pthread_cleanup_push 函数安排在线程退出时需要调用的线程清理处理程序(如同进程中的 atexit 函数),它们的执行顺序与注册顺序相反。
#include <pthread.h>
int pthread_cancel(pthread_t tid);
/* 返回值:若成功,返回 0;否则,返回错误编号*/
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
默认情况下,pthread_cancel 函数会使得线程 tid 表现得如同调用了参数为 PTHREAD_CANCELED 的 pthread_exit 函数,但线程可以选择忽略取消或者控制如何被取消。注意该函数并不等待线程终止,它仅仅提出请求。
当线程执行以下动作时,pthread_cleanup_push 函数就会调用清理函数 rtn,调用时只有一个参数 arg:
* 调用 pthread_exit 时;
* 相应取消请求时;
* 用非零 execute 参数调用 pthread_cleanup_pop 时。如果 execute 为 0,清理函数将不被调用。
不管发生上述哪种情况,pthread_cleanup_pop 都将删除上次 pthread_cleanup_push 调用建立的清理处理程序。注意,pthread_cleanup_pop 必须和 pthread_cleanup_push 在与线程相同的作用域中以匹配对的形式使用,因为它们可能实现为宏。
下面这个程序给出了一个如何使用线程清理处理程序的例子。这里为了匹配 pthread_cleanup_push,我们调用了参数为 0 的 pthread_cleanup_pop 函数。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void cleanup(void *arg){
printf("cleanup: %s\n", (char *)arg);
}
void *thr_fn1(void *arg){
printf("thread 1 starts push\n");
pthread_cleanup_push(cleanup, "first clean handler in thread 1");
pthread_cleanup_push(cleanup, "second clean handler in thread 1");
printf("thread 1 push complete\n");
if(arg)
return (void *)1;
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return (void *)1;
}
void *thr_fn2(void *arg){
printf("thread 2 starts push\n");
pthread_cleanup_push(cleanup, "first clean handler in thread 2");
pthread_cleanup_push(cleanup, "second clean handler in thread 2");
printf("thread 2 push complete\n");
if(arg)
pthread_exit((void *)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void *)2);
}
int main(void){
pthread_t tid1, tid2;
void *rtn;
if(pthread_create(&tid1, NULL, thr_fn1, (void *)1) != 0){
printf("pthread_create 1 error\n");
exit(1);
}
if(pthread_create(&tid2, NULL, thr_fn2, (void *)1) != 0){
printf("pthread_create 2 error\n");
exit(1);
}
if(pthread_join(tid1, &rtn) != 0){
printf("pthread_join 1 error\n");
exit(1);
}
printf("thread 1 return code: %ld\n", (long)rtn);
if(pthread_join(tid2, &rtn) != 0){
printf("pthread_join 2 error\n");
exit(2);
}
printf("thread 2 exit code: %ld\n", (long)rtn);
exit(0);
}
Linux 上的运行结果:
$ ./threadClean.out
thread 2 starts push
thread 2 push complete
cleanup: second clean handler in thread 2
cleanup: first clean handler in thread 2
thread 1 starts push
thread 1 push complete
thread 1 return code: 1
thread 2 exit code: 2
从输出可以看出只有第二个线程的清理处理程序被调用了,因为第一个线程是使用 return 返回的,而第二个是调用 pthread_exit 返回的。另外,清理处理程序是按照与安装时的相反顺序被调用的。
如果在 FreeBSD 或者 Mac OS 上运行该程序将会出现段异常并产生 core 文件。因为这两个平台上的 pthread_cleanup_push 是用宏实现的,而宏把某些上下文存放在栈上。当线程 1 在调用 pthread_cleanup_push 和调用 pthread_cleanup_pop 之间返回时,栈已被改写,而这两个平台在调用清理程序时就用了这个被改写的上下文。唯一的可移植的方法是调用 pthread_exit。
2898

被折叠的 条评论
为什么被折叠?



