muduo源码学习 Day04

前言

 我想这篇既然是学习muduo的开始,就应该先站在muduo的“外面“,去观摩一下,再去深入了解它。
 因为我最后的目标就是用自己的一个tinymuduo库去写一个多线程服务器,所以我重点关注的就是它的多线程模型。

一、为什么要用多线程?为什么不用单线程?用多线程的话该怎么用?

 这是学习服务器以来一直围绕我的问题,我现在的理解就是如果不是为了更好的利用CPU多核,那多线程就没有多大的价值,单线程下的IO复用恐怕更称心意。当然我见得还很少,只是自己一些粗浅的理解,只希望自己未来回头看的时候,能有更深的理解。
 既然提到了单线程,就先来看一下单线程服务器的模式,我只学习过一个,知道用的最广泛的也就这个,即Reactor的单线程模式,也就是eventloop,Day01中详细分析了它的工作模式,通过非阻塞IO和多路复用的方式,也可以同时处理多个连接,多路复用嘛,程序的基本结构就是一个事件循环,一个事件驱动,回调“连接的handler“实现业务逻辑,(记得redis哦),适合IO密集型应用。
 muduo的多线程模型采用了one loop one thread + thread pool 的编程模型,首先池的思想的好处就是线程数目基本固定,可以在程序启动的时候就确定,不会频繁的创建销毁,减少开销,one loop one thread其实从设计上就是让IO事件发生的线程固定,这个连接对象的一切操作都在同一个线程里发生,同一个TCP连接不必考虑事件并发。thread pool主要去解决计算量大的任务,使用方法与单reactor多线程模型类似。

为什么要去使用多线程

 单线程模式的服务器不能很好的处理任务的优先级,事件驱动的方式决定了他没有很好的考虑到优先级,天生就是先来先服务,而多线程可以轻松的解决这个问题(thread_ id后面会分析到)。
 从性能角度看,主线程+工作线程这种模式与一个单线程程序做对比 占不了多大优势(书上是这样讲的,我的理解是可能实际场景中任务量都比较大)。**多线程模型主要提升的就是平均响应性能,不会出现一个响应延迟过长的现象,对比一下Day01的两个模型的差异就很好理解了,多线程我认为最好的地方就在于分工明确,每个线程都有自己的业务,自己的“生活“,不像单线程鱼龙混杂。**多线程提高并发度也分模式,如果one thread one connection 这种模式反而远远低于单线程的多路复用,one loop one thread 的并发度足够大,且与CPU数目成正比。多线程可以将IO操作交给专门线程去作,实现计算与IO相互重叠,降低延迟。书上最后的总结,我觉得说的很透彻,一个请求或者说任务拿过来就是一条条指令,线程不能减少工作量,即不能减少CPU工作时间,能做的只是去合理调度,减少工期(说白了就是怎么充分利用CPU)。

怎么去用,有哪些方法要去学习

 多线程模式中的线程只要分为3类 IO线程,计算线程还有连接第三方的线程(数据库,异步日志等)。

1、绝不意气用事

 我们不能主观的去猜测程序中线程的运行顺序,必须通过适当的同步去验证,看的见其他线程运行的结果,说白了就是你的代码不能用sleep去保证一个线程在前,一个线程在后,得用同步去解决。

2、封装class,尽量减少使用线程原语

 封装好的锁。线程,条件变量基本已经满足需要。线程id不使用pthread_self()获得,使用一个变量保存gettid的返回值用来记得当前的线程id。
 用RAII封装文件描述符,用socket对象包装文件描述符,所有对此文件描述符的读写操作都通过此对象进行(连接即对象)。

3、能不共享就不共享,凡要共享先考虑并发读写

 如果一个对象从始至终都是在一个线程里没出去过,那对它而言就是单线程,就是线程安全的(没说就是零卡),两个线程你怎么读各自的或者共享的对象都行,就是不能有并发写,一旦有写操作,只读操作也必须跟着加锁。

4、每个文件描述符只由一个线程操作,比如对同一个epoll fd的操作(添加 删除 修改 等待)都放到同一个线程中执行

二、muduo的多线程模型

1.一切的开始-----------------Tcpsever

 准备好了各种工具(前面封装的那些),也学习了主要的模型(多reactor多线程服务器模型),正式开始学习muduo网路库,并开始一步一步实现自己的网络库。这里真的感谢导师一句真言,代码写之前要先设计(敲算法敲多了总想直接莽,又不知道怎么开始,浪费了很多时间)。muduo的样例中写的很清楚,一切的开始就是Tcpsever,那我们也就顺着它往下看。
 想象一下一个Tcpsever都要什么,也就是这个类中都应该有什么成员,成员函数。他的身份是主reactor,也就是他一定有一个accepter(Day01中重点分析),也一定有一个线程池(里面都是loop),他自己肯定跑在单独的一个loop中(事实证明这个loop还是客户端传过来的,至于为什么留到结尾处阐明)。也就是说他现在里面应该有两个成员类accepter和线程池一个成员loop
