UNPv1第二十三章:线程

在传统的UNIX模型中,当一个进程需要由另一个实体执行某件事时,该进程派生(fork)一个子进程,让子进程去进行处理。UNIX下的大多数网络服务器程序都是这么编写的,这在我们的并发服务程序例子中可以看出:父进程接收连接,派生子进程,子进程处理与客户的交互。

虽然这种模式很多年来使用的很好,但是fork有一些问题:

  1. fork是昂贵的。内存映像要从父进程拷贝到子进程,所有描述字要在子进程中复制等等。目前的实现使用一种称做写时拷贝(copy-on-write)技术,可避免父进程数据空间向子进程的拷贝,除非子进程需要自己的拷贝。尽管有这种优化技术,fork仍然是昂贵的。
  2. fork子进程后,需要用进程间通信(IPC)在父子进程之间传递信息。fork之前的信息容易传递,因为子进程一开始就有父进程数据空间及所有描述字的拷贝。但是从子进程返回信息给父进程需要做更多的工作。

线程有助于解决这两个问题。线程有时候称为轻权进程(lightweight process),因为线程比进程”轻权”。也就是说,创建线程要比创建进程块10~100倍。

一个进程中的所有线程共享相同的全局内存,这使得线程很容易共享信息,但是这种简易性也带来了同步(synchronization)问题。一个进程中的所有线程不仅共享全局变量,而且共享:

1. 进程指令
2. 大多数数据
3. 打开的文件(如描述字)
4. 信号处理程序和信号处置
5. 当前工作目录
6. 用户ID和组ID

但是每个线程有自己的:

1. 线程ID
2. 寄存器集合,包括程序计数器和栈指针
3. 栈(用于存放局部变量和返回地址)
4. errno
5. 信号掩码
6. 优先级

1. 基本线程函数:创建和终止

讲述5个基本线程函数,利用他们代替fork重新编写我们的TCP客户-服务器程序

(1)当一个程序由exec启动时,会创建一个称为初始线程(initial thread)或主线程(main thread)的单个线程。额外线程则由pthread_create函数创建。

#include <pthread.h> 
int pthread_create(pthread_t * tid, const pthread_attr_t * attr, void *  (*func)(void *), void * arg);  
//return: success 0, failed Exxx

一个进程中的每个线程都由一个线程ID(thread ID)标识,其数据类型是pthread_t(常常是unsigned int)。如果新的线程创建成功,其ID将通过tid指针返回。
每个线程都有很多属性(attribute):优先级,起始栈大小,是否应该是一个守护线程,得等。我们通常使用缺省值,将attr参数说明为空指针。
最后,当创建一个线程时,我们要说明一个它将执行的函数。函数的地址由func参数指定,该函数的调用参数是一个指针arg。如果我们需要多个调用参数,我们必须将它们打包成一个结构,然后将其地址当作唯一的参数传递给起始函数。
注意func和arg的声明,func函数取一个通用指针(void )参数,并返回一个通用指针(void )。这就使得我们可以传递一个指针(指向任何我们想要指向的东西)给线程,由线程返回一个指针(同样地,指向任何我们想要指向的东西)。

(2)我们可以调用pthread_join等待一个线程终止。把线程和UNIX进程相比,pthread_create类似于fork,pthread_join类似与waitpid。

#include <pthread.h>
int pthread_join(pthread_t tid, void * * status); 
//返回:成功为0,出错为正的Exxx值

我们必须指定要等待线程的tid。很可惜,我们没有办法等待任意一个线程结束(类似于waitpid的进程ID参数为-1的情况)。我们在讨论图23.14时还将涉及这个问题。如果,status指针非空,线程的返回值(一个指向某个对象的指针)将存放在status指向的位置。

(3)每个线程都有一个ID以在给定的进程内标识自己。线程ID由pthread_create返回,我们也看到了它在pthread_join中的使用。线程用pthread_self取得自己的线程ID。

#include <pthread.h>
pthread_t pthread_self(void);  //返回:调用线程的线程ID

与UNIX进程相比,线程的pthread_self类似于getpid。

(4)线程或者是可汇合的(joinable)或者是脱离的(detached)。当可汇合的线程终止时,其线程ID和退出状态将保留,直到另外一个线程调用pthread_join。脱离的线程则像守护进程:当它终止时,所有的资源都将释放,我们不能等待它终止。如果一个线程需要知道另一个线程什么时候终止,最好保留第二个线程的可汇合性。
pthread_detach函数将指定的线程变为脱离的。

#include <pthread.h>
int pthread_detach(pthread_t tid); 
//返回:成功为0,出错为正Exxx值

该函数通常被想脱离自己的线程调用,如: pthread_detach( pthread_self() );

(5)终止线程的一种方法是调用pthread_exit

#include <pthread.h>
void pthread_exit(void * status);

如果线程未脱离,其线程ID和退出状态将一直保留到调用进程中的某个其他线程调用pthread_join。
指针status不能指向局部于调用线程的对象,因为线程终止时这些对象也消失。
有两种其他方法可使线程终止:
1. 启动线程的函数(pthread_create的第3个参数)返回。既然该函数必须说明为返回一个void指针,该返回值便是线程的终止状态。
2. 如果进程的main函数返回或者任何线程调用了exit,进程将终止,线程将随之终止。

2. 互斥锁

我们称线程编程为并发编程(concurrent programming)或并行编程(parallel programming),因为多个线程可并发运行并访问相同的变量。虽然我们刚刚讨论的错误情形以单CPU系统为前提,但是如果线程A和线程B在多处理器系统的不同CPU上同时运行,潜在的错误同样存在。在通常的Unix编程中,我们没有遇到这种并发编程问题,因为用fork时,除了描述字外,父进程和子进程不共享任何东西。但是,当我们讨论进程间的共享内存时仍将遇到这类问题。
我们刚刚讨论的问题,即多个线程修改一个共享变量,是最简单的问题。解决方法是用一个互斥锁(mutex, 代表mutual exclusion)保护共享变量。只有我们持有该互斥锁才能访问该变量。在Pthreads中,互斥锁是类型为pthread_mutex_t的变量。我们用下面两个函数为互斥锁加锁和解锁。

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t * mptr);  
        //return: success 0, failed Exxx
int pthread_mutex_unlock(pthread_mutex_t * mptr);  
        //return: success 0, failed Exxx

如果我们试图为一个已被其他线程锁住的互斥锁加锁,程序便会阻塞直到该互斥锁被解锁。
如果互斥锁变量是静态分配的,我们必须将它初始化为常值PTHREAD_MUTEX_INITIALIZER

3. 条件变量

我们需要一中方法使得主循环进入睡眠,直到有一线程通知它某件事已就绪。条件变量(condition variable)加上互斥锁可以提供这种功能。互斥锁提供互斥机制,条件变量提供信号机制。
在Pthreads中,条件变量是一个pthread_cond_t类型的变量。条件变量使用下述两个函数:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t * cptr, pthread_mutex_t * mptr);
int pthread_cond_signal(pthread_cond_t * cptr);
//return: success 0, failed Exxx

第二个函数的名字中“signal”一词不是指Unix的SIGxxx信号。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

设有两个共享的变量 x 和 y,通过互斥量 mut 保护,当 x > y 时,条件变量 cond 被触发。

        int x, y;
        pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
        pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

        //等待直到 x > y 的执行流程:
        pthread_mutex_lock(&mut);
        while (x <= y) {
                pthread_cond_wait(&cond, &mut);
        }
        /* 对 x、y 进行操作 */
        pthread_mutex_unlock(&mut);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值