前言

网络编程的连接断开一向比连接建立复杂的多,这一点在陈硕写的muduo库中体现的淋漓尽致,同时也充分体现了C++程序在对象生命周期管理上的复杂性,稍有不慎,满盘皆输。

为了纪念自己啃下muduo库的断开连接,本篇博客将会着重讲述muduo库连接的断开过程,以及断开过程中可能会遇到的各种问题,还有muduo库作者精妙的思想。


一、销毁连接复杂在哪里?

何时才会断开连接

讨论断开连接的复杂性之前,我们需要先明确muduo库什么时候才会断开连接,并将TcpConnection对象销毁。

muduo库有两种断开连接的方式,一种是调用handleClose(被动),一种是调用shutdown(主动)。按照陈硕本人的说法,muduo库不会主动断开连接。其意思并不是muduo库会傻傻地等待对面“主动”调用close,自己才会有断开连接的操作。实际上,这里不会主动断开连接的意思是muduo库会等待对面调用close函数,自己才真正将TcpConnection对象销毁。

也就是说,muduo库断开连接的时机只有在recv函数返回0的时候,才会真正进行连接的销毁。

但如果我们想主动断开连接怎么办?很简单,我们可以“诱导”客户端调用close函数。这一点在TcpConnection::shutdown函数里有所体现,它会调用shutdown这个系统调用,提醒客户端我们想断开连接。如果客户端懂事的话,他就会主动调用close,然后服务端的recv会返回0字节,muduo库就可以着手进行连接的销毁了。



当然主动关闭连接不是这里的重点,重点是我上面的一句话:muduo库断开连接的时机只有在recv函数返回0的时候,才会真正进行连接的销毁,而且最终都会调用handleClose。

让我们看看是怎么发生的:

void TcpConnection::handleRead(Timestamp receiveTime)
{
loop_->assertInLoopThread();
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if (n == 0)
{
//看看这里:当readFd返回值为0的时候,说明客户端调用了close,现在我们就可以放心的销毁连接了
handleClose();
}
else
{
errno = savedErrno;
LOG_SYSERR << "TcpConnection::handleRead";
handleError();
}
}

销毁连接对象可能会出现的问题

目前我们已经知道销毁连接对象发生在handleClose函数中,且销毁连接对象需要把connections_中的shared_ptr指针销毁掉。如果你和我一样是一个菜鸟程序员,你可能就会想:我们只需要在handleClose里将TcpServer对象中的connections_中的TcpConnection对象erase掉就行了,具体实现可以在handleClose里调用TcpServer的回调函数。

大错特错。请注意一下,调用handleClose的时候,当前this指针指向的就是即将销毁的连接对象,也就是说,我们正在尝试在一个对象的成员函数中销毁掉对象本身

这可以说是大逆不道,因为handlerClose始终是要返回的,当他返回的时候,发现自己所属的对象不见了,接下来函数可能还有一些程序要运行,运行在一个已经不存在的对象上,之后会发生什么事情,谁也不知道。就编程的规范来说,这种事情是绝对不允许的。

此外还有一些线程安全问题,之后也会提到。

因此,muduo库为了安全地销毁连接,写了一长串弯弯绕绕的函数调用,这是之后要讲述的重点。

二、销毁连接对象的过程

部分引用该篇博客:muduo网络库学习:Tcp建立连接与断开连接

handleClose

我们已经知道,销毁连接无论如何都会调用handleClose,就让我们看看handleClose中都发生了什么:

  1. 将Channel从Poller中移除
  2. 调用用户提供的关闭回调函数connectionCallback_
  3. 调用TcpServer提供的关闭回调函数closeCallBack_->TcpServer::removeConnection,将自己从TcpServer的tcp连接map中移除
void TcpConnection::handleClose()
{
loop_->assertInLoopThread();
LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString();
assert(state_ == kConnected || state_ == kDisconnecting);
// we don't close fd, leave it to dtor, so we can find leaks easily.
setState(kDisconnected);
channel_->disableAll(); /* 此时当前的TcpConnection的引用计数为2,一个是guardThis,另一个在TcpServer的connections_中 */
TcpConnectionPtr guardThis(shared_from_this());
connectionCallback_(guardThis);
// must be the last line
/*
* closeCallback返回后,TcpServer的connections_(tcp连接map)已经将TcpConnection删除,引用计数变为1
* 此时如果函数返回,guardThis也会被销毁,引用计数变为0,这个TcpConnection就会被销毁
* 所以在TcpServer::removeConnectionInLoop使用bind将TcpConnection生命期延长,引用计数加一,变为2
* 就算guardThis销毁,引用计数仍然有1个
* 等到调用完connectDestroyed后,bind绑定的TcpConnection也会被销毁,引用计数为0,TcpConnection析构
*/
closeCallback_(guardThis);
}

