· 线程池的同步机制主要有信号量和条件变量,项目中使用的是信号量+互斥量。 信号量就是一个针对共享资源的计数器,由两个原子操作来对其进行控制,分别是PV操作。P操作:将信号量-1,如果不为负数则表示获取到了资源,继续执行,否则阻塞。V操作:将信号量+1,如果信号量大于0则表明当前无线程正在等待获取资源,就继续执行,否则唤醒一个阻塞队列中的线程/进程。通过这种方式实现线程间的同步,也可实现生产者-消费者模型:生产者生产出一份资源后,放到缓冲区,然后执行V操作,消费者尝试获取资源,如果信号量为负数就阻塞等待,直到被唤醒,然后去缓冲区中获取资源。当然在这个过程中,对缓冲区的操作代码属于临界区代码,需要互斥机制,即每次只能有一个线程访问缓冲区。 反映到项目中就是:epoll检测到有可读/可写事件发生时,将其插入任务队列中并执行V操作,将信号量+1,工作线程在一个while(1)循环中,如果尝试获取任务失败,就阻塞等待,直到被唤醒,然后开始执行任务。当然,所有的针对任务队列的操作都需要上锁,因此线程池还需要一个互斥量来实现线程之间互斥。
服务器的并发模型为reactor和同步实现的proactor。
- reactor:本项目中的reactor为单reactor多线程模式,当操作系统检测到有事件发生时,根据事件类型来决定是执行accept操作还是一般的process,accept(即创建新连接)是由主线程完成的,如果是一般的读写事件,则主线程将任务插入任务队列,由线程池中的子线程进行处理。reactor模型在数据读取上是同步的,即它只通知线程有事件发生,但具体的读写操作是由线程自己完成的。
- proactor:proactor是一种异步模型,与reactor不同,proactor的数据读写均是由操作系统完成的。但在本项目中使用的是主线程读写数据的方式来模仿proactor,效率并没有提升。
- 同步异步方面:reactor和proactor最主要的区别就在于数据处理,reactor是同步处理,即内核只负责通知有事件发生,具体的IO操作由线程完成。proactor是异步处理,即数据的IO也由内核完成,完成时会通知工作线程来处理。 2. 单reactor和多reactor:单reactor模式下,只有一个线程来负责监听连接、建立连接,而多reactor可以有一个mainreactor来负责接收新连接,接收到的新连接分发给subreactor再次进行监听。
-
select和poll:select和poll都是一种多路IO复用模型,但不适用于高并发场景。select和poll每次都需要先将文件描述符集合拷贝到内核、内核轮询文件描述符集合,再将有事件发生的文件描述符进行标记,然后将这个集合拷贝回用户态,用户态再去轮询这个集合,才能得知具体是哪个文件描述符发生了事件。也就是说,select和poll每次都要有两次文件描述符集合的拷贝和两次轮询,效率低下。select和poll的区别就是,select监听的文件描述符有上限,这个上限是由Linux中的文件的一个宏定义决定的,要想改变它就只能重新编译内核,而poll没有文件描述符的限制。
- epoll:epoll与其它两个方法不同之处在于,每次调用epoll_wait去监听文件描述符时,不需要将庞大的文件描述符集合拷贝到内核,原因就在于内核会维护一个红黑树,每次要监听新的事件时,就调用epoll_ctl函数将节点插入红黑树,而epoll_wait的返回,不会将整个文件描述符集合返回,而是只返回有事件发生的文件描述符集合
首先使用ALARM函数,每隔一定时间往主线程发送SIGALARM信号。主线程收到信号后,执行清除非活跃连接的操作。首先调用time函数获取当前时间,然后从头逐个比较链表中定时器的时间,如果比当前时间小,就认为是非活跃连接。而定时器上的时间会随着连接有事件发生而得到更新,这个更新并不是直接获取事件发生时的时间,而是在这个基础上加上一个常数。 包括每个定时器的初始化,也是获取当前时间再加上一个固定常数。定时器均位于链表之中,链表采用对时间升序的方式排列定时器,当某个连接有事件发生时,要调整对应的定时器位置,做法就是先更新它的时间,然后将其从链表中取出,再插入链表。由于是双向链表,这个取出的操作并不十分复杂。