如何实现一个网络库的多线程机制?实际上,并不需要各种繁杂的锁机制。先用一句话来阐述核心思想:以函数执行线程的转移作为实现线程安全的机制。
具体来说,分为如下几点:

  • one loop per thread : 其中的每个线程均和一个独立的 EventLoop 关联
  • EventLoopThreadPool : 线程池,从池中取出一个新的线程,等价于取出一个新的 EventLoop
  • 线程安全性 : 通过EventLoop::runInLoop()函数,使得函数的执行能够在线程间进行转移
  • 线程切换:整个流程中,线程切换仅发生在 Connection 建立和 Connection 删除这两个步骤中,其余的阶段都为单线程执行
    • 新建 Connection:Server 所在线程负责新建连接,随后利用EventLoop::runInLoop()将新连接的控制权转移到线程池中的 IO 线程;
    • 删除 Connection:由于 Server 中保存有 Connection 的副本,因此需要将删除 Connection 的操作转移到 Server 所在线程,方式还是利用EventLoop::runInLoop()
  • 事件 Event :
    • 连接请求 : 当网络对端发来连接请求时,由 Server 所在的线程负责建立新的连接(TcpConnectionPtr)
    • IO 响应 : 特定的 TCP 连接上发生的数据交互则由线程池中的新的线程负责,也称为 IO 线程。因此,TcpConnectionPtr 指示的对端的 socket fd 由 IO 线程中进行注册(也就是生成 struct pollfd 并进行 IO 多路复用)

下面以图 1为例,解释一下一个多线程服务器建立 TCP 连接的整个流程机制。

  1. Server 线程一直监听自己的端口;
  2. 某一个时刻,网络对端发来了一个 TCP 连接请求;
  3. 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 连接,实现高并发