与process类似,thread是允许应用程序并发执行多个任务的一种机制。
同一程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括:
(传统意义上的UNIX进程只是多线程程序的一个特例,该进程只包含一个线程)
同一进程的多个线程可以并发执行。在多个处理器环境下,多个线程还可以同时并行。
比如,一个线程因为等待I/O而遭阻塞,其他线程仍然可以继续运行。
相比于之下,多进程程序存在如下限制:
fork()
创建进程的代价相对较高。
fork()
在时间上的开销依然不菲。线程解决了上面两个问题。
clone()
来实现线程的fork()
需要复制进程的诸多属性,而线程间是共享的,无需复制内存页、页表。除了全局内存外,线程还共享以下属性:
fcntl()
创建的记录锁(record lock)。setitimer()
)和POSIX定时器(timer_create()
)。times()
返回)。getrusage()
返回)。setpriority()
和nice()
返回)。各线程独有的属性,列出其中一部分:
signalstack()
)。errno
变量。所有线程栈均驻留于同一虚拟地址空间。这意味着,利用一个合适的指针,各线程可以在对方栈中相互共享数据,这种方法偶尔能派上用场,但由于局部变量的状态有效与否取决于其所驻留栈帧的声明周期,故而需要谨慎处理这一问题。(当函数返回时,该函数栈帧所占用的内存区可能为后续的函数调用所重新使用。如果线程中止,那么新线程有可能会对已经中止线程的栈所占用的内存空间重新加以利用)。若无法正确处理,由此而产生的bug将难以捕获。
POSIX 线程API为SUSv3所接纳。
先介绍贯穿于Pthreads API的几个概念。
数据类型 | 描述 |
---|---|
pthread_t | 线程ID |
pthread_mutex_t | 互斥对象(Mutex) |
pthread_mutexattr_t | 互斥属性对象 |
pthread_cond_t | 条件变量(condition variable) |
pthread_condattr_t | 条件变量的属性对象 |
pthread_key_t | 线程特有数据的键(Key) |
pthread_once_t | 一次性初始化控制上下文 |
pthread_attr_t | 线程的属性对象 |
SUSv3并未规定如何实现这些数据结构,可移植的程序应将其视为“不透明”数据。
亦即,程序应避免对此类数据类型变量的结构或内容产生任何依赖。尤其是,不能使用C语言的比较操作符(==)去比较这些类型的变量。
在传统UNIX API中,errno是一个全局整型变量。然而这无法满足多线程程序的需要,这回引发竞争条件。因此,多线程程序中,每个线程都有属于自己的errno
。在Linux中,线程特有的errno
的实现方式与大多数UNIX实现相类似:
将errno
定义为一个宏,可展开为函数调用,该函数返回一个可修改的左值,且为每个线程所独有。(因为左值可以修改,多线程程序仍然能够以errno=value
的方式对errno
赋值)。
如今,需要声明
errno
的程序必须包含,以启用对
errno
的线程级实现。
从系统调用和库函数中返回状态,传统做法是:返回0表示成功,返回-1表示失败,并置errno
以标识错误。
Pthreads API反其道而行之。所有Pthreads API函数返回0表示成功,返回一正值表示失败。这一失败时的返回值,与传统UNIX系统调用置于errno
中的值含义相同。
由于多线程对errno
的每次引用都会带来函数调用的开销,因此,本书示例不会直接将Pthreads函数的返回值赋给errno
,而是使用一个中间变量 ,并利用自己实现的诊断函数errExitEN()
,如下所示:
pthread_t *thread;
int s;
s = pthread_create(&thread, NULL, func, &arg);
if( s!= 0)
errExitEN(s, "pthread_create);
在Linux平台上,在编译调用了Pthreads API的程序时,需要设置 cc -pthread
的编译选项。
该选项的效果如下:
_REENTRANT
预处理宏。这回公开对少数可重入(reentrant)函数的声明。libpthread
进行链接(等价于-lpthread
)启动程序时,产生的进程只有单条线程,称之为初始(initial)或主(main)线程。
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start)(void *), void *arg);
//Returns 0 on success, or a positive error number on error.
新线程通过调用start(arg)
而开始执行。
调用pthread_create()
的线程会继续执行该调用之后的语句。
参数start和arg
arg
声明为void *
类型,意味着可以通过将指向任意对象的指针传递给start()
函数。一般情况下,arg
指向一个全局或堆变量,也可将其置位NULL
。 如果需要向start()
传递多个参数,可以将arg
指向一个结构。通过审慎的类型强制转换, arg
甚至可以传递int
类型的值。严格来说,对于
int
和void *
间相互强制转换的后果,C语言标准并未加以定义。不过大多数C语言编译器允许这样的操作,并且也能达成预期的目的,即int j == (int)((void*)j)
。
start
函数的返回值时,必须小心谨慎。原因在于,取消线程时的返回值PTHREAD_CNACELLED
通常是由实现所定义的整型值,再经强制转换为void*
。若线程某甲的start
将此整型值返回给正在执行pthread_join()
操作的线程某乙,某乙会误认为某甲遭到了取消。start
函数的返回值强制转换为整型,那么就必须确保线程正常结束时的返回值与当前Pthreads实现中的PTHREAD_CANCELLED
不同。如欲保证程序的可移植性,则在任何将要运行该应用的实现中,正常退出线程的返回值应不同于PTHREAD_CANCELLED
值。参数thread
thread
指向pthread_t
类型的缓冲区,在pthread_create()
返回前,会在此保存一个该线程的唯一标识,后续的Pthreads函数将使用该标识来引用此线程。SUSv3曾明确指出,在新线程开始执行之前,实现无需对
thread
参数所指向的缓冲区进行初始化,即线程可能会在pthread_create()
返回之前就已经开始运行,如果新线程需要获取自己的线程ID,则只能使用pthread_self()
。
参数attr
attr
是指向pthread_attr_t
对象的指针,该对象指定了新线程的各种属性。如果将attr
设置为NULL
,那么创建的新线程将使用各种默认属性。调用pthread_create()
后,应用程序无从确定系统接着会调度哪一个线程来使用CPU资源。程序如隐含了对特定调度顺序的依赖,会导致竞争条件。如果对执行顺序有强制要求,需要使用同步技术。
可以如下方式终止线程的运行:
start
函数执行return
并返回指定值。pthread_exit()
。pthread_cancelled()
取消线程。exit()
,或者主线程执行了return
语句(在main()
函数中) ,都会导致进程中所有线程立即终止。pthread_exit()
函数将终止调用线程,且其返回值可由另一线程通过调用pthread_join()
来获取。
#include
void pthread_exit(void *retval);
调用pthread_exit()
相当于在线程的start
函数中执行return
,不同之处在于,可在线程start
函数所调用的任意函数中调用pthread_exit()
。
参数retval
指定了线程的返回值。retval
所指向的内容不应分配于线程栈中,因为线程终止后,将无法确定线程栈的内容是否有效。
出于同样的理由,也不应在线程栈中分配线程start
函数的返回值。
如果主线程调用了pthread_exit()
,而非调用exit()
或是执行return
语句,那么其他线程将继续运行。
进程内部的每个线程都有一个唯一标识,称为线程ID。线程ID会返回给pthread_create()
调用者,一个线程可以通过pthread_self()
来获取自己的线程ID。
#incldue <pthread.h>
pthread_t pthread_self(void);
//Return the thread ID of the calling thread.
线程ID的用途:
pthread_join()
、pthread_detach()
、pthread_cancel()
和pthread_kill()
等。pthread_equal()
可检查两个线程的ID是否相同。#include
int pthread_equal(pthread_t t1, pthread_t t2);
//Returns nonzero value if t1 and t2 are equal, otherwise 0;
例如,为了检查调用线程的线程ID与保存与变量t1中的线程ID是否一致,可以这样编写:
if(pthread_equal(tid, pthread_self()))
printf("tid matches self\n);
因为必须将pthread_t
作为一种不透明的数据类型加以对待,所以函数pthread_equal()
是必须的。Linux将pthread_t
定义为无符号长整型(unsigned long),但在其他实现中,则有可能是一个指针或结构。
在NPTL中,
pthread_t
实际上是一个经强制转化而为无符号长整型的指针。
SUSv3并未要求将
pthread_t
实现为一个标量类型,该类型也可以是一个结构。因此下列显示线程ID的代码实例并不具有可移植性(尽管在包括Linux在内的许多实现上均可正常运行,而且有时调试程序还很实用。)
pthread_t thr;
printf("Thread ID = %ld\n", (long)thr); //WRONG!
在Linux的线程实现中,线程ID在所有进程中都是唯一的。不过在其他实现中未必如此。
在对已终止线程施以pthread_join
,或者在已分离(detached)线程退出后,实现可以复用该线程的线程ID。
POSIX线程ID与Linux专有的系统调用
getpid()
所返回的线程ID并不相同。POSIX线程ID由线程库实现来负责分配和维护。getpid()
返回的线程ID是一个由内核(Kernel)分配的数字,类似于进程ID(process ID)。虽然在Linux NPTL线程实现中,每个POSIX线程都对应一个唯一的内核线程ID,但应用程序一般无需了解内核线程ID(况且,如果程序依赖于此信息 ,也将无法移植。)
函数pthread_join()
等待由thread标识的线程终止。(如果线程已经终止,pthread_join()
会立即返回)。这种操作被称为连接(joining)。
#include
int pthread_join(pthread_t thread, void **retval);
//Returns 0 on success, or a positive error number on error.
若retval
为一非空指针,将会保存线程终止时返回值的拷贝,该返回值亦即线程调用return
或pthread_exit()
时所指定的值。
如向pthread_join()
传入一个之前已经连接过的线程ID,将会导致无法预知的行为。例如,相同的线程ID在参与一次连接后恰好为另一新线程所重用,再度连接的可能就是这个新线程。
若线程并未分离( detached),则必须使用pthread_join()
来进行连接。如果未能连接,那么线程终止时将产生僵尸线程,与僵尸进程的概念相类似。除了浪费系统资源外,僵尸线程累积过多,应用将再也无法创建新的线程。
pthread_join()
执行的功能类似于针对进程的waitpid()
调用,不过二者之间存在一些显著差别。
pthread_join()
与该进程的任何其他线程连接起来。
fork()
创建了子进程,那么它也是唯一能够对子进程调用wait()
的进程。调用pthread_create()
创建的新线程与发起调用的线程之间,就没有这样的关系。waitpid(-1, &status, options)
做到这一点),也不能以非阻塞(nonblocking)方式进行连接(类似于设置WHOHANG
标志的waitpid()
)。使用条件(condition)变量可以实现类似的功能。限制
pthread_join()
只能连接特定线程ID,其用意在于:程序应只能连接它所“知道的”线程,线程之间并无层次关系,如果听任“与任意线程连接”的操作发生,那么“任意”线程就可以包括由库函数私自创建的线程,从而带来问题,结果是,函数库的获取线程返回状态时将不再能与该线程连接,只会一错再错,试图连接一个已经连接过的线程ID。换言之,“连接任意线程”的操作与模块化的程序设计理念背道而驰。
#include
#include
#include
#include
static void *threadFunc(void *arg)
{
char *s = (char *)arg;
printf("%s", s);
return (void *)strlen(s);
}
int main(void)
{
pthread_t t1;
void *res;
int s;
s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");
if (s != 0) {
fprintf(stderr, "error\n");
exit(EXIT_FAILURE);
}
printf("Message from main()\n");
s = pthread_join(t1, &res);
if (s != 0) {
fprintf(stderr, "error s!= 0\n");
exit(EXIT_FAILURE);
}
printf("Thread returned %ld\n", (long)res);
exit(EXIT_SUCCESS);
}
$gcc 1.c -o 1 -lpthread
$./1
Message from main()
Hello world
Thread return 12
默认情况下,线程是可连接的(joinable),也就是说,当线程退出时,其他线程可以通过调用pthread_join
获取其返回状态。
有时,程序员并不关心线程的返回状态,只是希望系统在线程终止时能够自动清理并移除之。在这种情况下,可以调用pthread_detach()
并向thread
参数传入指定线程的标识符,将该线程标记为处于分离(detached)状态。
#include
int pthread_detach(pthread_t thread);
//Returns 0 on success, or a positive error number on error.
如下例所示,使用pthread_deatch()
,线程可以自动分离。
pthread_detach(pthread_self());
一旦线程处于分离状态,就不能再使用pthread_join()
来获取其状态,也无法使其重返“可连接”状态。
其他线程调用了exit()
,或是主线程执行return
语句时,即使遭到分离的线程也还是会受到影响。
此时,不管线程处于可连接还是已分离状态,进程的所有线程会立即终止,换言之,pthread_detach()
只是控制线程终止之后发生的事,而非何时或如何终止线程。
这里只点出如下之类的一些属性:线程栈的位置和大小、线程调度策略和优先级,以及线程是否处于可连接或分离状态。
下列代码示例创建了一个新线程,该线程刚一创建就遭到分离(而非之后再调用pthread_detach()
)。这段代码首先以缺省值对线程属性结构进行初始化,接着为创建分离线程而设置属性,最后再以此线程属性结构来创建新线程。线程一旦创建,就无需再保留该属性对象,故而程序将其销毁。
pthread_t thr;
pthread_attr_t attr;
int s;
s = pthread_attr_init(&attr);
if (s != 0) errExitEN(s, "pthread_attr_init");
s = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (s != 0) errExitEN(s, "pthread_attr_setdetachstate");
s = pthread_create(&thr, &attr, threadFunc, (void*)1);
if (s != 0) errExitEN(s, "pthread_create");
s = pthread_attr_destory(&attr);
if (s != 0) errExitEN(s, "pthread_attr_destory");
将应用程序实现为一组线程还是进程?这里简单考虑一些可能影响这一决定的部分因素。
多线程的优点:
线程相比进程的缺点:
影响选择的还有如下几点: