欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Muduo 网络库源码分析 之 关键技术点总结

程序员文章站 2022-06-14 10:18:49
...

最近又把muduo网络库仔细研究了一遍,收获良多。本文将对muduo中的设计思想以及关键的技术细节进行总结和分析,当然由于篇幅的原因这里更多的是对关键技术的简略提及,具体细节还需要读者自己去查找学习资料。

muduo/base

  • Date类 
    • 日期类的封装,使用Julian(儒略日)可以方便的计算日期差。具体公式和思想见 儒略日的计算
  • Exception类 
    • 异常类的封装,对外提供what()输出错误信息和stacktrace()函数进行栈追踪,使用时需要throw muduo::Exception("oops");,外部使用catch (const muduo::Exception& ex) 捕获并使用ex.what()/stackTrace()获取详细信息。
  • Atomic类 
    • 原子性操作比锁的开销小,所以我们可以使用gcc提供的自增自减原子操作;最小的执行单元是汇编语句,不是语言语句。
  • CountDownLatch类 
    • 既可用于所有子线程等待主线程发起“起跑”,也可用于主线程等待子线程初始化完毕才开始工作。其中使用RAII技法封装MutextLockGuard,hodler表示锁属于哪一个线程。
  • TimeStamp类 
    • TimeStamp 继承至less_than_comparable<>,使用模板元编程,只需要实现<,可自动实现>,<=,>=。
  • BlockingQueue

    • BlockingQueue和BoundedBlockingQueue分别是*有界队列,本质上是生产者消费者问题,使用信号量或者条件变量解决。ThreadPool的本质也是生产者消费者问题,任务队列中是任务函数(生产者),线程队列就相当于消费者。基本流程如下图:Muduo 网络库源码分析 之 关键技术点总结
  • 异步日志类

    • 对于一般的日志类的实现,(1) 重载<<格式化输出 (2)级别处理 (3)缓冲区。
    • 为了提高效率并防止阻塞业务线程,用一个背景线程负责收集日志消息,并写入日志文件,其他业务线程只管往这个日志线程发送日志消息,这称为异步日志。基本实现仍然是生产者(业务线程)与消费者(日志线程)和缓冲区,但是这样简单的模型会造成写文件操作比较频繁,因为每一次signal我们就需要进行写操作,将消息全部写入文件,效率较低。muduo使用多缓冲机制,即mutliple buffering,使用多个缓冲区,当一块缓冲区写满或者时间超时才signal;如果发生消息堆积,会丢弃只剩2块内存块。另外使用swap公共缓冲区来避免竞争,一次获得所有的消息并写入文件。 
      Muduo 网络库源码分析 之 关键技术点总结
  • __type_traits技法 
    • StringPiece是google的一个高效字符串类,其中使用了__type_traits对不同类型进行了进一步优化。在STL中为了提供通用的操作而又不损失效率,traits就是通过定义一些结构体或者类,并利用模板给类型赋予一些特性,这些特性根据类型的不同而异。在程序设计中可以使用这些traits来判断一个类型的一些特性,实现同一种操作因类型不同而异的效果。可以参照 这篇文章了解。

muduo/net

  • reactor

    • reactor+线程池适合CPU密集型,multiple reactors适合突发I/O型,一般一个千兆网口一个rector;multiple rectors(线程) + thread pool 更能适应突发I/O和密集计算。其中multiple reactors中的main reactor只注册OP_ACCEPT事件,并派发注册I/O事件到sub reactor上监听,多个sub reactor采用round-robin的机制分配。具体实现见EventLoopThread.
  • TcpConnection

    • TcpConnection是对已连接套接字的抽象;Channnel是selectable IO channel,负责注册和响应IO事件,但并不拥有file descriptor, 
      Channel是Acceptor、Connector、EventLoop、TimerQueue、TcpConnection的成员,生命期由后者控制。
  • TimerQueue类 
    • timers_和activeTimers_保存的是相同的数据,timers_是按到期时间排序,activeTimers_按照对象地址排序,并且timerQueue只关注最早 
      的那个定时器,所以当发生可读事件的时候,需要使用getExpired()获取所有的超时事件,因为可能有同一时刻的多个定时器。
  • runInLoop 
    • runInLoop的实现:需要使用eventfd唤醒的两种情况 (1) 调用queueInLoop的线程不是当前IO线程。(2)是当前IO线程并且正在调用pendingFunctor。
  • rvo优化 
    • C++的函数返回vector之类或者自定义类型可以避免产生额外的拷贝构造函数、析构函数开销,返璞归真。
  • shared_from_this() 
    • 获得自身对象的shared_ptr对象,直接强制转换会导致引用计数+1
  • TcpConnection生命周期 
    • TcpConnection对象不能由removeConnection销毁,因为如果此时Channel中的handleEvent()还在执行的话,会造成core dump,我们使用shared_ptr管理引用计数为1,在Channel中维护一个weak_ptr(tie_),将这个shared_ptr对象赋值给tie_,引用技术仍为1;当连接关闭,在handleEvent中将tie_提升,得到一个shared_ptr对象,引用计数就为2。
  • Buffer类 
    • 自己设计的可变缓冲区,成员变量vector<char>readIndexwriteIndex,同时处理粘包问题。Buffer::readFd()中的extraBuffer通过堆上和栈上空间的结合,避免了内存资源的巨额开销。先加入栈空间再扩充和直接扩充的区别就是明确知道多少数据,避免巨大的buffer浪费并且减少read系统调用。