前两点和连接对象的生命周期没什么关系,不多说。重点是第三点。

在创建连接的时候,TcpConnection对象的closeCallback_回调函数会绑定TcpServer的removeConnection,参数是TcpConnection对象的shared_ptr,为的就是给该连接对象续命,让连接对象的指针在connections_中被删除掉后,还能在其他地方苟延残喘一下。

接下来让我们看看removeConnection中发生了什么。

TcpServer::removeConnection

void TcpServer::removeConnection(const TcpConnectionPtr& conn)
{
// FIXME: unsafe
/*
* 在TcpConnection所在的事件驱动循环所在的线程执行删除工作
* 因为需要操作TcpServer::connections_,就需要传TcpServer的this指针到TcpConnection所在线程
* 会导致将TcpServer暴露给TcpConnection线程,也不具有线程安全性
*
* TcpConnection所在线程:在创建时从事件驱动循环线程池中选择的某个事件驱动循环线程
* TcpServer所在线程:事件驱动循环线程池所在线程,不在线程池中
*
* 1.调用这个函数的线程是TcpConnection所在线程,因为它被激活,然后调用回调函数,都是在自己线程执行的
* 2.而removeConnection的调用者TcpServer的this指针如今在TcpConnection所在线程
* 3.不同的removeConnection的调用者处在不同的线程中,如果同时操作TcpServer的成员的话,是有可能出错的
* 4.所以不安全
*
* 为什么不在TcpServer所在线程执行以满足线程安全性(TcpConnection就是由TcpServer所在线程创建的)
* 1.只有TcpConnection自己知道自己什么时候需要关闭,TcpServer哪里会知道
* 2.一旦需要关闭,就必定需要将自己从TcpServer的connections_中移除,还是暴露了TcpServer
* 3.这里仅仅让一条语句变为线程不安全的,然后直接用TcpServer所在线程调用删除操作转为线程安全
*/
loop_->runInLoop(std::bind(&TcpServer::removeConnectionInLoop, this, conn));
}

该函数的代码很短,因为他不是线程安全的:该函数是在TcpConnection对象的事件循环线程中调用的,不同的事件循环线程可能会在这里发生冲突,因为这些不同的线程有可能在同一时间调用removeConnection。

因此,为了线程安全,muduo库这里调用了runInLoop函数。这个函数是线程安全的,可以将绑定的函数移动到loop_所在线程执行。这样,所有销毁连接的动作就全部转移到了TcpServer所在的主事件循环中,做到了线程安全。

在这里,销毁连接的动作是TcpServer::removeConnectionInLoop,要求运行在同一个事件循环中(函数调用又套了一层)。

另外还有一个细节,当removeConnection返回,然后handleClose返回后,该线程上的所有shared_ptr就不复存在了。现在,连接对象的生命被两个分享指针对象掌握,一个在TcpConnection的connection_里,一个在removeConnectionInLoop的参数列表里(因为std::bind函数拷贝了一份分享指针)。

这里不得不赞美一下陈硕大佬所开发的runInLoop函数,这为保证线程安全提供了一种新的思路。按照我以前所接触到的做法,通常都是在有线程安全问题的函数里加锁,这无论是在效率上还是在编写难度上都是比较高的。而runInLoop函数将非线程安全的函数统一移动到同一个线程上执行,这样的做法非常巧妙和优雅,即保证了线程安全,又简化了代码。当然,runInLoop中还是需要加锁的。

TcpServer::removeConnectionInLoop

这个函数的作用是将TcpServer中的connections_里的连接对象移除掉。因为传入的参数是当前要销毁TcpConnection对象的shared_ptr,因此在erase之后,智能指针的引用计数还有1,就是当前函数里存在的连接对象指针。只有当shared_ptr的引用计数为0时对象才会被销毁掉,所以现在连接对象还有一条狗命。