那么它的成员函数应该有哪些呢?
给accepter创建好的连接对象一个温暖的家,也就是给他分配一个新的loop,还应该可以把这个对象从他家赶出来并关闭这都是tcpsever应该做到的(accepter创建对象的函数也是tcpsever提供的,我也不知道为什么muduo要弄函数指针来回传);
基于此,先实现一个简单的tcpsever.h文件

class TcpServer : public NonCopyAble {
   public:
   TcpServer(EventLoop* loop, const Address& address);//address 是封装好的地址类
   ~TcpServer();
  
    void start() ;
    void removeConnection(const TcpConnectionPtr& conn);
    void removeConnectionInLoop(const TcpConnectionPtr& conn);
    void newConnection(int connfd, const Address& address);
 
    private:
    typedef std::map<int, TcpconnectionPtr> connectionMap;
 	const std::string iPport_;
    EventLoop* loop_;
    int nextConnid_;//最开始传1,新连接来了就++,并于新连接对象绑定
    std::unique_ptr<EventLoopThreadPool> threadPool;//这里我认为只能有一个实例,所以没用shared
    std::unique_ptr<Acceptor> acceptor_;//avoid revealing Acceptor
  };
             

这里我没有写muduo源码那些回调函数,下一篇accepter和channel类中一并给出,调来调去实在蒙了
 源码中还用removeConnection这个函数将真正的删除函数通过loop->runInLoop注册到连接所属的线程中保证了线程安全。大体上就这么多,因为这些成员函数的实现基本都用到了其他的类,所以实现将在accepter类和channal类分析完之后给出。

2.Accepter

 先提问,他是干什么的----------------------------------------------肯定是执行accept啊,众所周知accept一定会返回一个cfd,然后根据这个cfd创建一个连接对象。整体上就是这样,accept和多路复用结合就一定有一个小可爱去告诉accepter 客户连接请求来了,你该accept了,至于怎么创建连接留到后面,这个小可爱就是channel,channel其实就是用来保存客户端套接字和关心的事件,这里学习了muduo中的channal实现方式,下节会介绍。
设计 Accepter类一定有一个Channel成员类,一定有一个Socket对象(存lfd),当然一定有一个listen函数,还应该有个和多路复用打配合的回调函数,这个回调函数就是用来创建新的连接的。也就是说平时我们在写会回射服务器的时候,accept返回了一个cfd,我们就去盯着这个cfd了(把cfd的读写事件注册的epoll或者事件集中),而现在多了一步,不能简单的挂cfd了,挂的是生成的连接对象中的channel,还要在另一个loop中挂,也就是原来的回调函数是挂cfd,现在是生成新的连接,给他放到一个新的loop,再挂(这里其实挂的都是channel);

class Acceptor : noncopyable
{
 public:
  typedef std::function<void (int sockfd, const InetAddress&)> NewConnectionCallback;

  Acceptor(EventLoop* loop, const InetAddress& listenAddr);
  ~Acceptor();

  void setNewConnectionCallback(const NewConnectionCallback& cb)
  { newConnectionCallback_ = cb; }

  void listen();

  bool listening() const { return listening_; }
 private:
  void handleRead();
  EventLoop* loop_;//主LOOP
  Socket acceptSocket_;//自己的家人
  Channel acceptChannel_;//自己的家人
  NewConnectionCallback newConnectionCallback_;//这就是那个创建连接的函数,并给他分到一个loop中,这个函数在Tcpsever中,但是 是Tcpsever调用的Tcpconnection这个类中的
  
  bool listening_;
  int idleFd_;//非常巧妙地处理文件描述符不够怎么办
};