muduo/examples

  • chargen测试服务器的吞吐量;千兆网卡跑满大概100M/s(1000M/8),机械硬盘的读写速度也就差不多也是这样,固态硬盘可以达到500M/s。

  • Filetransfer文件传输的时候,每次发送64k,然后再设置WriteCompleteCallback_再小块发送可以避免应用层缓冲区占用大量内存。

  • chat 多人聊天室,mutex保护vector,多条消息不能并行发送,存在较高的锁竞争。优化一:使用shared_ptr实现 
    copy_on_write,通过建立副本,达到并行发送消息的目的。优化二:消息到达第一个客户端和最后一个客户端之间有延迟,可以放在自己的IO线程中发送。

  • NTP网络时间同步

 Client   server
    |   |
T1  |   |  T2
    |   |  T3
T4  |   |               

RTT = (T4-T1)-(T3-T2)
clock offset = [(T4+T1)-(T2+T3)]/2 (自己加一个系数K,代表客户端与服务端的时间差,公式轻易推出)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

其他关键技术点

  • volatile 
    • 防止编译器对代码进行优化,对于用这个关键字声明的变量,系统总是从它所在的内存读取数据,不会使用寄存器中的备份。在Atomic.h中使用。
  • __thread 
    • __thread修饰的变量是线程局部存储的,仅限于POD类型和类的指针;非POD类型可以使用线程特定数据TSD。
  • 单例模式 
    • 一个类只有一个实例,并提供一个访问他的全局访问点。(1)私有构造函数。(2)类定义中含有该类的静态私有对象。(3)静态公有函数获取 
      静态私有对象。
  • 异步回调的理解 
    • 所谓的异步回调,主线程使用poll/epoll进行事件循环,事件包括各种IO时间和timerfd实现的定时器事件。没有事件发生时阻塞在poll/epoll处,有事件发生时会对activeChannel进行遍历调用其中的回调函数。至于定时器是使用硬件时钟中断实现的,与sleep这种软件阻塞不同。所以我们常说的通过写消息来唤醒线程的含义就是触发一个IO事件,使得poll/epoll解除阻塞,向下得以执行回调函数。
  • CAS无锁操作 
    • CAS原语有三个参数,内存地址,期望值,新值。如果内存地址的值==期望值,表示该值未修改,此时可以修改成新值。否则表示修改失败,返回false。无锁队列的实现可参照CoolShell
    • 注意无锁结构不一定比有锁结构更快,锁指令本身很简单,真正影响性能的是锁争用(Lock Contention)。当contention发生的时候,有锁的情况会陷入内核睡觉,无锁情况会不断spin,其中陷入内核睡觉是有开销的,这个开销当临界区很小的时候所占的比重就很大,这也就是lockfree在这种情况下性能提高很高的原因。lockfree的意义不在于绝对的高性能,他比mutex的有点是使用lockfree可以避免死锁/活锁,优先级翻转等问题,但是ABA problem、memory order等问题使得lockfree比mutex难实现得多。除非瓶颈已经确定,否则最好还是老老实实的使用mutex+condvar吧。
  • 避免重复include多余头文件 
    • 在头文件中使用引用或者指针,而不是使用值的,使用前置声明,而不是直接包含他的头文件。
    • 使用impl手法,简单来说就是类里面包含类的指针,在cpp里面实现。
  • (void)ret 
    • 防止编译警告,变量未使用(限于release版本中 int n = …; assert(n==6))
  • vim行首添加注释 
    • % 1,10s/^/#/g 在1-10行首添加#注释

参考资料

  • muduo源码
  • muduo使用手册
  • 《linux多线程服务端编程》