多路复用
- 使用一个进程(且只有主线程),监控若干个文件描述符的读写操作,并作出对应的响应,这种读写模式称为多路复用
使用场景
- 多用于TCP的服务端,用于监控客户端的连接和收发数据
- 适合并发量大,但任务量短小的情景,例如Web服务器
优点
- 不需要频繁地创建、销毁进程,从而节约了时间资源、内存资源,也避免了进程之间的资源竞争、等待
缺点
- 单个客户端的任务不能太耗时,否则其它客户端会感知到卡顿
select
-
fd_set 数据类型,是文件描述符的集合,使用以下函数进行操作:
-
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> void FD_SET(int fd, fd_set *set); // 功能:添加fd到集合set中 void FD_CLR(int fd, fd_set *set); // 功能:从集合set中删除fd int FD_ISSET(int fd, fd_set *set); // 功能:判断fd是否存在于集合set中 // 返回值:存在返回非零,不存在返回0 void FD_ZERO(fd_set *set); // 功能:清空集合set int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); // 功能:监控多个文件描述符的读、写、异常等操作 // nfds:被监控的文件描述中最大值+1 // readfds:监控读操作的文件描述符集合 // writefds:监控写操作的文件描述符集合 // exceptfds:监控异常操作的文件描述符集合 /* timeout:设置超时时间 NULL 一直阻塞,直到监控的某些文件描述符发生了对应的变化 0秒0微秒 非阻塞 秒数>0 最多等待超时时间,超时后会返回0 */ /* struct timeval { long tv_sec; // seconds long tv_usec; // microseconds }; */ /* 返回值: 1、监控到发生了对应操作的文件描述符数量 2、超时返回0 3、错误返回-1 */ /* 注意: readfds、writefds、exceptfds 这三个集合参数既是输入也是输出,所以当调用select函数时需要往集合中存放被监控的文件描述符,当函数监控成功返回后,会把发生了操作的文件描述符存入到集合中,需要读取 */
-
缺点
- 1、每次调用select时都需要重新向它传递被监控的集合
- 2、调用结束后若想知道具体是哪些文件描述符发生了对应的操作,需要把所有监控的文件描述符都通过FD_ISSET测试一遍
-
优点
- 它是最早的多路复用函数,几乎所有的操作系统都支持,程序的兼容性很高
pselect
-
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask); // 功能:功能与select大致相同,多一些增加的功能,相当于select的增强版,但是本质没区别,缺点一致
-
区别
-
1、超时时间结构类型不同,pselect精度更高11
-
2、pselect的timeout参数,还可以输出剩余时间
-
3、pselect监控时可以通过sigmask参数设置要屏蔽的信号,可以保障pselect监控时不受某些信号的干扰
-
缺点一致
-
poll
-
#include <poll.h> int poll(struct pollfd *fds,nfds_t nfds,int timeout); // 功能:监控一些文件描述符 // fds:struct pollfd结构数组 // nfds:fds结构数组的长度 // timeout:超时时间 /* 返回值: 1、监控到发生了对应操作的文件描述符数量 2、超时返回0 3、错误返回-1 */ // 一个pollfd对应一个被监控的文件描述符 struct pollfd { int fd; // 被监控的文件描述符 short events; // 要监控的事件 short revents; // 实际监控到的事件 }; /* events\revents可选参数: POLLIN 普通优先级的读事件 001 POLLOUT 普通优先级的写事件 010 r:110 & 001 POLLPRI 高优先级的读事件 POLLRDHUP 对方socket关闭事件 POLLERR 通信错误事件,只能在revents中获取 POLLHUP 对方挂起事件 POLLNVAL 非法描述符,只能在revents中获取 */
epoll
-
#include <sys/epoll.h> int epoll_create(int size); // 功能:创建epoll对象,该对象可以用于保存被监控的文件描述符 // size:epoll对象监控文件描述符的数量 // 返回值:返回一个epoll对象的描述符 int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event); // 功能:控制epoll对象,如:添加、删除监控的文件描述符、修改监控事件 // epfd:epoll对象描述符 /* op: EPOLL_CTL_ADD 添加描述符 EPOLL_CTL_DEL 删除描述符 EPOLL_CTL_MOD 修改描述符要监控的事件 fd:要操作的描述符 event: 要监控的事件结构体 struct epoll_event { uint32_t events; // 要监控的事件,参考poll的events 名字前加E epoll_data_t data; // 监控的fd 赋给 data.fd }; */ // 返回值:成功返回0 失败返回-1 int epoll_wait(int epfd,struct epoll_event *events,int maxevents, int timeout); // 功能:监控文件描述符,直接返回产生对应事件的文件描述符 // epfd:epoll对象描述符 // events:输出型参数,用于存储发生了事件的描述符数组 // maxevents:用于声明监控可以返回的最多描述符数量 // timeout:超时时间 /* 返回值: 1、监控到发生了对应操作的文件描述符数量 2、超时返回0 3、错误返回-1 */
-
与select对比的优点
- 1、只需要准备一次要监控的描述符
- 2、会把发生了事件的描述符返回,不需要像select遍历所有的描述符
- 3、编程结构更简洁
epoll的条件触发和边缘触发
- 条件触发:当文件缓冲区中有需要读取的数据时就会触发事件
- epoll默认的模式
- 类似按键盘
- 边缘触发:当数据产生发送动作时就会触发一次事件,如果缓冲区中还有需要读取的数据时不再触发事件
- 1、被监控的描述符的事件增加EPOLLET 边缘触发
- 2、要循环读取数据直到读取完毕
- 3、recv读取时必须以非阻塞MSG_DONTWAIT读取
- 4、当recv的返回值为**-1时读取完毕**,0表示连接断开
- 优点:某些情况下大大地降低事件触发的次数,提高程序运行效率
- 类似鼠标单击
线程管理
基本概念
- 线程是进程的执行路线,它是进程内部的控制序列,是进程的一部分
- 进程是一个资源单位,线程是一个执行单位,线程是进程的实体,是真正负责执行的
- 线程是轻量级的,没有自己独立的代码段、数据段、bss段、堆区、环境变量、命令行参数、文件描述符、信号处理函数、当前工作目录等资源,一个进程中所有线程都是共享以上资源
- 每个线程都有自己独立的栈区、线程ID、错误码、权限掩码、程序计数器、独立的调度优先级等必不可少的资源
- 一个进程中同时可以有多个线程(多条执行路线),称第一个进程中的线程为主线程,并且一个进程至少要拥有一个线程
- 查看线程命令:ps -T -p 进程号或者htop程序命令
- 线程也有不同的状态,系统提供了线程的控制接口函数,例如:创建、销毁、控制等
- 进程中所有的线程都是在同一个地址空间活动,进程中的所有资源对于线程而言都是共享的,虽然线程有属于自己的栈区等资源,但是默认下没有添加保护机制,对于其它线程而言是可见,因此当多个线程协同工作时,需要解决资源竞争的问题(线程同步问题)[加锁]
- 多个线程协同工作时,它们之间不需要间接数据交换,也就不需要类似IPC的通信机制,并且开辟线程系统开销小,切换任务路线更快,因此线程的使用更简单而高效
- 线程之间有优先级的差异
- 从表面上看,当主线程结束时,其它所有线程也随之结束,但并不是因为主线程结束导致的,而是因为主线程执行了进程的main函数的return n语句,导致整个进程结束,而所有线程都属于进程的一部分,所以才会随着进程结束而结束,如果主线程是通过调用pthread_exit()结束的,那么就没执行return n语句的话,其他线程不受影响不会结束
POSIX线程
- 早期的UNIX和Linux系统是没有线程概念的,微软的Windows系统首先提出使用的线程,之后UNIX和Linux逐渐增加了线程技术
- 早期各个厂商提供私有的线程库,但是接口和实现差异较大,不易于移植,IEEE组织于1995年指定了一套统一的线程接口规范标准,遵循该标准的线程称为POSIX线程,简称 pthread
- pthread线程包含一个头文件 pthread.h 和一个共享库 libpthread.so ,编译参数加 -pthread
线程管理
-
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); // 功能:创建一个线程 // thread:输出型参数,用于获取创建出的线程ID // attr:线程属性,一般写NULL以默认属性创建即可 // start_routine:线程的入口函数,相当于线程的main函数 // arg:传递给线程入口函数的参数,不需要传递时写NULL即可 // 注意:入口函数的参数和返回值要确保它们的持久化,不适合传递栈内存,比较适合的是全局变量 // 返回值:成功返回0,失败返回错误码 int pthread_join(pthread_t thread, void **retval); // 功能:等待某个线程结束,并获取该线程结束时的返回值,然后回收释放线程资源 // thread:要等待结束的线程ID // retval:用于存储入口函数返回值的地址的指针 // 返回值:成功返回0,失败返回错误码,线程还没结束会阻塞等待 pthread_t pthread_self(void); // 功能:获取调用者的线程ID int pthread_equal(pthread_t t1, pthread_t t2) // 功能:比较两个线程ID是否相等 // 返回值:相等返回非零,不相等返回0 // 注意:部分系统的线程ID是以结构形式实现的,因此不能直接使用==比较,也不适合用0初始化,使用此函数代码可移植性更高
线程的执行轨迹
同步方式
- 可结合态(默认)
- 在使用默认属性创建一个线程时,线程状态为可结合态,也称为joinable态,处于此状态的线程可以被另一个线程使用pthread_join()等待其结束并释放线程资源
- 如果一个joinable态的线程在结束时,没有被pthread_join进行释放资源,该线程变成"僵尸线程",每个僵尸线程都会消耗一些系统资源,如果有大量的僵尸线程出现时,可能会导致创建线程失败
异步方式
-
分离状态
-
把线程状态设置为detach态,detach态的线程无需经过pthread_join来回收资源,当线程终止后会由系统自动释放资源
- 如果调用pthread_join去等待detach态的线程,则不会等待也不会回收资源,立即返回
- 如果在分离前调用了pthread_join,然后再分离,那么pthread_join会等到该线程结束后才返回,但不会去回收资源
-
注意:为了避免资源泄漏,要么对每个joinable态的线程显式地调用pthread_join来回收资源;要么设置成detach态,让系统来回收资源
-
int pthread_detach(pthread_t thread); // 功能:设置线程thread为detach态
-
注意:两种分离用法
- 1、线程自己调用:pthread_detach(pthread_self());
- 2、创建者\其它线程调用:pthread_detach(tid);
线程的终止
-
1、线程执行完入口函数的最后一行代码
-
2、线程执行入口函数的return 语句
-
3、线程调用了pthread_exit函数
-
void pthread_exit(void *retval); // 功能:结束当前调用的线程并返回retval给pthread_join的调用者
-
-
4、如果进程结束,那么进程中所有的线程都会随之结束
- 线程调用exit函数,会杀死所有线程和进程
-
5、向指定的线程发送取消请求
-
int pthread_cancel(pthread_t thread); // 功能:向指定的线程发送取消请求,默认情况下都会响应请求 int pthread_setcancelstate(int state,int *oldstate); // 功能:设置本线程是否要响应取消请求,并获取之前的状态 /* state: PTHREAD_CANCEL_ENABLE 允许响应 PTHREAD_CANCEL_DISABLE 禁止响应 */
-