void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn)
{
loop_->assertInLoopThread();
LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_
<< "] - connection " << conn->name();
//移除连接对象指针,引用计数-1
size_t n = connections_.erase(conn->name());
(void)n;
assert(n == 1);
EventLoop* ioLoop = conn->getLoop(); /*
* std::bind绑定函数指针,注意是值绑定,也就是说conn会复制一份到bind上
* 这就会延长TcpConnection生命期,否则
* 1.此时对于TcpConnection的引用计数为2,参数一个,connections_中一个
* 2.connections_删除掉TcpConnection后,引用计数为1
* 3.如果此时removeConnectionInLoop返回,另一个线程的handleClose也返回(和removeConnectionInLoop返回没有关系),引用计数为0,会被析构
* 4.bind会值绑定,conn复制一份,TcpConnection引用计数加1,就不会导致TcpConnection被析构
*/
ioLoop->queueInLoop(
std::bind(&TcpConnection::connectDestroyed, conn));
}

当erase掉对象指针时,TcpConnection对象的生命已经细若游丝(陈硕原话)。如果放任函数结束掉的话,conn指针会当场析构,引用计数归零,连接对象就不复存在了。

显然muduo库不想让连接对象就这样消失,他还需要进行一些收尾工作。在该函数的末尾,又回调了TcpConnection::connectDestroyed,并将其移动到连接对象所在的事件循环线程中,由poller循环执行该回调函数。当该回调函数结束后,该连接对象就正式消失了。

TcpConnection::connectDestroyed

最后看看收尾工作吧。没什么特别值得说的。无非是确保连接关闭,移除channel罢了。当该函数执行结束后,连接对象的引用计数归零,调用析构函数,正式销毁。

void TcpConnection::connectDestroyed()
{
loop_->assertInLoopThread();
if (state_ == kConnected)
{
setState(kDisconnected);
channel_->disableAll(); connectionCallback_(shared_from_this());
}
channel_->remove();
}

总结

这里放一张官方的生命周期图

回顾整个流程,看似很复杂,梳理完感觉也没多少东西。如果仅仅是为了内存安全的话,在handleClose中创建一个自己的shared_ptr就已经可以保证handleClose结束前对象不会被销毁,后续的很多操作是为了线程安全以及保证最后能够执行TcpConnection::connectDestroyed。

一点其他想法

能不能把connectdestroyd函数提前到handleClose调用时呢?

在个人实现的muduo库中,采用了延迟销毁的操作。没有那么多线程的切换和函数调用的嵌套,仅仅是在handleClose的函数内,将该连接持有的资源释放,如socket的关闭,channel的移除等等。这样整个连接对象实际上就是一个名存实亡的状态,占有一份内存,却没有掌握任何资源。当有同名的新连接对象产生时,就会把connections中原有的对象覆盖掉,实现了延迟删除。

这样的做法我说不上好不好,在牺牲了一定内存空间的情况下简化了代码,我觉得还是可以的。

PS:现在突然想到,这种延迟删除的做法也是有风险的:前脚关闭了socket,万一函数还没有返回,后脚又来了一个相同socket的连接怎么办?还是会出现同样的内存安全问题。路漫漫其修远兮!

