本章解读C++开源项目 muduo 代码,与配套书籍《Linux多线程服务端编程》,均来自作者陈硕,是业内比较有名的大神。
1、开源地址:https://github.com/chenshuo/muduo
2、muduo 是C++多线程服务器框架,支持高并发的高性能服务器。
3、muduo 基础源码与2010年左右完成,最近几年已无重大更新,上一次提交是2022年。
muduo 源码框架
├── base
│ ├── AsyncLogging.h //异步日志。
│ ├── Atomic.h //对原子整型数据的封装(C++11标准已经加入原子数据)。
│ ├── BlockingQueue.h //线程安全的阻塞任务队列,成员:queue_,mutex_,接口:put、take,当队列为空时,条件变量wait等待。
│ ├── BoundedBlockingQueue.h //成员:circular_buffer queue_,与 BlockingQueue 类似,是大小有限制的环形队列。
│ ├── Condition.h //成员:pthread_cond_t pcond_、MutexLock,对条件变量的封装。
│ ├── copyable.h //可复制的基类。
│ ├── CountDownLatch.h //条件变量实现的计数器,计数变为0时唤醒wait。
│ ├── CurrentThread.h //当前线程的一些信息,线程名、线程号、线程调用栈(backtrace)等。
│ ├── Date.h //对日期的封装(年/月/日)。
│ ├── Exception.h //对异常 std::exception 的封装,特点是可以打印调用栈(CurrentThread::stackTrace)。
│ ├── FileUtil.h //文件读写,class ReadSmallFile,class AppendFile 。
│ ├── GzipFile.h //zlib压缩解压缩的封装。
│ ├── LogFile.h //写日志的日志文件。
│ ├── Logging.h //提供 LOG_TRACE、LOG_INFO等记录日志接口。
│ ├── LogStream.h //日志格式化,重载operator<<等。
│ ├── Mutex.h //封装了 class MutexLock,class MutexLockGuard 类,框架内基本都是使用这两个互斥锁类。
│ ├── noncopyable.h //不允许拷贝基类,delete 了复制构造和拷贝构造。
│ ├── ProcessInfo.h //进程信息查询接口,进程号,用户名,主机IP,线程数量,线程状态,可执行文件路径等。
│ ├── Singleton.h //pthread_once 实现的线程安全的单例类。
│ ├── StringPiece.h //字符串操作封装。
│ ├── Thread.h //线程封装,pthread_create 创建线程,bind 指定线程处理函数。
│ ├── ThreadLocal.h //pthread_key_t 线程变量封装,线程私有的全局变量。
│ ├── ThreadLocalSingleton.h //成员:static thread T* t_value,线程单例模式,每个线程都有一个该单例的实例。
│ ├── ThreadPool.h //线程池,创建指定数量的线程,从一个任务队列阻塞地取任务执行,线程安全。
│ ├── Timestamp.h //时间戳,没有使用线程做时间同步,每次调用 gettimeofday 获取当前时间。
│ ├── TimeZone.h //时区的封装。
│ ├── Types.h //包含了几个常用头文件。
│ └── WeakCallback.h //弱回调,传递智能指针弱引用 std::weak_ptr ,回调时如果 lock() 强引用成功则执行回调,否则不执行回调。
└── net
├── Acceptor.h //成员:Socket 、acceptChannel、NewConnectionCallback,接口:listen、handleRead,接受接入的TCP连接。
├── boilerplate.h
├── Buffer.h //成员:std::vector buffer_、readerIndex_、writerIndex_,对读写内存的封装。
├── Callbacks.h //各种回调函数 std::function 模板定义。
├── Channel.h //成员:readCallback_、writeCallback_、closeCallback_、errorCallback_,fd的事件和回调管理。
├── Connector.h //成员:serverAddr_,channel_,NewConnectionCallback,States,tcp连接器,tcp非阻塞connect操作。
├── Endian.h //字节序转换接口,例:hostToNetwork32。
├── EventLoop.h //成员:poller_,timerQueue_,std::vector pendingFunctors_,接口:线程主循环 loop(处理监听的网络fd、定时器fd,runInLoop 加入的异步任务队列),为外部调用提供接口。
├── EventLoopThread.h //成员:EventLoop* loop_,Thread thread_,创建线程,并把 EventLoop 主循环放在线程里执行。
├── EventLoopThreadPool.h //成员:threads_,vector
├── http
│ ├── HttpContext.h //成员:HttpRequestParseState state_、HttpRequest request_,接口:parseRequest,解析http请求。
│ ├── HttpRequest.h //成员:Method、Version、path_、headers_,保存http请求的状态信息,提供设置和查询接口。
│ ├── HttpResponse.h //成员:std::map
│ └── HttpServer.h //成员:TcpServer server_,httpCallback_,接口:onConnection、onMessage、onRequest,一个提供简单功能的http服务端。
├── InetAddress.h //成员:struct sockaddr_in addr_,struct sockaddr_in6 addr6_,对网络地址的封装,支持ipv4和ipv6。
├── inspect
│ ├── Inspector.h //成员:HttpServer server_,ProcessInspector,PerformanceInspector,SystemInspector,
std::map
│ ├── PerformanceInspector.h //基于gperftools的性能监测接口。
│ ├── ProcessInspector.h //进程数据监测接口,进程号,线程,打开的文件描述符等。
│ └── SystemInspector.h //操作系统信息监测接口,系统版本,cpu信息,cpu状态等。
├── poller
│ ├── EPollPoller.h //epoll_wait 监听事件,把 Channel 传入epoll参数,由上层 EventLoop 处理 Channel 的事件。
│ └── PollPoller.h //与 EPollPoller 类似,使用的是 ::poll。
├── Poller.h //抽象类,接口:poll,updateChannel,成员:ChannelMap channels_,EventLoop* ownerLoop_。
├── Socket.h //成员:const int sockfd_,接口:listen、bindAddress、accept,和一些参数设置,是对 socket fd 常用接口的封装。
├── SocketsOps.h//socket 系统调用接口的封装,全局接口。
├── TcpClient.h //成员:ConnectorPtr connector_,TcpConnectionPtr connection_,tcp客户端的封装,接口:connect,disconnect。
├── TcpConnection.h//成员:socket_、channel_、InetAddress localAddr_、peerAddr_、Buffer inputBuffer_、outputBuffer_,TCP连接操作,包括数据的发送和接收。
├── TcpServer.h //成员:std::unique_ptr acceptor_,std::shared_ptr threadPool_,ConnectionMap connections_,tcp服务端,创建 EventLoopThreadPool 线程池,启动监听,把接入的tcp客户端平均分配到线程池。
├── Timer.h //成员:TimerCallback callback_、Timestamp expiration_、double interval_,定义定时器,管理回调、定时触发等。
├── TimerId.h //成员:Timer* timer_、int64_t sequence_,对 Timer 的封装。
├── TimerQueue.h//成员:EventLoop* loop_、const int timerfd_、TimerList timers_、Channel timerfdChannel_,接口:addTimer、cancel,在 EventLoop 线程管理定时器。
└── ZlibStream.h//ZlibInputStream,ZlibOutputStream,输入为zlib压缩数据,输出未压缩数据。
前言
本书写于2012年,2013年出版,主要讲述采用C++在x86-64linux上编写多线程TCP网络服务程序的主流技术,也是对作者多年生成环境开发的经验总结。
本书源码参见开源项目 muduo,其是一个基于非阻塞IO和事件驱动的C++网络库,并未涉及UDP和文件存储,和书是同一个作者。
本书所使用的C++版本是2005年之后的C++语言和库,并非C++11及以后的C++版本,所使用的 shared_ptr/bind 等来自 boost 库。
muduo 也多年未做重大更新,是一个老且成熟的库,对于喜欢新技术的朋友来说不太友好。
阅读过程中,不感兴趣的部分(对开发帮助不大)只是大致浏览一遍,没有做笔记。
第1部分 C++多线程系统编程
C++要求程序员自己管理对象的生命周期,在多线程环境下较为困难。
1、对象构造的线程安全:不要在构造函数中注册任何回调;不要在构造函数中泄露this指针。
2、对象析构的线程安全:
(a)线程锁mutex不能在析构时保证多线程安全,锁本身也会在析构时被销毁,在析构中加锁本身就是错误的。
(b)如果同时读写一个class的两个对象,存在死锁可能,比如swap(a,b)和swap(b,a)。
©如果要锁住相同类的多个对象,可以比较mutex对象地址,始终先加锁地址较小的mutex。
3、万能的指针线程安全解决方案:智能指针 shared_ptr。
4、shared_ptr 的线程安全
(a)shared_ptr 是一个类模板,用来管理对象的生命周期。
(b)智能指针的引用计数是线程安全的,但 shared_ptr 对象的多线程读写不安全,需要加锁。
©弱回调:如果对象还存在,则执行它的接口,否则忽略,weak_ptr 弱引用可以实现。
智能指针的知识和应用详见:。
一、线程同步原则(作者的建议):
1、尽量避免共享对象,优先共享不可改变对象,共享可修改对象时,必须用同步措施保护对象。
2、使用高级的并发编程构建:TaskQueue、Producer-Consumer Queue、CountDownLatch 等。
3、必须用线程锁时,只用非递归的互斥锁和条件变量,慎用读写锁,不要用信号量。
4、除了使用 atomic 整数外,不自己编写 lock-free 代码。
二、互斥锁(mutex)使用原则
1、用RAII理念封装 mutex 的创建、销毁、加锁、解锁。
2、只用非递归的 metux。
3、不手动调用 lock 和 unlock,交给 Guard 对象管理。
4、每次构造 Guard 对象时,思考已经持有的锁,防止因加锁顺序不同导致的死锁。
5、不使用跨进程的 mutex,加锁和解锁在同一个线程。
三、不使用递归锁
递归锁不容易死锁,方便使用,但出现bug很难排查,比如两个接口A和B都获取锁,A和B会修改同一个容器V,如果A在遍历V时调用了B,B修改了V,则会出现意想不到的问题。
使用非递归锁,出现问题会今早暴露,且方便排查。
(不是不能使用递归锁,只要逻辑够严谨即可,在 ZLToolKit 中几乎都是使用递归锁 std::recursive_mutex)
四、死锁
1、在获取一个锁后没有解锁,又去获取它则会造成死锁,RAII模式下还未离开 Guard 作用域又调用了其他接口获取锁。
2、在两个不同的线程分别连续获取两把锁,如果获取两个锁的顺序不同,则可能会造成死锁。
五、不使用读写锁和信号量
1、读写锁:对性能提升有限;容易在读锁中修改了数据;写锁会造成读锁死锁;使用复杂容易出错;开销比普通锁大。
2、信号量:大意是,条件变量配合互斥锁完全可以替换信号量,且不易用错。
六、封装 MutexLock、MutexLockGuard、Condition
详见 muduo/base
七、sleep 不是同步原语
1、手动 sleep 只能出现在测试程序,不应该出现在生产环境,如果依赖 sleep 做轮询是低效的,业余的做法。
2、正确的线程等待:select/poll/epoll_wait 上等待;条件变量上等待;互斥锁上等待等。
八、普通 mutex 配合 shared_ptr 替换读写锁
1、巧妙的设计,容器定义为智能指针 pmap ,假如是单线程写、多线程读模型。
2、读时获取锁并返回 pmap,引用计数加1,在遍历 pmap 时无锁,读完返回后减1。
3、写时全程持有锁,如果发现 pmap 的引用计数大于1,说明有线程在读,此时不直接改 pmap ,而是拷贝一个新的 pmap 并交换新旧 pmap。
4、此时成员变量持有的是新拷贝的 pmap,修改新的 pmap。读线程在读的一直是那个旧的 pmap。
5、旧的 pmap 怎么释放? 读写执行完后离开作用域,相应的智能指针引用计数都会减1,自然就释放了。
思路:使用 shared_ptr 管理容器,读时use_count加1,写时如果use_count大于1说明有线程在读,复制一份容器,在新复制的上面修改,交换新旧容器。
一、多线程服务器模型
1、多线程的价值是为了更好地发挥多核处理器的效能。
2、event loop 线程:reactor模型,基于epoll,网络IO、定时器、异步执行等。
3、线程池:对于没有IO而只有计算任务的线程,使用 event loop 有点浪费,补充方案就是共享任务队列的多个线程组成的线程池(muduo/base/ThreadPool.h)。
4、多线程服务器推荐模型:非阻塞IO + 每个线程一个事件循环+线程池。
二、进程间通信只用TCP
作者罗列了一堆使用TCP的好处,比如可以跨机器使用,可以使用tcpdump抓包分析,可以查看tcp端口占用,可以使用长连接,双向通信等等。
三、单线程服务器
1、程序可能会 fork 子进程,多线程也可以fork,但会让程序变得异常复杂(fork子进程只有一个线程,其他线程都消失了)。
2、限制程序的CPU占用率,其只能占满一个core,不影响其他core给别的进程使用。
3、可以让单个core的性能达到最佳。
四、多线程服务器
1、IO线程:event loop 线程,基于epoll_wait系统调用,也可以处理定时器和异步操作。
2、计算线程:主循环是 blocking queue,阻塞在条件变量上,等待处理任务队列。
3、其他线程:比如日志,时钟等。
一、多线程编程特点
1、不能依赖任何一个线程的执行速度或通过sleep的方式控制线程执行的先后顺序,跨线程必须有同步措施。
2、muduo 对多线程的封装:muduo::Thread(线程的创建和等待结束),muduo::MutexLock(mutex的创建、销毁、加锁、解锁),muduo::Condtion(条件变量的创建、销毁、等待、通知、广播)。
3、C++标准库大多数泛型算法是线程安全的,因为这些都是无状态纯函数。
4、C++的iostream不是线程安全的,因为可以拆分为多个<<语句;printf是线程安全的,但这等于是调用了全局锁,任何时刻只能一个线程调用printf。
5、一个程序创建线程的数量应小于等于 core 数量,不应该创建过多的线程,否则会增加内核调度的负担,降低整体性能。
二、线程的创建和销毁的守则
1、程序库不应该在未告知的情况下创建自己的线程,程序的每个线程必须是程序员知道并掌控的。
2、用相同的方式创建线程,比如 muduo::Thread,方便管理。
3、main()函数之前不应该启动线程,C++会在main之前完成全局对象的构造,无需考虑并发和线程安全,在此过程中启动线程是不安全的。
4、程序在初始化阶段就创建全部工作线程,在程序运行期间不再创建或销毁线程,因为线程销毁通常是不安全的。
5、线程正常退出只有一种方式:从线程主函数返回,其他pthead_exit、pthread_cancel、抛出异常等都是错误的退出方式。
三、多线程IO
1、socket是双向IO,读和写可以分到两个线程进行,但不能多个线程进行读或写。
2、磁盘IO:每个磁盘配一个线程,因为每个磁盘都有一个操作队列;或者简单点:一个文件只有一个进程的一个线程来读写。
四、多线程中的 fork 和 signal
1、fork 一般不能在多线程调用,因为linux的fork只克隆当前线程,不克隆其他线程。
2、多线程时代 signal 语义复杂,不建议使用。
文章介绍了日志的功能需求和性能需求,以及muduo日志的实现思路(这里不应花费过多时间,每个框架都有自己的日志封装,功能大同小异)。
这三章内容是在介绍 muduo 开源库,我的建议是直接看 muduo 源码,有什么疑问再来看书。
一、分布式系统面临的问题
1、 RPC(Remote Procedure Call Protocol)调度超时,无法区分是网络故障,还是对方机器崩溃,或者是上行问题还是下行问题,硬件故障还是软件故障等。
2、分布式系统的负荷均衡问题。
3、分布式系统软件的可靠性,作者罗列了大量测试数据和计算公式,可以做材料素材。
4、软件遇到异常错误重启是在所难免的,服务端程序要保证可以随时重启,操作系统能回收资源,不要使用操作系统都无法回收的资源:如共享内存,全局锁和全局信号量,父子进程共享fd通信等。
5、优雅地重启:停止服务进程心跳,对于短链接,关闭监听端口,不会有新请求到达,对于长连接,客户端主动failover到备用地址或其他服务器。
6、还有一种迁移:先启动新版本的服务进程,然后让旧版本服务进程停止接受新请求,所有新请求导向新进程,一段时间后旧版本进程没有活动请求后,直接kill。
二、分布式系统心跳设计
1、TCP连接心跳的必要性:操作系统崩溃,不会发出FIN;硬件故障导致机器重启,也不会发出FIN;FIN可能丢包。
2、TCP keepalive 不能替代心跳:keepalive 由操作系统负责探测,即便进程死锁或阻塞,操作系统也会正常收发 TCP keepalive。
3、应该使用工作线程收发心跳,防止工作线程死锁或阻塞还在继续收发心跳。
4、与业务消息用同一个连接收发心跳,不要使用单独的心跳连接,避免TCP做业务连接,UDP做心跳连接()。
三、分布式系统部署、监控、进程管理的几重境界
程序员一般不关系这些,感兴趣的可以看看。
1、全手工操作
2、使用零散的自动化脚本和第三方组件
3、自制机群管理系统,集中化配置
4、机群管理与 naming service 结合
C++编译知识,编译优化,想深入了解编译的可以看看。
从muduo源码可以看出,作者不喜欢使用继承与派生、虚函数与多态,原因:
1、引入基类和派生类,带来了灵活性,但代码易读性变差,在几十个类的继承体内绕来绕去确实很费脑筋。
2、继承与派生增加了类之间的耦合性,涉及改动时牵一发动全身(一个事物归于哪个类有时是模糊的,随着需求的改变可能会归于不同的类)。
3、虚函数会破坏二进制文件的兼容性(动态库开发的思路,可以看看作者的观点)。
4、使用 std::function 和 std::bind 替代虚函数回调。
作者总结了几个C++开发的知识点,我没仔细看,工作中遇到了可以看看。
1、用异或来交换变量是错误的
2、不要重载全局operator new()
3、带符号整数的除法与余数
4、在单元测试中mock系统调用
5、慎用著名namespace
6、采用有利于版本管理的代码格式
7、再探std:string
8、用sTL algorithm轻松解决几道算法面试题