第11章 线程
典型的UNIX进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程以后,在程序设计时可以把线程设计成在同一时刻能够做不止一件事情,每个线程处理各自独立的任务。
(1)通过为每种事件类型的处理分配单独的线程,能够简化处理异步事件的代码。
(2)多个进程必需使用操作系统提供的复杂机制才能实现内存和文件描述符的共享。
(3)有些问题可以通过将其分解从而改善整个程序的吞吐量。
(4)交互的程序同样可以通过使用多线程实现响应时间得改善,多线程可以把程序中处理用户输入输出的部分与其他部分分开。
一、线程标识
就像每个进程ID一样,每个线程也有一个线程ID。进程在整个系统中是唯一的,但线程ID不同,线程ID只在它所属的进程环境中有效。
线程用pthread_t数据类型来表示,实现的时候可以用一个结构来代表pthread_t数据类型,所以,可移植的操作系统实现不能把它作为整数处理。因此必需使用函数来对两个线程ID进行比较:
int pthread_equal(pthread_t tid1, pthread_t tid2);
线程可以通过pthread_self函数获得自身的线程ID:
pthread_t pthread_self(void);
二、线程创建
在传统的UINIX进程模型中,每个进程只有一个控制线程。在POSIX线程的情况下,程序开始运行时,它是以单进程中的单个控制线程启动的,在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。新增的线程可以通过调用pthread_create函数创建:
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t, void *(*start_rtn)(void *), void *restrict arg);
注意:pthread函数在调用失败时通常会返回错误码,它们并不像其他POSIX函数一样设置errno。
实例:创建一个线程并且打印进程ID、新线程的线程ID以及初始现成的线程ID。
#include "apue.h"
#include <dlfcn.h>
#include <pthread.h>
static void * thr_fn(void *arg);
static void printids(const char *s);
pthread_t ntid;
static void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, (unsigned int)tid,(unsigned int)tid);
}
static 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_quit("can't create thread: %s\n", strerror(err));
}
printids("main thread: ");
sleep(1);
exit(0);
}
编译和运行:
gcc 11.1.c -o 11.1 -lpthread
./11.1
main thread: pid 4381 tid 3078342336 (0xb77bc6c0)
new thread: pid 4381 tid 3078339440 (0xb77bbb70)
这个实例有两个特别之处,需要处理主线程和新线程之间的竞争。首先是主线程需要休眠,如果主线程不休眠,它就可能退出,这样在新线程有机会运行之前整个进程可能就已经终止了。这种行为特征依赖于操作系统中的线程实现和调度算法。
三、线程终止
如果进程中的任意线程调用了exit、_Exit、_exit,那么整个进程就会终止。与此类似,如果信号的默认动作是终止进程,那么把该信号发送到线程会终止整个进程。
单个线程可以通过下列三个方式退出:
(1)线程只是从启动例程中返回,返回值是线程的退出码.
(2)线程可以被同一进程中的其他线程取消.
(3)线程调用pthread_exit.
void pthread_exit(void *rval_ptr);
进程中的其它线程可以通过调用pthread_join函数访问到这个指针:
int pthrea_join(pthread_t thread, void **rval_ptr)
调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果线程只是从它的启动例程返回,rval_ptr将包含返回码。如果线程被取消,则有rval_ptr指定的内存单元就置为PTHREAD_CANCELED。
实例:获取已终止的线程的退出码:
#include "apue.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(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
{
err_quit("can't creat thread 1: %s\n", strerror(err));
}
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
{
err_quit("can't creat thread 2: %s\n", strerror(err));
}
err = pthread_join(tid1, &tret);
if (err != 0)
{
err_quit("can't join with thread 1: %s\n", strerror(err));
}
printf("thread 1 exit code %d\n", (int)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
{
err_quit("can't join with thread 2: %s\n", strerror(err));
}
printf("thread 2 exit code %d\n", (int)tret);
exit(0);
}
编译和运行结果:
gcc 11.2.c -o 11.2 -lpthread
./11.2
thread 2 exiting
thread 1 returning
thread 1 exit code 1
thread 2 exit code 2
可以看出,当一个线程通过调用pthread_exit退出或者简单地从启动例程中返回时, 进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态。
实例:pthread_exit函数的不正确使用。
#include "apue.h"
#include <pthread.h>
struct foo
{
int a;
int b;
int c;
int d;
};
void printfoo(const char *s, const struct foo *fp)
{
printf(s);
printf("struct at 0x%xx\n", (unsigned)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)
{
struct foo foo = {1, 2, 3, 4};
printfoo("thread 1: \n", &foo);
pthread_exit((void*)&foo);
}
void *thr_fn2(void)
{
printfoo("thread 2: ID is %d\n", 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_quit("can't create thread 1: %s\n", strerror(err));
}
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
{
err_quit("can't join with thread 1: %s\n", strerror(err));
}
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
{
err_quit("can't create thread 2: %s\n", strerror(err));
}
sleep(1);
printf("parent: \n", fp);
exit(0);
}
编译和运行结果:
gcc 11.3.c -o 11.3 -lpthread
thread 1:
struct at 0xb77e6370x
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 9120912
struct at 0xb77e6b70x
foo.a = -1216451728
foo.b = 138305552
foo.c = -1216451728
foo.d = 1
parent:
可以看出,当主线程访问这个结构时,结构的内容已经改变。为了解决这个问题,可以使用全局结构, 或者用malloc函数分配结构
pthread_cancel函数:
线程可以通过调用thread_cancel函数来请求取消同一进程中的其它线程:
int pthread_cancel(pthread_t tid);
pthread_cancel函数并不等待线程终止,它仅仅提出请求。
清理函数:
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int excute);
当线程执行以下动作时调用清理函数, 调用参数为arg,清理函数rtn的调用顺序是由pthread_cleanup_push函数来安排的。
(1)调用pthread_exit时
(2)响应取消请求时
(3)用非零excute参数调用pthread_cleanup_pop时;
实例:线程清理处理程序
#include "apue.h"
#include <pthread.h>
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);
}
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)
{
return((void *)2);
}
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return((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_quit("can't create thread 1: %s\n", strerror(err));
}
err = pthread_create(&tid2, NULL, thr_fn2, (void *)2);
if (err != 0)
{
err_quit("can't create thread 2: %s\n", strerror(err));
}
err = pthread_join(tid1, &tret);
if (err != 0)
{
err_quit("can't join with thread 1: %s\n", strerror(err));
}
printf("thread 1 exit code %s\n", (int)tret);
err = pthread_join(tid1, &tret);
if (err != 0)
{
err_quit("can't join with thread 1: %s\n", strerror(err));
}
printf("thread 2 exit code %s\n", (int)tret);
exit(0);
}
如果线程是通过它的启动例程中返回而终止的话,那么它的清理程序就不会被调用,还要注意清理程序是按照于它们安装时相反的顺序被调用的。
在默认情况下,线程的终止状态会保存到对该线程调用pthreat_join,如果线程已经处于分离状态,线程的底层存储资源可以在线程终止时立即被收回
pthread_detach调用可以用于使线程可以进入分离状态。
int pthread_detach(pthread_t tid);
四、线程同步
当某个线程可以修改变量,而其它线程也可以读取或者修改这个变量的时候,就需要对这些线程进行同步,以确保他们在访问变量的存储内容时不会访问到无效的数值。
为了解决这个问题,线程不得不使用锁,在同一时间只允许一个线程访问该变量。
当两个或多个线程试图在同一时间修改同一变量时, 也需要进行同步。
1.互斥量
可以通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量从本质上说是一 把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量的锁。
互斥量用pthread_mutex_t数据类型来表示,在使用互斥量以前,必须首先对它进行初始化。也可以通过调 用pthread_mutex_init函数进行初始化。如果动态分配互斥量,则释放内存前要调pthread_mutex_destroy。
int pthreat_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
要用默认的属性初始化互斥量,只需要把attr设置为NULL。
对互斥量进行加锁,需要调用pthread_mutex_lock。对互斥量解锁,需要调用pthread_mutex_unlock。
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:若成功则返回0, 否则返回错误编码。
实例:使用互斥量保护数据结构
#include <stdlib.h>
#include <pthread.h>
struct foo
{
int f_count;
pthread_mutex_t f_lock;
};
struct foo *
foo_alloc(void)
{
if ((fp = malloc(sizeof(struct foo))) != NULL)
{
fp->fp_count = 1;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0)
{
free(fp);
return(NULL);
}
}
return(fp);
}
void foo_hold(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void
foo_rele(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
if (--fp->f_count == 0)
{
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
}
else
{
pthread_mutex_unlock(&fp->flock);
}
}
2.避免死锁
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,使用互斥量时,还有其他更不明显 的方式也能产生死锁。
可以通过小心的控制互斥量加锁的顺序来避免死锁的发生。
实例:使用两个互斥量
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29;
#define HASH(fp) (((unsigned long)fp) % NHASH)
struct foo *fh[NHASH];
struct foo
{
int f_count;
pthread_mutex_t f_lock;
struct foo *next;
int f_id;
};
struct foo *
foo_alloc(void)
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL)
{
fp->fp_count = 1;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0)
{
free(fp);
return(NULL);
}
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
fp->f_next = fh(idx);
fh[idx] = fp;
pthread_mutex_lock(&fp->flock);
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void foo_hold(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
struct foo *
foo_find(int id)
{
struct foo *fp;
int idx;
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
for (fp = fh[idx]; 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)
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count == 1)
{
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count != 1)
{
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
idx = HASH(fp);
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->next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
}
else
{
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}
实例:简化的加、解锁
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29;
#define HASH(fp) (((unsigned long)fp) % NHASH)
struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALZER;
struct foo
{
int f_count;
pthread_mutex_t f_lock;
struct foo *next;
int f_id;
};
struct foo *
foo_alloc(void)
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL)
{
fp->fp_count = 1;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0)
{
free(fp);
return(NULL);
}
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
fp->f_next = fh(idx);
fh[idx] = fp;
pthread_mutex_lock(&fp->flock);
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void foo_hold(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
struct foo *
foo_find(int id)
{
struct foo *fp;
int idx;
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
for (fp = fh[idx]; fp != NULL; fp = fp->f_next)
{
if (fp->f_id == id)
{
fp->f_count++;
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}
void
foo_rele(struct foo *fp)
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&fp->f_lock);
if (--fp->f_count == 0)
{
idx = HASH(fp);
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(&fp->f_lock);
}
}
3.读写锁
读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态,要么是不加锁状态,而且一 次只有一个线程可以对其加锁。读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以占有读模式的读写锁。
实例:使用读写锁
#include <pthread.h>
struct job
{
struct job *j_next;
struct job *j_prev;
pthread_t j_id;
};
struct queue
{
struct job *q_head;
struct job *q_tail;
pthread_t q_lock;
};
int queue_init(stuct queue *qp)
{
int err;
qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->q_lock, NULL);
if (err != 0)
{
return(err);
}
return 0;
}
void
job_insert(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = qp->q_head;
jp->prev = NULL;
if (qp->q_head != NULL)
{
qp->q_head->j_prev = jp;
}
else
{
qp->q_tail = jp;
}
qp->q_head = jp;
}
void
job_append(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->lock);
jp->j_next = NULL;
jp->j_prev = qp->q_tail;
if (qp->q_tail != NULL)
{
qp->q_tail->j_next = jp;
}
else
{
qp->q_head = jp;
}
qp->q_tail = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
void
job_remove(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
if (jp == qp->q_head)
{
qp->q_head = jp->j_next;
if (qp->q_tail == jp)
{
q_tail = NULL;
}
}
else if (jp == qp->qtail)
{
qp->q_tail = jp->j_prev;
if (qp->q_head == jp)
{
qp->q_head = NULL;
}
}
else
{
jp->j_prev->j_next = jp->j_next;
jp->j_next->j_prev = jp->j_prev;
}
pthread_rwlock_unlock(&qp->q_lock);
}
struct job *
{
struct job *jp;
if (pthread_rwlock_rdlock(&qp->q_lock) != 0)
{
return(NULL);
}
for (jp = qp->q_head; jp != NULL; jp = jp->j_next)
{
if (pthread_equal(jp->j_id, id))
{
break;
}
}
pthread_rwlock_unlock(&qp->q_lock);
return (jp);
}
4.条件变量
条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会和的场所。条件变量与互斥量一 起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其它线程在获得互斥量之前不 会察觉到这种改变,因为必需锁住互斥量以后才能计算条件。
实例:使用条件变量
#include <stdlib.h>
struct msg
{
struct msg *m_next;
}
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALZER;
void process_msg(void)
{
struct msg *mp;
for (;;)
{
pthread_mutex_lock(&qlock);
while (workq == NULL)
{
pthread_cond_wait(&qready, &qlock);
}
mp = workq;
worq = mp->m_next;
pthread_mutex_unlock(&qlock);
}
}
void
equeue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
在本章中,介绍了线程的概念,讨论了现有的创建线程和销毁线程的POSIX.1原语;此外,还介绍了线程同步的问题,讨论了三种基本的同步机制:互斥、读写锁以及条件变量,了解了如何使用它们来保护共享资源。