谈谈muduo库的销毁连接对象——C++程序内存管理和线程安全的极致体现的更多相关文章

  1. Objective-C:Objective-C 和 Core Foundation 对象相互转换的内存管理

    Objective-C 和 Core Foundation 对象相互转换的内存管理 iOS允许Objective-C 和 Core Foundation 对象之间可以轻松的转换,拿 NSString ...

  2. Objective-C 和 Core Foundation 对象相互转换的内存管理总结

    本文转载至 http://blog.csdn.net/allison162004/article/details/38756649 OS允许Objective-C 和 Core Foundation ...

  3. muduo库的简单使用-echo服务的编写

    muduo库的简单使用 muduo是一个基于事件驱动的非阻塞网络库,采用C++和Boost库编写. 它的使用方法很简单,参考这篇文章:TCP网络编程本质论 里面有这么几句: 我认为,TCP 网络编程最 ...

  4. muduo库整体架构简析

    muduo是一个高质量的Reactor网络库,采用one loop per thread + thread loop架构实现,代码简洁,逻辑清晰,是学习网络编程的很好的典范. muduo的代码分为两部 ...

  5. SignalR代理对象异常:Uncaught TypeError: Cannot read property 'client' of undefined 推出的结论 SignalR 简单示例 通过三个DEMO学会SignalR的三种实现方式 SignalR推送框架两个项目永久连接通讯使用 SignalR 集线器简单实例2 用SignalR创建实时永久长连接异步网络应用程序

    SignalR代理对象异常:Uncaught TypeError: Cannot read property 'client' of undefined 推出的结论   异常汇总:http://www ...

  6. muduo库源码剖析(一) reactor模式

    一. Reactor模式简介 Reactor释义“反应堆”,是一种事件驱动机制.和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,而是恰恰相反,Reactor逆置了事件处理流程, ...

  7. SQL优化技巧--远程连接对象引起的CTE性能问题

    背景 最近SSIS的开发过程中遇到几个问题.其中使用CTE时,遇到一个远程连接对象,结果导致严重的性能问题,为了应急我就修改了代码. 之前我写了一篇介绍CTE的随笔包含了CTE的用法等: http:/ ...

  8. 数据库连接工具类 数据库连接工具类——仅仅获得连接对象 ConnDB.java

    package com.util; import java.sql.Connection; import java.sql.DriverManager; /** * 数据库连接工具类——仅仅获得连接对 ...

  9. JDBC 创建连接对象的三种方式 、 properties文件的建立、编辑和信息获取

    创建连接对象的三种方式 //第一种方式 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/ ...

  10. Com 笔记 -可连接对象

    首先 两个概念: 1.入接口 :源对象提供给客户调用的. 2.出接口 :源对象通过此接口可直接或间接调用客户.类似delegate 对于实现了出接口的对象称为可连接对象. 对应每一个出接口,可连接对象 ...

随机推荐

  1. Vocabulary

    词汇(Vocabulary) blackmail ( n.) :the obtaining of money or advancement by threatening to make known u ...

  2. 常见的企业Wiki

    企业Wiki(Enterprise Wiki)指适用于企业或组织内部使用的Wiki.与非企业Wiki(如著名的MediaWiki)最根本的不同点在于,企业Wiki是为企业量身定做的Wiki.通过鼓励. ...

  3. 04-华为HyperReplication中的多时间片技术

    简介 多时间片技术,是应用于HyperReplication的异步远程复制:提高效率, 在同步远程复制中,没有这个概念: 时间片:在Cache中管理一段时间内写入数据的逻辑空间(数据大小没有限定): ...

  4. Lora升级!ReLoRa!最新论文 High-Rank Training Through Low-Rank Updates

    关注公众号TechLead,分享AI与云服务技术的全维度知识.作者拥有10+年互联网服务架构.AI产品研发经验.团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专 ...

  5. mac os 升级到13后,系统免密失败

    # sudo vim /etc/ssh/ssh_config # 添加以下内容 PubkeyAcceptedKeyTypes +ssh-rsa

  6. MySQL锁:InnoDB行锁需要避免的坑

    前言 换了工作之后,接近半年没有发博客了(一直加班),emmmm.....今天好不容易有时间,记录下工作中遇到的一些问题,接下来应该重拾知识点了.因为新公司工作中MySQL库经常出现查询慢,锁等待,节 ...

  7. 容器Cgroup和Namespace特性简介

    一般来说,容器技术主要包括Cgroup和Namespace这两个内核特性.Cgroup Cgroup是control group,又称为控制组,它主要是做资源控制.原理是将一组进程放在放在一个控制组里 ...

  8. zookeeper源码(04)leader选举流程

    在"zookeeper源码(03)集群启动流程"中介绍了leader选举的入口,本文将详细分析leader选举组件和流程. leader选举流程(重要) quorumPeer的st ...

  9. SNN_文献阅读_Spiking Deep Convolutional Neural Networks for Energy-Efficient Object Recognition

    两种方法将CNN转化成为SNN: 直接训练一个类似CNN架构的SNN「虽然有类似于STDP等无监督方法,但是处于起步状态」 训练初始的CNN,将训练得到的权重直接应用于类似于CNN架构的SNN「将CN ...

  10. FP-Growth算法全解析:理论基础与实战指导

    本篇博客全面探讨了FP-Growth算法,从基础原理到实际应用和代码实现.我们深入剖析了该算法的优缺点,并通过Python示例展示了如何进行频繁项集挖掘. 关注TechLead,分享AI全维度知识.作 ...