总结一下,Accepter从Tcpsever那里接收到有新的客户连接(因为accepter自己的channel在Tcpsever所在loop中的poller中注册,有一点绕),channel被激活后会被放到所在loop中的激活队列中,遍历激活队列时去执行channel对应的激活函数,激活函数根据激活原因,这里就是可读事件(新连接来了一定执行可读呀)选择回调函数,去执行相应的回调函数。下面可以清楚地看到accept_channel把可读事件的回调函数设置成了handleread,而handleread中调用的就是Tcpsever提供的newConnectionCallback_函数,就是去传建一个新连接对象。

Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
  : loop_(loop),
    acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
    acceptChannel_(loop, acceptSocket_.fd()),
    listening_(false),
    idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
  assert(idleFd_ >= 0);
  acceptSocket_.setReuseAddr(true);
  acceptSocket_.setReusePort(reuseport);
  acceptSocket_.bindAddress(listenAddr);
  acceptChannel_.setReadCallback(
      std::bind(&Acceptor::handleRead, this));
}
...
void Acceptor::handleRead()
{
  loop_->assertInLoopThread();
  InetAddress peerAddr;
  //FIXME loop until no more
  int connfd = acceptSocket_.accept(&peerAddr);
  if (connfd >= 0)
  {
    // string hostport = peerAddr.toIpPort();
    // LOG_TRACE << "Accepts of " << hostport;
    if (newConnectionCallback_)
    {
      newConnectionCallback_(connfd, peerAddr);
    }
    else
    {
      sockets::close(connfd);
    }
  }
 ...

3.不知道为什么要分开写的socket,channel,tcpconnection和poller

(1)Channel
首先衔接上面的accepter,分析channel类,channel其实就是个事件类,就是对监听文件描述符(socketfd)和其需要被关心的事件做的一个封装。
设计:channel应该主要有下面几个功能
1.他应该可以主动的设置被关心的事件出发以后所调用的回调函数,--------------------- set
2.将自己注册到所属loop的poller中,或从poller中移除------------------------ enable 和 disable
3.应该可以提供被激活后需要执行的回调函数,这样遍历激活队列(激活队列里放的都是channel)的时候,直接调用channel所属的回调函数就好,事实上是调用handleEventWithGuard这个函数,这个函数会根据激活原因调用不同的回调函数。---------------------------------------------handleEventWithGuard(Timestamp receiveTime);

class Channel : noncopyable
{
 public:
  typedef std::function<void()> EventCallback;
  typedef std::function<void(Timestamp)> ReadEventCallback;
  
  Channel(EventLoop* loop, int fd);
  ~Channel();

  void handleEvent(Timestamp receiveTime);
  void setReadCallback(ReadEventCallback cb)
  { readCallback_ = std::move(cb); }
  void setWriteCallback(EventCallback cb)
  { writeCallback_ = std::move(cb); }
  void setCloseCallback(EventCallback cb)
  { closeCallback_ = std::move(cb); }
  void setErrorCallback(EventCallback cb)
  { errorCallback_ = std::move(cb); }

  void tie(const std::shared_ptr<void>&);

  int fd() const { return fd_; }
  int events() const { return events_; }
  void set_revents(int revt) { revents_ = revt; } // used by pollers
  // int revents() const { return revents_; }
  bool isNoneEvent() const { return events_ == kNoneEvent; }

  void enableReading() { events_ |= kReadEvent; update(); }//update 会把这些操作实现
  void disableReading() { events_ &= ~kReadEvent; update(); }
  void enableWriting() { events_ |= kWriteEvent; update(); }
  void disableWriting() { events_ &= ~kWriteEvent; update(); }
  void disableAll() { events_ = kNoneEvent; update(); }
  bool isWriting() const { return events_ & kWriteEvent; }
  bool isReading() const { return events_ & kReadEvent; }

  // for Poller
  int index() { return index_; }
  void set_index(int idx) { index_ = idx; }

  void doNotLogHup() { logHup_ = false; }

  EventLoop* ownerLoop() { return loop_; }
  void remove();

 private:
  static string eventsToString(int fd, int ev);
  void update();
  void handleEventWithGuard(Timestamp receiveTime);
  static const int kNoneEvent;
  static const int kReadEvent;
  static const int kWriteEvent;

  EventLoop* loop_;
  const int  fd_;
  int        events_;
  int        revents_; // it's the received event types of epoll or poll
  int        index_; // used by Poller.
  bool       logHup_;

  std::weak_ptr<void> tie_;
 //构造函数的时候 会为这些函数赋值(handleread 就是这时候传进来的)
  ReadEventCallback readCallback_;
  EventCallback writeCallback_;
  EventCallback closeCallback_;
  EventCallback errorCallback_;
};

 我们先来看handleEventWithGuard这个函数的实现,handleEventWithGuard这个函数是channel激活后去调用的函数,它是根据激活事件去调用TcpConnection中的函数,所以在这之前我们必须要知道TcpConnection这个对象是否还活着,这里可以联想前面博客中的对象池,一样的我们使用weak_ptr,采用弱回调技术,既不会发生因为使用shared_ptr无法销毁TcpConnection对象,又可以避免访问有问题的TcpConnection对象(因为Channel中也留有对TcpConnection的引用)。通过handleEvent这个函数中对weak_ptr----------tie_指针的尝试加锁,保证了线程安全。

void Channel::handleEvent(Timestamp receiveTime)
{
  std::shared_ptr<void> guard;
  if (tied_)
  {
    guard = tie_.lock();
    if (guard)
    {
      handleEventWithGuard(receiveTime);
    }
  }
  else
  {
    handleEventWithGuard(receiveTime);
  }
}

void Channel::handleEventWithGuard(Timestamp receiveTime)
{
  eventHandling_ = true;
  LOG_TRACE << reventsToString();
  if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
  {
    if (logHup_)
    {
      LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLHUP";
    }
    if (closeCallback_) closeCallback_();
  }

  if (revents_ & POLLNVAL)
  {
    LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLNVAL";
  }

  if (revents_ & (POLLERR | POLLNVAL))
  {
    if (errorCallback_) errorCallback_();
  }
  if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))
  {
    if (readCallback_) readCallback_(receiveTime);
  }
  if (revents_ & POLLOUT)
  {
    if (writeCallback_) writeCallback_();
  }
  eventHandling_ = false;
}

