在传统UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的。在POSIX线程(pthread)的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。新增的线程可以通过调用pthread_create
函数创建。
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *), void *restrict arg);
//返回值:若成功,返回0;否则,返回错误编号
当pthread_create
成功返回时,新创建线程的线程ID会被设置成tidp
指向的内存单元。attr
参数用于定制各种不同的线程属性。我们将在12.3节中讨论线程属性,但现在我们把它置为NULL
,创建一个具有默认属性的线程。
新创建的线程从start_rtn
函数的地址开始运行,该函数只有一个无类型指针参数arg
。如果需要向start_rtn
函数传递的参数有一个以上,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg
参数传入。
线程创建时并不能保证哪个线程会先运行:是新创建的线程,还是调用线程。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。
注意,pthread
函数在调用失败时通常会返回错误码,它们并不像其他的 POSIX 函数一样设置errno
。每个线程都提供errno
的副本,这只是为了与使用errno
的现有函数兼容。在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局状态,这样可以把错误的范围限制在引起出错的函数中。
实例
虽然没有可移植的打印线程 ID 的方法,但是可以写一个小的测试程序来完成这个任务,以便更深入地了解线程是如何工作的。图 11-2 中的程序创建了一个线程,打印了进程 ID、新线程的线程ID以及初始线程的线程ID。
#include "apue.h"
#include <pthread.h>
pthread_t ntid;
void
printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid,
(unsigned long)tid, (unsigned long)tid);
}
void *
thr_fn(void *arg)
{
printids("new thread: ");
return((void *)0);
}
int
main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0)
err_exit(err, "can't create thread");
printids("main thread:");
sleep(1);
exit(0);
}
图11-2 打印线程ID
这个实例有两个特别之处,需要处理主线程和新线程之间的竞争。(我们将在这章后面的内容中学习如何更好地处理这种竞争。)第一个特别之处在于,主线程需要休眠,如果主线程不休眠,它就可能会退出,这样新线程还没有机会运行,整个进程可能就已经终止了。这种行为特征依赖于操作系统中的线程实现和调度算法。
第二个特别之处在于新线程是通过调用pthread_self
函数获取自己的线程ID的,而不是从共享内存中读出的,或者从线程的启动例程中以参数的形式接收到的。回忆 pthread_create
函数,它会通过第一个参数(tidp
)返回新建线程的线程ID。在这个例子中,主线程把新线程ID存放在 ntid
中,但是新建的线程并不能安全地使用它,如果新线程在主线程调用pthread_create
返回之前就运行了,那么新线程看到的是未经初始化的ntid
的内容,这个内容并不是正确的线程ID。
在Solaris上运行图11-2中的程序,得到:
$ ./a.out
main thread: pid 20075 tid 1 (0x1)
new thread: pid 20075 tid 2 (0x2)
正如我们期望的,两个线程的进程ID相同,但线程ID不同。在FreeBSD上运行图11-2中的程序,得到:
$ ./a.out
main thread: pid 37396 tid 673190208 (0x28201140)
new thread: pid 37396 tid 673280320 (0x28217140)
也如我们期望的,两个线程有相同的进程ID。如果把线程ID看成是十进制整数,那么这两个值看起来很奇怪,但是如果把它们转化成十六进制,看起来就更合理了。就像前面提到的,FreeBSD使用指向线程数据结构的指针作为它的线程ID。
我们期望Mac OS X与FreeBSD相似,但事实上,在MacOS X中,主线程ID与用pthread_create
新创建的线程的线程ID不在相同的地址范围内:
$ ./a.out
main thread: pid 31807 tid 140735073889440(0x7fff70162ca0)
new thread: pid 31807 tid 4295716864(0x1000b7000)
相同的程序在Linux上运行得到:
$ ./a.out
main thread: pid 17874 tid 140693894424320(0x7ff5d9996700)
new thread: pid 17874 tid 140693886129920(0x7ff5d91ad700)
尽管Linux线程ID是用无符号长整型来表示的,但是它们看起来像指针。
Linux 2.4和Linux 2.6在线程实现上是不同的。Linux 2.4中,LinuxThreads是用单独的进程实现每个线程的,这使得它很难与POSIX线程的行为匹配。Linux 2.6中,对Linux内核和线程库进行了很大的修改,采用了一个称为Native POSIX线程库(Native POSIX Thread Library,NPTL)的新线程实现。它支持单个进程中有多个线程的模型,也更容易支持POSIX线程的语义。