Linux多线程服务端编程:使用muduo C++网络库 学习笔记 第八章 muduo网络库设计与实现(下)

TimerQueue::cancel()

8.2实现的TimerQueue不能注销定时器,本节补充这一功能。TimerQueue::cancel()的一种简单实现是用shared_ptr来管理Timer对象,再将TimerrId定义为weak_ptr<Timer>,这样几乎不用我们做什么事情。在C++ 11中应该也足够高效,因为shared_ptr具备移动语义,可以做到引用计数值始终不变,没有原子操作的开销。但用shared_ptr来管理Timer对象似乎显得有点小题大做,而且这种做法也有一个小小的缺点,如果用户一直持有TimerId,会造成引用计数所占的内存无法释放,而本节展示的做法不会有这个问题。

本节采用更传统的方式,保持现有的设计,让TimerId包含Timer *。但这是不够的,因为无法区分地址相同的先后两个Timer对象。因此每个Timer对象有一个全局递增的序列号int64_t sequence_(用原子计数器(AtomicInt64)生成),TimerId同时保存Timer *和sequence_,这样TimerQueue::cancel()就能根据TimerId找到需要注销的Timer对象。
在这里插入图片描述
TimerQueue新增了cancel()接口函数,这个函数是线程安全的。
在这里插入图片描述
在这里插入图片描述
cancel()有对应的cancelInLoop()函数,因此TimerQueue不必用锁。TimerQueue新增了几个数据成员,activeTimers_保存的是目前有效的Timer的指针,并满足invariant:timers_.size() == activeTimers_.size(),因为这两个容器保存的是相同的数据,只不过timers_是按到期时间排序,activeTimers_是按对象地址排序。
在这里插入图片描述
由于TimerId不负责Timer的生命期,其中保存的Timer *可能失效,因此不能直接dereference,只有在activeTimers_中找到了Timer时才能提领。注销定时器的流程如下,照例用EventLoop::runInLoop()将调用转发到IO线程:

// reactor/s11/TimerQueue.cc
void TimerQueue::cancel(TimerId timerId)
{
    loop_->runInLoop(boost::bind(&TimerQueue::cancelInLoop, this, timerId));
}

void TimerQueue::cancelInLoop(TimerId timerId)
{
    loop_->assertInLoopThread();
    assert(timers_.size() == activeTimers_.size());
    ActiveTimer timer(timerId.timer_, timerId.sequence_);
    ActiveTimerSet::iterator it = activeTimers_.find(timer);
    if (it != activeTimers_.end())
    {
        size_t n = timers_.erase(Entry(it->first->expiration(), it->first));
        assert(n == 1); (void)n;
        delete it->first;    // FIXME: no delete please
        activeTimers_.erase(it);
    }
    else if (callingExpiredTimers_)
    {
        cancelingTimers_.insert(timer);
    }
    assert(timers_.size() == activeTimers_.size());
}

上面这段代码中的cancelingTimers_和callingExpiredTimers_是为了应对“自注销”这种情况,即在定时器回调中注销当前定时器:

// s11/test4.cc
muduo::EventLoop *g_loop;
muduo::TimerId toCancel;

void cancelSelf()
{
    print("cancelSelf()");
    g_loop->cancel(toCancel);    // 1
}

int main()
{
    muduo::EventLoop loop;
    g_loop = &loop;
    
    toCancel = loop.runEvery(5, cancelSelf);
    loop.loop();
}

当运行到注释1所在行的时候,toCancel代表的timer已经不在timers_和activeTimers_这两个容器中,而是位于162行的expired中(见8.2.1的getExpired()实现)。
在这里插入图片描述
在这里插入图片描述
为了应对这种自销毁情况,TimerQueue会记住在本次调用到期Timer期间有哪些cancel()请求(放到cancelingTimers_中),并且不再把已cancel()的Timer添加回timers_和activeTimers_当中。执行完所有已到期的Timer后,会调用reset函数,reset函数会重新设置周期性执行的timer(除非此周期性执行的timer已被cancel,即该timer存在于cancelingTimers_中),其中会访问到刚刚执行的到期timer,如果在cancelInLoop函数中就delete了timer,则在reset函数中会报错,因此要在reset函数中再delete timer。
在这里插入图片描述
注意TimerQueue在执行170行时没有检查Timer是否已撤销,这是因为TimerQueue::cancel()并不提供strong guarantee。TimerQueue::getExpired()和TimerQueue::insert()均增加了与activeTimers_有关的处理,此处从略。