(2)Socket
放松一下,这个类还比较简单。
分析:如我我要对socket进行封装里面应该有什么,肯定有sockfd和客户地址,bind,listen,accept的函数实现也封装在这里,还应该可以设置这个文件描述符的一些属性,比如设置Tcp长连接的开关。
实现

class Socket : noncopyable
{
 public:
  explicit Socket(int sockfd)
    : sockfd_(sockfd)
  { }

  // Socket(Socket&&) // move constructor in C++11
  ~Socket();
  int fd() const { return sockfd_; }
  /// abort if address in use
  void bindAddress(const InetAddress& localaddr);
  /// abort if address in use
  void listen();

 
  int accept(InetAddress* peeraddr);

  void shutdownWrite();
  void setTcpNoDelay(bool on);
  void setReuseAddr(bool on);
  void setReusePort(bool on);
  void setKeepAlive(bool on);

(3)Poller
看名字猜功能,他就是epoll和poll的封装
分析:以epoll为例,那肯定主要就是epoll_creat, epoll_wait , epoll_ctl,成员肯定有一个装epoll_event的结构体数组,支持的操作主要就是把channel挂到激活队列上

/*
 * 对epoll函数的封装,继承自Poller
 */
class EPollPoller : public Poller
{
 public:
  EPollPoller(EventLoop* loop);
  virtual ~EPollPoller();

  /* epoll_wait */ 
  virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels);
  /* ADD/MOD/DEL */ 
  virtual void updateChannel(Channel* channel);
  /* DEL */
  virtual void removeChannel(Channel* channel);

 private:
  static const int kInitEventListSize = 16;

  /* EPOLL_CTL_ADD/MOD/DEL转成字符串 */
  static const char* operationToString(int op);

  /* epoll_wait返回后将就绪的文件描述符添加到参数的激活队列中 */
  void fillActiveChannels(int numEvents,
                          ChannelList* activeChannels) const;

  /* 由updateChannel/removeChannel间接调用,执行epoll_ctl */
  void update(int operation, Channel* channel);

  typedef std::vector<struct epoll_event> EventList;

  int epollfd_;
  EventList events_;
};

Tcpconnection留到下一篇和这一篇遗留的Tcpsever的一部分一起,顶不住了。

总结

Tcpsever虽然开始了,但还没结束,会在下一篇给出具体的流程,在分析完Tcpconnection之后,重新回到Tcpsever的实现上。总之,本篇确定了Tcpsever中应该有的成员和支持的操作,Tcpsever.start 跑起来开始,accepter利用它的socket类和channel类就为我们做好了一切连接前的准备吗,在sever的初始化同时线程池也同时开始run,channel通过主loop中的poller收到连接请求,就会将这个channel放到激活队列中,当epoll_wait返回时,就会去遍历激活队列,调用channel中的handleEvent也就是激活函数,handleEvent尝试对指向Tcpconnection对象的weak_ptr尝试加锁,加锁成功后说明该Tcpconnection对象确保存活,调用handleEventWithGuard函数,根据事件激活原因,去执行Tcpconnection对象中对应的回调函数,创建新的连接,并为其找到一个loop,分好它的channel,确定好channel中的回调函数,将它的channel注册到它自己loop中的poller中,等待激活。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值