11.1 引言
了解如何使用多个控制线程(简称线程) 在单进程环境中执行多个任务。一个进程中的所有线程都可以访问该进程的组成部件,如文件描述符fd和内存。
不管在什么情况下,只要单个资源需要在多个用户间共享,就必须处理一致性问题。本章最后讨论目前可用的线程同步机制,防止多个线程在共享资源时出现不一致问题
11.2 线程概念
典型的UNIX进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。有了多个控制线程以后,在程序设计时就可以把进程设计在某一个时刻下能够做不止一件事,每个线程处理各自独立的任务。这种方法有很多好处:
-
通过为每种事件类型分配单独的处理线程,可以简化处理异步事件的代码。每个线程在进行事件处理时可以采用同步编程模式,同步编程模式要比异步编程模式简单的多。
-
多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享,而多个线程可以自动地访问相同的存储地址空间和文件描述符。
-
有些问题可以分解从而提高整个程序的吞吐量。在只有一个控制线程的情况下,一个单线程进程要完成多个任务,只需要将这些任务串行化。但有多个控制线程时,相互独立的任务的处理就可以交叉进行,此时只需要为每个任务分配一个单独的线程。当然只有在两个任务的处理过程不相互依赖的情况下,两个任务才可以交叉运行。
-
交互的程序同样可以通过多线程来改善相应时间,多线程可以把程序中处理用户输入输出的部分与其他部分分开。
有些人将多线程的程序设计与多处理器或者多核系统联系起来。但是即使程序运行在单处理器上,也能得到多线程编程模型的好处。处理器的数量并不影响程序结构,所以不管处理器的个数多少,程序都可以通过使用线程得以简化。而且,即使多线程程序在串行化任务时不得不阻塞,由于某些线程在阻塞时还有另外一些线程可以运行,所以多线程程序在单处理器上运行还是可以改善响应时间和吞吐量。
每个线程都包含有表示执行环境所必须的信息,其中包括进程中标识线程的线程ID,一组寄存器的值,栈,调度优先级和策略,信号屏蔽字,errno变量以及线程私有数据。一个进程的所有信息对于该进程的所有线程都是共享的,包括可执行程序的代码,程序的全局内存和堆内存,栈以及文件描述符。
主要讨论的线程结构来自POSIX.1-2001.线程接口也成为pthread 或者 POSIX线程.
11.3 线程标识
实现的时候使用一个结构代表pthread_t数据类型,所以可移植的操作系统实现不能将其作为整数处理。因此必须使用一个函数来对两个线程进行比较。
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
使用结构体pthread_t 数据类型的后果是不能用一种可移植的方式来打印该数据类型的值。 在程序调试的时候打印线程id是非常有用的,而在其他情况下通常不需要打印线程ID。因此最坏的情况是,有可能出现不可移植的调试代码。
线程可以通过pthread_self函数获得自身线程ID
#include <pthread.h>
pthread_t phtread_self(void);
当线程需要识别线程ID作为标识的数据结构时,pthread_self 函数可以与pthread_equal 函数一起使用。例如,主线程可能把工作任务放在一个队列中,用线程ID控制每个工作线程处理哪些作业。主线程不允许每个线程任意处理从队列顶端取出的作业,而是由主线程控制作业的分配,主线程会在每个待处理的结构中放置处理该作业的线程ID,每个工作线程只能移出标有自己线程ID的作业。
11.4 线程创建
在传统的UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程中只包含一个线程是相同的。在POSIX线程(pthread)情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。新增的线程可以通过pthread_create函数创建。
#include <phtread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t* restrict attr,
void*(*start_rtn)(void*),
void* restrict arg);
当pthread_create成功返回时,新创建的线程ID会被设置成tidp指向的内存单元。attr参数用于定制各种不同的线程属性。指定为NULL为创建一个具有默认属性的线程。
新创建的线程从start_rtn 函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传入的参数有一个以上,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
创建线程时并不能保证哪个线程先运行 :是新创建的线程,还是调用线程。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。
注意,pthread函数在调用失败时通常会返回错误码,它们并不想其他POSIX函数一样设置errno。每个线程都提供errno副本,这只是为了与使用errno的现有函数兼容。在线程中,从函数中返回错误码更为清晰整洁,不需要依赖哪些随着函数执行不断变化的全局状态,这样可以将错误的范围限制在引起出错的函数中。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.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 NULL;
}
int main()
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if(err!=0)
{
perror("pthread_create");
exit(0);
}
printids("main thread:");
sleep(1);
exit(0);
}
patrick@ubuntu:~/apue/chapter11$ ./11-1
main thread: pid 50677 tid 140273204557632 (0x7f93e6877740)
new thread: pid 50677 tid 140273196046080 (0x7f93e6059700)
这个实例有两个特别之处,需要处理主线程和新线程之间的竞争。第一个特别之处在于,主线程需要休眠,如果主线程不休眠,它就可能退出,这事线程还没有机会运行,整个进程可能就已经终止了。这种行为特征依赖于操作系统中的线程实现和调度算法。
第二个特别之处在于新线程是通过pthread_self函数获得自己的线程ID的,而不是从共享内存中读出的,或者从线程的启动例程中以参数的形式接收到的。回忆pthread_create函数,它会通过第一个参数(tidp)返回新建线程的线程ID。在这个例子中,主线程将新线程ID存放在ntid中,但是新建的线程并不能安全地使用它,如果新线程在主线程调用pthread_create返回之前就运行了,那么新线程看到的是未初始化的nitd内容,这个内容并不是正确的线程ID。
11.5 线程终止
如果进程中的任意线程调用了exit,_Exit, 或者 _exit,那么整个进程就会终止。与此相类似,如果默认的动作是终止进程,那么,发送到线程的信号就会终止整个进程(12.8中讨论信号与线程间是如何交互的)。
单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
- 线程可以简单地从启动例程(start_rtn)中返回,返回值是线程的退出码(return)。
- 线程可以被同一进程中的其他线程取消(pthread_cancel)。
- 线程调用pthread_exit。
#include <pthread.h>
void phtread_exit(void* rval_ptr);
rval_ptr参数是一个无类型指针,与传给启动例程的单个参数类似。进程中其他线程也可以通过pthread_join函数访问到整个指针。
#include<pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
程序11-3展示了如何获取已终止的线程退出码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
void* thr_fn1(void* arg)
{
printf("thread 1 returning\n");
return ((void*)1);
}
void* thr_fn2(void* arg)
{
printf("thread 2 exiting\n");
pthread_exit((void*)2);
}
int main()
{
int err;
pthread_t tid1, tid2;
void* tret;
err= pthread_create(&tid1, NULL, thr_fn1, NULL);
if(err!=0)
{
perror("pthread_create thread1");
exit(0);
}
err= pthread_create(&tid2, NULL, thr_fn2, NULL);
if(err!=0)
{
perror("pthread_create thread2");
exit(0);
}
err = pthread_join(tid1, & tret);
if(err!= 0)
{
perror("pthread_join thread1");
exit(0);
}
printf("thread1 exit with code %ld\n", (long)tret);
err = pthread_join(tid2, & tret);
if(err!= 0)
{
perror("pthread_join thread2");
exit(0);
}
printf("thread2 exit with code %ld\n", (long)tret);
exit(0);
}
patrick@ubuntu:~/apue/chapter11$ ./11-3
thread 1 returning
thread 2 exiting
thread1 exit with code 1
thread2 exit with code 2
可以看到,当一个线程通过调用pthread_exit退出或者简单地从启动例程中返回时,进程中的其他线程可以用过调用pthread_join函数获得该线程的退出状态。
pthread_create和pthread_exit函数的无类型指针参数可以传递的值不止一个,这个指针可以传递包含负责信息的结构和地址,但是注意,这个结构所使用的内存在调用者完成调用以后必须仍然是有效的。例如,在调用线程的栈上分配了该结构,然后把指向这个结构的指针传递给了pthread_exit,那么调用pthread_join的线程试图使用该结构时,这个栈有可能已经被撤销,这块内存也已另作他用。
程序11-4 给出了自动变量(分配在栈上)作为pthread_exit的参数时出现的问题
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
struct foo
{
int a, b, c, d;
};
void printfoo(const char*s, const struct foo *fp)
{
printf("%s",s);
printf(" structure at 0x%lx\n", (unsigned long) fp);
printf(" foo.a = %d\n", fp->a);
printf(" foo.b = %d\n", fp->b);
printf(" foo.c = %d\n", fp->c);
printf(" foo.d = %d\n", fp->d);
}
void* thr_fn1(void* arg)
{
struct foo foo = {
1,2,3,4};
printfoo("thread 1:\n", &foo);
pthread_exit((void*)&foo); // stack variable (auto variable)
}
void* thr_fn2(void* arg)
{
printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
pthread_exit((void*)0);
}
int main()
{
int err;
pthread_t tid1, tid2;
struct foo *fp;
err = pthread_create(&tid1,NULL,thr_fn1, NULL);
if(err!=0)
{
perror("pthread_create");
exit(0);
}
err= pthread_join(tid1, (void*)&fp);
if(err!=0)
{
perror("pthread_join thread1");
exit(0);
}
sleep(1);
printf("parent starting second thread\n");
err= pthread_create(&tid2, NULL, thr_fn2, NULL);
if(err!=0)
{
perror("pthread_create thread2");
exit(0);
}
sleep(1);
printfoo