ID
、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno
变量以及线程私有数据。pthread_t
数据类型表示。对于该类型的实现不同操作系统不同,Linux是unsigned long int
,某些操作系统用一个结构体实现pthread_t
。因此不能将pthread_t
类型当做整数处理(如用数值的方式进行比较)。因此必须通过一个函数来对两个线程ID进行比较pthread_equal
比较两个线程IDint pthread_equal(pthread_t t1, pthread_t t2);
pthread_self
函数获得自身线程IDpthread_t pthread_self(void);
pthread_create
函数创建一个线程int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
thread
参数:若线程创建成功,保存该线程ID
attr
:定制各种不同的线程属性。NULL
即为默认属性start_routine
:线程从该地址开始运行,该函数接受一个void *
参数并返回一个void *
类型arg
:传递给start_routine
的参数值。如果需要向start_routine
函数传递的参数有一个以上,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg
参数传入。POSIX
函数一样设置errno
pthread_create
函数的第一个参数,不能在start_routine
线程函数中使用!因为无法保证哪个线程先执行,因此如果新线程在主线程调用pthread_create
返回之前就运行了,那么新线程看到的是未经初始化的thread
成员。应该通过pthread_self
获取自身线程ID。ID
、新线程的线程ID
以及初始线程的线程ID
。#include "apue.h"
#include
pthread_t ntid;
void
printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
/*这里使用pthread_self来获取自己的线程ID,而不是从共享内存或者从线程的启动例程中以参数的形式接收到的。
在这个例子中,主线程把新线程ID存放在了ntid中,如果新线程在主线程调用pthread_create之前就返回了,
那么显然ntid中的内容并不是正确的线程ID*/
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);
}
命令行输出lh@LH_LINUX:~/桌面/apue.3e/threads$ ./threadid
main thread: pid 3343 tid 140680582641408 (0x7ff2c027b700)
new thread: pid 3343 tid 140680574322432 (0x7ff2bfa8c700)
如果进程中的任何线程调用了exit
、_Exit
或_exit
,那么整个进程就会终止。
对于某个信号,如果默认的动作是终止进程,那么发送到线程的该信号就会终止整个进程。
单个线程可以通过以下3种方式退出,并且不导致整个进程终止:
pthread_exit
pthread_exit
void pthread_exit(void *retval);
retval
是一个void *
类型指针,即为线程退出码,进程中的其他线程可以通过pthread_join
函数访问这个指针。pthread_join
int pthread_join(pthread_t thread, void **retval);
pthread_exit
、从启动例程中返回或被取消)。retval
保存了指定线程return
值、pthread_exit
参数值、如果线程是被pthread_cancel
的则是PTHREAD_CANCELED
。pthread_detach
),则pthread_join
失败,返回EINVAL
。实例:获取已终止线程的退出码。
#include "apue.h"
#include
void *
thr_fn1(void *arg)
{
printf("thread 1 returning\n");
/*线程退出方式1:简单地从启动例程中返回*/
return((void *)1);
}
void *
thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
/*线程退出方式2:线程调用pthread_exit*/
pthread_exit((void *)2);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret);
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/threads$ ./exitstatus
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2
pthread_exit
退出或者简单地从启动例程中返回时,进程中的其他线程可以通过调用pthread_join
函数获得该线程的退出状态。pthread_join
可以使调用线程一直阻塞,直到指定线程退出,所以不用担心同步问题。实例:给出了局部变量(分配在栈上)作为pthread_exit
的参数时出现的问题。
#include "apue.h"
#include
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);
}
void *
thr_fn2(void *arg)
{
printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
pthread_exit((void *)0);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
struct foo *fp;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
err_exit(err, "can't join with thread 1");
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/threads$ ./badexit2
thread 1:
structure at 0x7f7355e59f30
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 140133339080448
parent:
structure at 0x7f7355e59f30
foo.a = 1445050624
foo.b = 32627
foo.c = 1442644546
foo.d = 32627
tid1
的栈上分配的)已经改变了。注意第二个线程(tid2
)的栈是如何覆盖第一个线程的。为了解决这个问题,可以使用全局结构,或者用malloc
函数分配结构。pthread_cancel
通过pthread_cancel
函数来请求取消同一进程中的其他线程。
int pthread_cancel(pthread_t thread);
thread
标识的线程行为如同调用了pthread_exit(PTHREAD_CANCEL)
,但是线程可以选择忽略取消或者控制如何被取消。pthread_cancel
并不等待线程终止,仅仅提出请求,而该请求可能会被忽略。线程清理处理程序
atexit
函数安排退出类似。void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
pthread_cleanup_push
arg
参数即为传递给routine
线程清理处理程序的参数值pthread_exit
pthread_cancel
)pthread_cleanup_pop
return
语句),那么不会调用线程清理处理程序。pthread_cleanup_pop
:
pthread_cleanup_push
建立的线程清理处理程序。但是如果execute
参数设置为0
,清理函数将不被调用。pthread_cleanup_push
和pthread_cleanup_pop
必须成对出现:pthread_cleanup_push
包含{
,pthread_cleanup_pop
包含}
,因此:
push
与pop
一定是成对出现的push
可以有多个,同样的pop
也要对应的数量,遵循"先进后出原则"。给出了一个如何使用线程处理程序的例子
#include "apue.h"
#include
void
cleanup(void *arg)
{
printf("cleanup: %s\n", (char *)arg);
}
void *
thr_fn1(void *arg)
{
printf("thread 1 start\n");
/*建立清理处理程序*/
pthread_cleanup_push(cleanup, "thread 1 first handler");
pthread_cleanup_push(cleanup, "thread 1 second handler");
printf("thread 1 push complete\n");
if (arg)
/*从启动例程中返回而终止,清理处理程序不会被调用*/
return((void *)1);
/*传递参数0,将不调用清理函数,那为什么还要调用pthread_cleanup_pop?
因为pthread_cleanup_pop需要和pthread_cleanup_push匹配起来,否则可能编译不通过*/
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return((void *)1);
}
void *
thr_fn2(void *arg)
{
printf("thread 2 start\n");
pthread_cleanup_push(cleanup, "thread 2 first handler");
pthread_cleanup_push(cleanup, "thread 2 second handler");
printf("thread 2 push complete\n");
if (arg)
/*调用pthread_exit后,清理处理程序被调用*/
pthread_exit((void *)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void *)2);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
if (err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret);
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/threads$ ./cleanup
thread 2 start
thread 2 push complete
thread 1 start
thread 1 push complete
thread 1 exit code 1
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 2 exit code 2
pthread_detach
pthread_join
。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join
获取它的终止状态。pthread_detach
函数分离线程int pthread_detach(pthread_t thread);
mutex
本质是一把锁,在访问共享资源前对互斥量加锁,在访问完成后解锁互斥量。pthread_mutex_t
数据类型表示,在使用之前必须进行初始化。如果是静态分配的互斥量,可以将它设置为PTHREAD_MUTEX_INITIALIZER
进行初始化。也可以通过pthread_mutex_init
函数初始化。如果是动态分配的互斥量(如用malloc
分配),则在释放内存前需要调用pthread_mutex_destroy
。/*成功则返回0,错误则返回编号*/
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
attr
设为NULL
。int pthread_mutex_lock (pthread_mutex_t *__mutex);
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
int pthread_mutex_unlock (pthread_mutex_t *__mutex)
pthread_mutex_lock
:对互斥量加锁。如果互斥量已经上锁,则调用线程将阻塞直到互斥量被解锁pthread_mutex_trylock
:对互斥量尝试加锁,如果调用该函数时互斥量处于未锁状态,则锁住互斥量并返回0
;如果互斥量处于加锁状态,则不阻塞直接返回EBUSY
。pthread_mutex_unlock
:对互斥量解锁#include
#include
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
/*没必要加锁,在这个操作之前分配线程是唯一引用该对象的线程。
如果在这之后要将该对象放到一个列表中,那就有可能被其他线程发现,需要加锁。*/
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
/* ... continue initialization ... */
}
return(fp);
}
/*线程需要调用foo_hold对这个对象的引用计数加1。*/
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
/*当对象使用完毕时,必须调用foo_rele释放引用。如果有另一个线程在在调用foo_hold时
阻塞等待互斥锁,这时即使该队形引用计数为0,fool_rele释放该对象的内存仍然是不对的。
可以通过确保在释放内存前不会被找到 这种方式来避免上述问题。*/
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
/*当最后一个引用被释放时,对象所占的内存空间就被释放*/
if (--fp->f_count == 0) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
pthread_mutex_unlock(&fp->f_lock);
}
}
产生死锁的条件
如何避免死锁?
实例:更新6.1
节中的程序,展示了两个互斥量的使用方法。在同时需要两个互斥量时,总是让他们以相同的顺序加锁,这样可以避免死锁。此外,本例涉及散列表相关知识,详见散列表。本例中使用的散列函数的构造方法是除留余数法,哈希碰撞的处理基于拉链法。
#include
#include
#define NHASH 29
/*定义散列函数:除留余数法*/
#define HASH(id) (((unsigned long)id)%NHASH)
/*定义一个散列表*/
struct foo *fh[NHASH];
/*初始化互斥量hashlock*/
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
/*定义散列链的各字段*/
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
struct foo *f_next; /* protected by hashlock */
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
/*初始化互斥量fp->f_lock*/
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
/*通过key获取散列值*/
idx = HASH(id);
/*锁定互斥量hashlock,可以看出它用于保护散列链字段f_next和散列表fh。
加锁的原因:在这之后要将该对象放到一个列表中,那就有可能被其他线程发现,需要加锁。*/
pthread_mutex_lock(&hashlock);
/*维护散列链*/
fp->f_next = fh[idx];
fh[idx] = fp;
/*锁定互斥量f_lock,它用于保护对foo结构中的其他字段的访问*/
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
/*搜索被请求的结构,如果找到了,那么就增加其引用计数,并返回该结构的指针*/
struct foo *
foo_find(int id) /* find an existing object */
{
struct foo *fp;
/*锁住散列列表锁,然后搜索被请求的结构。注意,加锁的顺序是:
先锁定散列列表锁hashlock,再通过调用foo_hold锁定f_lock互斥量。
这样就与foo_alloc中的操作保持了一致。*/
pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
foo_hold(fp);
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
struct foo *tfp;
int idx;
/*如果这是最后一个引用,就需要对这个结构互斥量进行解锁,因为我们需要从散列列表中删除这个结构,这样才可以获取散列列表锁,然后重新获取结构互斥量。*/
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count == 1) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
/* need to recheck the condition */
/*从上一次获得结构互斥量以来我们可能被阻塞着,所以需要重新
检查条件,判断是否还需要释放这个结构*/
if (fp->f_count != 1) {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
/* remove from list */
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
/*如果不是最后一个引用,那么只需要简单地对整个引用计数减1,
并解锁所有互斥量*/
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}
可以看出foo_rele
函数设计地十分复杂,为了简化代码,我们可以在一开始就使用hashlock
,代码更正如下:
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&hashlock);
if (--fp->f_count == 0) { /* last reference, remove from list */
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
pthread_mutex_unlock(&hashlock);
}
}
pthread_mutex_timelock
指定阻塞时间int pthread_mutex_timedlock (pthread_mutex_t * mutex, const struct timespec *abstime)
pthread_mutex_lock
和pthread_mutex_trylock
之间abstime
指定的时间是绝对时间而不是相对时间abstime
后,该函数不会对互斥量加锁,而是返回错误ETIMEDOUT