如何实现一个网络库的多线程机制?实际上,并不需要各种繁杂的锁机制。先用一句话来阐述核心思想:以函数执行线程的转移作为实现线程安全的机制。
具体来说,分为如下几点:
- one loop per thread : 其中的每个线程均和一个独立的 EventLoop 关联
- EventLoopThreadPool : 线程池,从池中取出一个新的线程,等价于取出一个新的 EventLoop
- 线程安全性 : 通过
EventLoop::runInLoop()函数,使得函数的执行能够在线程间进行转移 - 线程切换:整个流程中,线程切换仅发生在 Connection 建立和 Connection 删除这两个步骤中,其余的阶段都为单线程执行
- 新建 Connection:Server 所在线程负责新建连接,随后利用
EventLoop::runInLoop()将新连接的控制权转移到线程池中的 IO 线程; - 删除 Connection:由于 Server 中保存有 Connection 的副本,因此需要将删除 Connection 的操作转移到 Server 所在线程,方式还是利用
EventLoop::runInLoop()
- 新建 Connection:Server 所在线程负责新建连接,随后利用
- 事件 Event :
- 连接请求 : 当网络对端发来连接请求时,由 Server 所在的线程负责建立新的连接(TcpConnectionPtr)
- IO 响应 : 特定的 TCP 连接上发生的数据交互则由线程池中的新的线程负责,也称为 IO 线程。因此,TcpConnectionPtr 指示的对端的 socket fd 由 IO 线程中进行注册(也就是生成 struct pollfd 并进行 IO 多路复用)
下面以为例,解释一下一个多线程服务器建立 TCP 连接的整个流程机制。
- Server 线程一直监听自己的端口;
- 某一个时刻,网络对端发来了一个 TCP 连接请求;
- Server 所在线程的 IO 多路复用得到 revent,开始进行响应:
a. 通过对端的 socket fd 和网络地址,建立一个 TcpConnection;
b. Server 自己保存一份 TcpConnection 的副本 TcpConnectionPtr(也就是一个 shared_ptr 共享指针);
c. 从线程池中选出一个新的 EventLoop,并在其中注册该 TcpConnection(意味着将后续该条连接上的 IO 操作转移到别的线程中);
当然,多线程的切换开销还是很大的,因此,Connection 的新建和删除会比单线程环境中慢一些,实测下来也确实如此。
实现过程中的其他知识点总结
- 监听套接字 vs. 已连接套接字:
- sockfd 在经过
listen()调用之后,从一个主动套接字转化为监听套接字(listening socket); - 监听套接字在经过
accpet()调用之后,返回一个已连接套接字(connected socket) - 为什么要区分这两种状态:其实就是为了更高效地实现并发服务器机制。监听套接字在服务端只有一个,但却可以返回多个已连接套接字,即可以分发给多个线程去分别执行各自的 TCP 连接,实现高并发
- sockfd 在经过