【1】线程同步机制
1. 互斥
1. 概念
临界资源:一次仅允许一个进程所使用的资源
临界区:指的是一个访问共享资源的程序片段
同步:按约定的顺序共同完成一件事
互斥:多个线程在访问临界资源时,同一时间只能一个线程访问
互斥锁:通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
2. 函数
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
功能:初始化互斥锁
参数:mutex:互斥锁
attr: 互斥锁属性 // NULL表示缺省属性
返回值:成功 0
失败 -1
int pthread_mutex_lock(pthread_mutex_t *mutex)
功能:申请互斥锁
参数:mutex:互斥锁
返回值:成功 0
失败 -1
注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回
int pthread_mutex_unlock(pthread_mutex_t *mutex)
功能:释放互斥锁
参数:mutex:互斥锁
返回值:成功 0
失败 -1
int pthread_mutex_destroy(pthread_mutex_t *mutex)
功能:销毁互斥锁
参数:mutex:互斥锁
3. 练习
int a[10]={0,1,2,3,4,5,6,7,8,9};
两个线程:一个线程打印数据,另一个线程倒置
4. 死锁
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
死锁产生的四个必要条件
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
Linux下的锁分类
1. 互斥锁
就是我们在线程中使用的互斥锁,用于保证临界资源操作的唯一性,保证原子操作
操作状态:上锁、解锁
函数接口:pthread_mutex_init/pthread_mutex_unlock
补充:原子操作:即不可再细分的操作,最小的执行单位,在操作完之前都不会被任何事件中断
2. 读写锁
读写锁允许更高的并行性,也叫共享互斥锁,一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁,即允许多个线程读但只允许一个线程写。
操作状态:读模式下加锁状态、写模式加锁状态、不加锁状态
3. 自旋锁
主要在内核中使用的锁机制,为了保护一段短小的临界区操作代码;互斥锁是当一个线程申请到锁,其他线程再申请时会阻塞,但是自旋锁会一直轮询判断锁是否可以获取
函数接口: spin_lock_init/spin_lock/spin_unlock
4. 递归锁
在同一个线程中,如果想要多次获得一个锁,只能使用递归锁,递归锁是不被提倡的,用到递归锁说明这个代码设计是有问题的,有些逻辑特别混乱或涉及三方开发对外提供服务不得已时,再用递归锁
2. 条件变量
1. 步骤
pthread_cond_init:初始化
pthread_cond_wait:阻塞等待条件产生,没有条件产生时阻塞,同时解锁,当条件产生时结束阻塞,再次上锁
pthread_mutex_lock(); //上锁
pthread_cond_wait(cond, lock); //如果没有条件产生时,解锁,当等待到条件产生时,上锁
pthread_cond_signal:产生条件,不阻塞
pthread_cond_wait先执行,pthread_cond_signal再产生条件
2. 函数
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化条件变量
参数:cond:是一个指向结构pthread_cond_t的指针
restrict attr:是一个指向结构pthread_condattr_t的指针,一般设为NULL
返回值:成功:0 失败:非0
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
功能:等待信号的产生
参数:restrict cond:要等待的条件
restrict mutex:对应的锁
返回值:成功:0,失败:不为0
注:当没有条件产生时函数会阻塞,同时会将锁解开;如果等待到条件产生,函数会结束阻塞同时进行上锁。
int pthread_cond_signal(pthread_cond_t *cond);
功能:给条件变量发送信号
参数:cond:条件变量值
返回值:成功:0,失败:非0
注:必须等待pthread_cond_wait函数先执行,再产生条件才可以
pthread_cond_boardcast();与pthread_cond_signal();区别:
pthread_cond_boardcast()可以唤醒所有正在等待该条件变量的线程
pthread_cond_signal()只能唤醒一个线程
int pthread_cond_destroy(pthread_cond_t *cond);
功能:将条件变量销毁
参数:cond:条件变量值
返回值:成功:0, 失败:非0
【2】linux IO模型
阻塞IO 非阻塞IO 信号驱动IO(了解) IO多路复用
场景假设一
假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?
1. 妈妈在房间呆着,和孩子一起睡:妈妈不累,但是不能干其他的事情
2. 时不时的看一下孩子,其他时间可以干一点其他的事情:累,但是可以干其他的事情
3. 妈妈在客厅干活,听孩子是否哭了:二者互不耽误
1. 阻塞式IO:最常见、效率低、不浪费cpu
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
学习的读写函数在调用过程中会发生阻塞相关函数如下:
•读操作中的read
读阻塞--》需要读缓冲区中有数据可读,读阻塞解除
•写操作中的write
写阻塞--》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。
2. 非阻塞式IO:轮询、耗费CPU、可以同时处理多路IO
•当我们设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。
1. 通过函数自带参数设置
2. 通过设置文件描述符的属性设置非阻塞
int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置文件描述符属性
参数:
fd:文件描述符
cmd:设置方式 - 功能选择
F_GETFL 获取文件描述符的状态信息 第三个参数化忽略
F_SETFL 设置文件描述符的状态信息 通过第三个参数设置
O_NONBLOCK 非阻塞
O_ASYNC 异步
O_SYNC 同步
arg:设置的值 in
返回值:
特殊选择返回特殊值 - F_GETFL 返回的状态值(int)
其他:成功0 失败-1,更新errno
使用:0为例
0-原本:阻塞、读权限 修改或添加非阻塞
int flags=fcntl(0,F_GETFL);//1.获取文件描述符原有的属性信息
flags = flags | O_NONBLOCK;//2.修改添加权限
fcntl(0,F_SETFL,flags); //3.将修改好的权限设置回去
3. 信号驱动IO:异步通知方式,底层驱动的支持
异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。
1. 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
2. 应用程序收到信号后做异步处理即可。
3. 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
//1.设置将文件描述符和进程号提交给内核驱动
//一旦fd有事件响应, 则内核驱动会给进程号发送一个SIGIO的信号
fcntl(fd,F_SETOWN,getpid());
//2.设置异步通知
int flags;
flags = fcntl(fd, F_GETFL); //获取原属性
flags |= O_ASYNC; //给flags设置异步 O_ASUNC 通知
fcntl(fd, F_SETFL, flags); //修改的属性设置进去,此时fd属于异步
//3.signal捕捉SIGIO信号 --- SIGIO:内核通知会进程有新的IO信号可用
//一旦内核给进程发送sigio信号,则执行handler
signal(SIGIO,handler);
阻塞IO(Blocking IO) |
非阻塞IO(Non-blocking IO) |
信号驱动IO(Signal-driven IO) | |
同步性 |
同步 |
非同步 |
异步 |
描述 |
调用IO操作的线程会被阻塞,直到操作完成 |
调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作 |
当IO操作可以进行时,内核会发送信号通知进程 |
特点 |
最常见、效率低、不耗费cpu, |
轮询、耗费CPU,可以处理多路IO,效率高 |
异步通知方式,需要底层驱动的支持 |
适应场景 |
小规模IO操作,对性能要求不高 |
高并发网络服务器,减少线程阻塞时间 |
实时性要求高的应用,避免轮询开销 |
场景假设二
假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?
阻塞IO?在一个房间
非阻塞IO?不停的每个房间查看
信号驱动IO?不行,因为只有一个信号,不知道那个孩子醒
1. 不停的每个房间看:超级无敌累,但是也可以干点其他的事情
2. 妈妈在客厅睡觉,孩子醒了之后自己找妈妈:既可以休息,也可以及时获取状态
4. IO多路复用:select poll epoll
● 应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;
● 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
● 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
● 比较好的方法是使用I/O多路复用技术。其基本思想是:
○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。
○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
○ 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
1. select
1. 特点
1. 一个进程最多只能监听1024个文件描述符
2. select被唤醒之后要重新轮询,效率相对低
3. select每次都会清空未发生响应的文件描述符,每次拷贝都需要从用户空间到内核空间,效率低,开销大
2. 编程步骤
1. 先构造一张关于文件描述符的表
2. 清空表 FD_ZERO
3. 将关心的文件描述符添加到表中 FD_SET
4. 调用select函数
5. 判断是哪一个或者式哪些文件描述符产生了事件 FD_ISSET
6. 做对应的逻辑处理
3. 函数接口
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:
实现IO的多路复用
参数:
nfds:关注的最大的文件描述符+1
readfds:关注的读表
writefds:关注的写表
exceptfds:关注的异常表
timeout:超时的设置
NULL:一直阻塞,直到有文件描述符就绪或出错
时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 = 10^-6秒 */
};
返回值:
准备好的文件描述符的个数
-1 :失败:
0:超时检测时间到并且没有文件描述符准备好
注意:
select返回后,关注列表中只存在准备好的文件描述符
操作表:
void FD_CLR(int fd, fd_set *set); //清除集合中的fd位
void FD_SET(int fd, fd_set *set);//将fd放入关注列表中
int FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中 是--》1 不是---》0
void FD_ZERO(fd_set *set);//清空关注列表
4. 练习
输入鼠标的时候,响应鼠标事件,输入键盘的时候,响应键盘事件 (两路IO)
5. 超时检测
概念
什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理
比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;
必要性
1. 避免进程在没有数据时无限制的阻塞;
2. 规定时间未完成语句应有的功能,则会执行相关功能
2. poll
特点
1. 优化文件描述符的限制,文件描述符的限制取决于系统
2. poll被唤醒之后要重新轮询一遍,效率相对低
3. poll不需要重新构造表,采用结构体数组,每次都需要从用户空间拷贝到内核空间
3.epoll
特点
1. 监听的最大的文件描述符没有个数限制
2. 异步IO,epoll当有事件产生被唤醒之后,文件描述符主动调用callback函数(回调函数)直接拿到唤醒的文件描述符,不需要轮询,效率高
3. epoll不需要重新构造文件描述符表,只需要从用户空间拷贝到内核空间一次。
总结
select |
poll |
epoll | |
监听个数 |
一个进程最多监听1024个文件描述符 |
由程序员自己决定 |
百万级 |
方式 |
每次都会被唤醒,都需要重新轮询 |
每次都会被唤醒,都需要重新轮询 |
红黑树内callback自动回调,不需要轮询 |
效率 |
文件描述符数目越多,轮询越多,效率越低 |
文件描述符数目越多,轮询越多,效率越低 |
不轮询,效率高 |
原理 |
每次使用select后,都会清空表 每次调用select,都需要拷贝用户空间的表到内核空间 内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环 |
不会清空结构体数组 每次调用poll,都需要拷贝用户空间的结构体到内核空间 内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环 |
不会清空表 epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时) 通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝 |
特点 |
一个进程最多能监听1024个文件描述符 select每次被唤醒,都要重新轮询表,效率低 select每次都清空未发生相应的文件描述符,每次都要拷贝用户空间的表到内核空间 |
优化文件描述符的个数限制 poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu) poll不需要构造文件描述符表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间 |
监听的文件描述符没有个数限制(取决于自己的系统) 异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高 epoll不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。 |
结构 |
数组 |
链表 |
哈希表(红黑树+就绪链表) |
开发复杂度 |
低 |
低 |
中 |