8.12 TcpClient

有了Connector,TcpClient就不难实现了,它的代码与TcpServer甚至有几分相似(都有newConnection和removeConnection这两个成员函数),只不过每个TcpClient只管理一个TcpConnection。代码从略,此处谈几个要点:
1.TcpClient具备TcpConnection断开之后重新连接的功能,加上Connector具备反复尝试连接的功能,因此客户端和服务端的启动顺序无关紧要。可以先启动客户端,一旦服务端启动,半分钟之内即可恢复连接(由Connector::kMaxRetryDelayMs常数控制);在客户端运行期间服务端可以重启,客户端也会自动重连。

2.连接断开后初次重试的延迟应该具有随机性,比方说服务器崩溃,它所有的客户连接同时断开,然后0.5s之后同时再次发起连接,这样既可能造成SYN丢包,也可能给服务端带来短期大负载,影响其服务质量。因此每个TcpClient应该等待一段随机的时间(0.5~2s),再重试,避免拥塞。

3.发起连接的时候如果发生TCP SYN丢包,那么系统默认的重试间隔是3s,这期间不会返回错误码,而且这个间隔似乎不容易修改。如果需要缩短间隔,可以再用一个定时器,在0.5s或1s之后发起另一次连接(http://bitsup.blogspot.com/2010/12/accelerated-connection-retry-for-http.html)。如果有需求的话,这个功能可以做到Connector中。

4.目前本节实现的TcpClient没有充分测试动态增减的情况,也就是说没有充分测试TcpClient的生命期比EventLoop短的情况,特别是没有充分测试TcpClient在连接建立期间析构的情况。编写这方面的单元测试多半要用到12.4介绍的技术。

注意目前muduo 0.8.0采用shared_ptr来管理Connector,因为在编写这部分代码的时候TimerQueue尚不支持cancel()操作。将来muduo 1.0会在充分测试的前提下改用这里展示的简洁的实现。

8.13 epoll

epoll(4)是Linux独有的高效的IO multiplexing机制,它与poll(2)的不同之处主要在于poll(2)每次返回整个文件描述符数组,用户代码需要遍历数组以找到那些文件描述符上有IO事件(见8.1.2的Poller::fillActiveChannels()),而epoll_wait(2)返回的是活动fd的列表,需要遍历的数组通常会小得多。在并发连接数较大而活动连接比例不高时,epoll(4)比poll(2)更高效。

本节我们把epoll(4)封装为EPoller class,它与8.1.2的Poller class具有完全相同的接口。muduo实际的做法是定义Poller基类并提供两份实现PollPoller和EPollPoller。这里为了简单起见,我们直接修改EventLoop,只需把代码中的Poller替换为EPoller。

EPoller的关键数据结构如下,其中events_不是保存所有关注的fd列表,而是一次epoll_wait(2)调用返回的活动fd列表,它的大小是自适应的。

typedef std::vector<struct epoll_event> EventList;
typedef std::map<int, Channel *> ChannelMap;
int epollfd_;    // ::epoll_create()
EventList events_;
ChannelMap channels_;

struct epoll_event的定义如下,注意epoll_data是个union,muduo使用的是其ptr成员,用于存放Channel *,这样可以减少一步look up。
在这里插入图片描述
为了减少转换,muduo Channel没有定义自己IO事件的常量,而是直接使用poll(2)的定义(POLLIN、POLLOUT等),在Linux中它们和epoll(4)的常量相等。

// reactor/s13/EPoller.cc
// On Linux, the constant of poll(2) and epoll(4)
// are expected to be the same.
// BOOST_STATIC_ASSERT是boost库的一个宏,类似assert,但它是编译期进行静态断言,不会生成运行时额外代码
BOOST_STATIC_ASSERT(EPOLLIN == POLLIN);
BOOST_STATIC_ASSERT(EPOLLPRI == POLLPRI);
BOOST_STATIC_ASSERT(EPOLLOUT == POLLOUT);
BOOST_STATIC_ASSERT(EPOLLRDHUP == POLLRDHUP);
BOOST_STATIC_ASSERT(EPOLLERR == POLLERR);
BOOST_STATIC_ASSERT(EPOLLHUP == POLLHUP);

EPoller::poll()的关键代码如下。注释1所在行在C++ 11中可写为events_.data()。注释2所在行表示如果当前活动fd的数目填满了events_,那么下次就尝试接收更多的活动fd。events_的初始长度是16(kInitEventListSize),其会根据程序的IO繁忙程度自动增长,但目前不会自动收缩。

// reactor/s13/EPoller.cc
Timestamp EPoller::poll(int timeoutMs, ChannelList *activeChannels)
{
    int numEvents = ::epoll_wait(epollfd_, 
                                 &*events_.begin(),    // 1
                                 static_cast<int>(events_.size()), 
                                 timeoutMs);
    Timestamp now(Timestamp::now());
    if (numEvents > 0)
    {
        LOG_TRACE << numEvents << " events happended";
        fillActiveChannels(numEvents, activeChannels);
        if (implicit_cast<size_t>(numEvents) == events_.size())
        {
            events_.resize(events_.size() * 2);    // 2
        }
    }
    // 此处epoll_wait(2)的错误处理从略
    return now;
}

EPoller::fillActiveChannels()的功能是将events_中的活动fd填入activeChannels,其中注释1行到注释2行是在检查invariant。

// reactor/s13/EPoller.cc
void EPoller::fillActiveChannels(int numEvents, ChannelList *activeChannels) const
{
    assert(implicit_cast<size_t>(numEvents) <= events_.size());
    for (int i = 0; i < numEvents; ++i)
    {
        Channel *channel = static_cast<Channel *>(events_[i].data.ptr);
#ifndef NDEBUG
        int fd = channel->fd();    // 1
        ChannelMap::const_iterator it = channels_.find(fd);
        assert(it != channels_.end());
        assert(it->second == channel);    // 2
#endif
        channel->set_revents(events_[i].events);
        activeChannels->push_back(channel);
    }
}

updateChannel()和removeChannel()的代码从略。因为epoll是有状态的,因此这两个函数要时刻维护内核中的fd状态与应用程序的状态相符,Channel::index()和Channel::set_index()被挪用为标记此Channel是否位于epoll的关注列表之中。updateChannel()和removeChannel()两个函数的复杂度是O(logN),因为Linux内核用红黑树来管理epoll关注的文件描述符清单。

测试程序无需修改,全都已经自动用上了epoll(4)。

至此,一个基于事件的非阻塞TCP网络库已经初具规模。

8.14 测试程序一览

本章简要介绍了muduo的实现过程,是一个具有教学示范意义的项目,希望有助于读者理解one loop per thread这一编程模型背后的实现,在运用时更加得心应手。如果对本章代码有疑问,应该以最新版的muduo源码为准。

本节没有配套代码,以下列出前面各节出现的测试代码的功能:
8.0:s00/test1.cc。在两个线程里各自运行一个EventLoop。

8.0:s00/test2.cc。试图在非IO线程调用EventLoop::loop(),程序崩溃。

8.1:s01/test3.cc。用Channel关注timerfd的可读事件。

8.2:s02/test4.cc。TimerQueue示例。

8.3:s03/test5.cc。IO线程调用EventLoop::runInLoop()和EventLoop::runAfter()。

8.3:s03/test6.cc。跨线程调用EventLoop::runInLoop()和EventLoop::runAfter()。

8.4:s04/test7.cc。Acceptor示例。

8.5:s05/test8.cc。discard服务。

8.8:s08/test9.cc。echo服务。

8.8:s08/test10.cc。发送两次数据,测试TcpConnection::send()。

8.9:s09/test11.cc。chargen服务,使用WriteCompleteCallback。

8.11:s11/test12.cc。Connector示例。

8.12:s12/test13.cc。TcpClient示例。

本章Acceptor、Connector、Reactor等术语是Douglas Schmidt发明的,见其原始论文。

作者在一篇访谈中谈到了muduo将来的计划:1.0版完善单元测试,基本覆盖各种code path,特别是各种Sockets API出错情况的测试,以及用户调用与IO事件的交互。2.0版启用C++ 11,特别是rvalue reference有助于提高性能与资源管理的便利性。以上计划中的版本尚无明确的事件表。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Linux多线程服务端编程使用muduo C++网络》是一本介绍使用muduo C++网络进行多线程服务端编程的电子书。该书由陈硕所著,适用于想要学习Linux多线程网络编程的开发人员。 本书从基础概念开始,详细介绍了多线程网络编程的原理和技术,并结合muduo C++网络使用示例,演示了如何开发高性能、稳定的网络服务端程序。 muduo C++网络是一个基于事件驱动的网络编程,它采用了Reactor模式,提供了高效的异步IO操作。该封装了多线程、多进程、事件等相关操作,使得开发者可以简单、快速地开发网络服务端应用。 在本书中,作者通过具体的实例和代码示例,讲解了muduo C++网络使用方法和注意事项。书中内容分为多个章节,包括网络编程基础、IO复用、事件回调、线程同步、线程池等,涵盖了开发者在实际项目中可能遇到的各种情况。 通过学习Linux多线程服务端编程使用muduo C++网络》,读者可以了解到多线程服务端编程的基本原理和技术,掌握使用muduo C++网络进行高效开发的方法,并能够开发出高并发、高性能的网络服务端应用。 总之,该书是一本实用的网络编程指南,对于想要学习Linux多线程网络编程以及使用muduo C++网络的开发人员来说,具有较高的参考价值。 ### 回答2: 《Linux 多线程服务端编程使用 muduo C++ 网络》是一本介绍如何使用 muduo C++ 网络进行 Linux 多线程服务端编程的指南。该书主要目的是教读者如何构建高性能、可扩展的网络服务端应用程序。 该书首先介绍了多线程编程的基础知识,包括线程创建、线程同步与互斥、线程安全的数据结构等内容。然后,书中详细介绍了 muduo C++ 网络使用方法,包括网络编程基础、事件驱动模型、网络编程设计模式等。读者可以通过学习这些内容,了解如何使用 muduo C++ 网络来构建高性能的多线程服务端。 该书还介绍了业界常用的网络协议及其实现原理,例如 TCP/IP、HTTP 协议等。通过学习这些知识,读者可以更好地理解网络编程的工作原理,从而更好地设计实现自己的网络服务端应用程序。 此外,书中还涵盖了一些实际案例和实战经验,读者可以通过这些案例了解如何应对常见的网络编程问题,并且学习到一些实际的开发技巧和调试技巧。 总而言之,《Linux 多线程服务端编程使用 muduo C++ 网络》是一本非常实用的指南,可以帮助读者快速入门多线程服务端编程,并且掌握使用 muduo C++ 网络构建高性能的网络服务端应用程序的技巧。无论是初学者还是有一定网络编程经验的开发者,都可以从这本书中获得很多有价值的知识和经验。 ### 回答3: 《Linux 多线程服务端编程使用 muduo C++ 网络》是一本关于使用muduo C++网络进行Linux多线程服务端编程的书籍。本书以muduo C++网络为基础,深入讲解了多线程服务端编程的相关知识和技巧。 本书主要内容包括: 1. muduo的介绍:介绍了muduo的特性、设计思想和基本用法。muduo是基于Reactor模式的网络,提供了高效的事件驱动网络编程框架,有助于开发者快速搭建高性能的网络服务端。 2. 多线程编程的基础知识:介绍了多线程编程的基本概念和相关的线程同步和互斥机制,如互斥锁、条件变量等。并讲解了如何正确地使用这些机制,以保证多线程程序的正确性和高效性。 3. muduo C++网络使用:详细介绍了muduo的线程模型、事件驱动机制和网络编程接口。通过实例代码和示意图,演示了如何使用muduo构建一个多线程网络服务端,包括创建监听套接字、事件的注册和处理、多线程任务分配等。 4. 高性能服务端设计和优化:讲解了如何设计和优化高性能的多线程服务端。包括使用线程池提高并发处理能力、使用非阻塞IO提升数据处理效率、优化网络通信性能等方面的内容。 该书适合具有一定Linux编程基础的开发人员学习和参考。通过学习该书,读者可以掌握使用muduo C++网络进行多线程服务端编程的技巧,提升服务端的性能和可靠性。同时,也可了解到网络编程领域的一些高级技术和最佳实践。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值