并发编程
三种基本的构造并发程序的方法:
- 进程
每个逻辑控制流都是一个进程,由内核来调度和维护,控制流必须使用某种显式的进程间通信(interprocess communication,IPC)机制。 - I/O多路复用
应用程序在一个进程的上下文中显式地调度它自己的逻辑控制流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态。所有的流都共享同一个地址空间(程序是一个单独的进程)。 - 线程
线程是运行在一个单一进程上下文的逻辑流,由内核进行调度。
基于进程的并发编程
共享文件表,但不共享用户地址空间。优点:隔离性。缺点:进程共享状态信息更加困难,进程控制和IPC的开销大。
基于I/O多路复用的并发编程
解决问题:处理多个独立的I/O事件
I/O多路复用(I/O multiplexing)使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
#include <sys/select.h>
int select(int n, fd_set *fdset, NULL, NULL, NULL);
返回已准备好的描述符的非零的个数,准备好集合的基数,若出错-1
fdset:读集合的描述符集合
n:该读集合的基数(n)(任何描述符集合的最大基数)
void FD_CLR(int fd, fd_set *set); // clear bit fd in fdset
int FD_ISSET(int fd, fd_set *set); // is bit fd in fdset on?
void FD_SET(int fd, fd_set *set); // turn on bit fd in fdset
void FD_ZERO(fd_set *set); // clear all bits in fdset
int select(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);
select函数处理类型为fd_set的集合(描述符集合)。将描述符集合看成一个大小为n的位向量
bn-1,……,b1,b0
每个位bk对应于描述符k。当且仅当bk=1,描述符k才表明是一个描述符集合的一个元素。
只允许对描述符集合做三件事:
- 分配它们
- 将一个此种类型的变量赋值给另一个变量
- 用FD_ZERO、FD_SET、FD_CLR和FD_ISSET宏来修改和检查它们
select函数会一直阻塞,直到读集合中至少有一个描述符准备好可以读。当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k就表示准备好可以读了。必须每次调用select时都更新读集合,select修改参数fdset指向的fd_set,指明读集合的一个子集(准备好集合,由读集合中准备好可以读了的描述符组成)。
I/O多路复用可以用做并发事件驱动服务器,select处理两种事件:①来自一个新客户端的连接请求到达;②一个已存在的客户端的已连接描述符准备好可以读了。
优点:
- 比基于进程的设计有个更多对程序行为的控制(给特殊客户端提供特殊服务)
- 每个逻辑流都能访问该进程的全部地址空间(运行在单一进程),容易调试
- 事件驱动设计比基于进程的设计高校(不需要进程上下文切换来调度新的流)
缺点:
- 编码复杂(并发粒度越小越复杂),粒度指每个逻辑流每个时间片需要执行的指令数量
- 不能充分利用多核处理器
基于线程的并发编程
线程就是运行在进程上下文中的逻辑流,线程由内核自动调度,每个线程都有自己的线程上下文,包括唯一的整数线程ID(TID),栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程里的线程共享该进程的整个虚拟地址空间(代码、数据、堆、共享库、打开的文件)。
线程和进程区别:
- 线程切换比进程快(线程的上下文小)
- 组织方式:进程按照父子层次组织;和一个进程相关的线程组成一个对等(线程)池,主线程和其它线程的区别仅在于主线程总是进程中第一个运行的线程,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止,每个对等线程都能读写相同的共享数据。
#include <pthread.h>
typedef void *(func)(void *);
int pthread_create(pthread_t *tid,
pthread_attr_t *attr, // 新创建线程的默认属性
void *(*start_routine)(void *),
void *arg);
成功:0;错误:非0
pthread_t pthread_self(void);
线程终止:
-
隐式:当顶层的线程例程返回时,线程会隐式地终止
-
显式:调用
pthread_exit
函数,如果主线程调用pthread_exit
会等待所有其它对等线程终止,然后再终止主线程和整个进程#include <pthread.h> void pthread_exit(void *thread_return);
-
某个对等线程调用
exit
函数,该函数终止进程以及所有与进程相关的线程 -
另一个对等线程通过以当前线程TID作为参数调用
pthread_cancel
函数来终止当前线程int pthread_cancel(pthread_t tid); 成功:0;出错:非0
回收已终止线程的资源:阻塞直到指定的tid终止,将线程例程返回的通用的(void *)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有内存资源
int pthread_join(pthread_t tid, void **thread_return);
成功:0;出错:非0
线程分离:任意时刻,线程是可结合的(默认)或者分离的。
- 一个可结合的线程能够被其它线程收回或杀死,在被其它线程回收之前,它的内存资源(如栈)是不释放的
- 一个分离的线程是不能被其它线程回收或杀死的,它的内存资源在他终止时由系统自动释放
int pthread_detach(pthread_t tid);
成功:0;出错:非0
初始化线程:pthread_once
函数初始化与线程例程相关的状态
// 不透明的对象,用户不可访问其内部结构,确保多线程程序中某个初始化例程只执行一次
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
多线程程序中的共享变量
寄存器是从不共享的,虚拟内存总是共享的。
线程栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立地访问的。不同的线程栈是不对其它线程设防的,如果一个线程以某种方式得到一个指向其它线程栈的指针,那么就可以修改这个栈的任何部分(如:对等线程直接通过全局变量间接引用主线程的栈的内容)。
多线程的C程序中变量根据它们的存储类型被映射到虚拟内存:
- 共享
- 全局变量:在运行时,虚拟内存的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用
- 本地静态变量:虚拟内存的读/写区域只包含每个全局变量的一个实例
- 私有
- 本地自动变量:在运行时,每个线程的栈都包含它自己的所有本地变量的实例
用信号量同步线程
信号量(semaphore)是具有非负整数值的全局变量,只能由两种特殊的操作处理:
- P(s):如果s非零,那么P将s减1,并且立即返回;如果s为零,那么就挂起这个线程,直到s变为非零
- V(s):V操作将s加1,如果有任何线程阻塞在P操作等待s变成非零,那么V操作会重启这些线程中的一个
#include <semaphore.h>
int sem_init(sem_t *sem, 0, unsigned int value);
int sem_wait(sem_t *s); // P
int sem_post(sem_t *s); // V