• 网络服务退出一个问题的解析


    一、问题

    在实际开发中遇到一个问题,解决的过程虽然不长,但确实是想得比较多,总结一下,以供参考。这是一个网络通信的服务端而且使用的是别人封装好的库,通信等都没有问题,但在退出时会报一个错误:“Failed to accept the socket”。这个错误引起的原因,通过查看堆栈信息和库源码,发现是服务端在线程中Wait时唤醒后会调用Accept函数引起的。虽然在退出时延时1秒可以解决问题,但这不是优雅的方式。一开始是认为封装库的Wait等待1秒造成必须退出时也跟着等待1秒。但在后来发现,线程的处理中其实已经有对退出线程的等待。以前遇到过类似问题,没有重视,这次一起搞定。

    二、解决

    解决的方法用了不少,下面一个个分析:
    1、把原来的全局网络封装库变量修改为局部指针后,问题消失,即类似如下:

    netlib nl;//全局变量
    int net(){
    ...
      //netlib nl;//局部变量
      netlib * nl = new netlib;
    std::thread td = std::thread([&](){
      nl.init();
      nl.stop();
      //修改为
    
      nl->init();
      nl.stop();
      });
    
      //td.deatch();
    
      //delete nl;//主动删除
      ...
    
      td.join();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    通过打印的日志发现,指针不主动回收由操作系统在程序完成后回收时,是不调用析构函数的。如果将上面的注释的删除指针解开,则会调用析构函数。但是对于局部和全局变量来说,则会主动调用析构函数,而错误也就是在这个析构函数中产生的。
    2、释放顺序和析构函数
    在netlib内部的接收网络数据函数中,也有一个线程,为了线程的安全退出在析构函数调用了类似下而把 机制:

    netlib::~netlib{
    
     closesocket();//注意这个函数是调用的父类的函数,问题就在这
      if (td && td->joinable()) {
        td->join();
      }
    // closesocket();//正确
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    理论上讲不会出现这个问题,但实际上只要是使用变量而不是指针(不主动删除)或者主动删除指针,同样会出现文中的异常。因为封装的库是继承了Socket类,所有的操作几乎全在父类中进行,于是突然怀疑是不是父类被先释放了相关值造成的,然后试着打印了一下(注意,这里引入了假的二次释放问题),验证确实如此。可忽然想到,释放顺序中,父类是后被释放的,这不符合道理的(不考虑虚基类)。于是写了一个相关释放的过程,发现确实是如此。
    3、二次析构
    事情到此,基本已经明了,然后 开始出昏招。由于程序都在一个工程中修改的,结果开始出现重复释放崩溃的问题,也就是说,有两次调用析构函数的动作,那想当然啊,二次析构崩溃的概率太大了。这个问题定位了足有半小时,才突然想起来是不是全局变量导致的,可又一想全局变量调用也不应该崩溃啊,毕竟全局变量只是定义了一下,又没有工作,连初始化都没有。然后自然就想起了为了验证父类先释放引入的函数,果然。这里说一下,这个netlib类有一个变量,它可以在判断返回是否为空指针,它有点稍微误导人,让人认为其可以使用,特别是在此次调试过程中,乱了套了。于是把后面的非判断空的函数上移,果然直接崩溃。立刻明白了怎么回事,引入了新BUG,二次释放崩溃不是真正的二次释放,是两个变量当然释放两次,只是因为引用了空指针调用相关函数,不崩溃会怎么样?

    4、解决问题
    这时候重新回来审视析构函数,经过分析日志,发现了端倪,那个异常总是在进入析构函数后,进入到第一个判断join后出现,而这个join意味着线程还活着,还在操作Socket,可一进入析构函数就调用了那个closesocket函数,它不能在前面啊,使用了它,当然后面的判断中父类中的指针当然是空的,线程在Wait(调用Accept)当然会报一个异常。然后 把其后移到正确位置,一切OK。
    大意了,资源的释放一定要有顺序,在使用者之后再释放,或者提前使用函数控制线程退出(这也是测试的过程中使用的,即要stop函数中进行了线程的集中退出),只在析构函数中处理资源。其实这才是正规的处理方式,不要在构造和析构函数里进行业务相关的代码控制,此次使用封装库,降低了戒备心,致此结果。

    三、扩展

    在上面解决问题的基础上,又想到两个问题:
    1、使用智能指针
    使用智能指针测试的结果和变量基本类似(但得去除主动删除指针的代码),会有一个析构的动作,其它都没有问题。代码类似于:

      auto nl = netlib::get();
      std::thread td = std::thread([&]() {
        nl->init();
        nl->stop();
      });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2、使用线程detach
    在前面的线程中使用了线程join的方式,让各个线程协调有序的退出,其实也可以使用detach,让线程主动分离。在本次的工程中,这样做是没有问题的,但需要注意的是,如果需要协调各个线程中有相关资源时互相调用时,还是需要处理一下线程中退出的顺序,否则仍然会引起崩溃。
    这里只是一个Socket的接收动作,从打印日志看,直接就退出了子线程,然后 父线程再调用析构函数(调用关闭Socket)退出。
    这个问题,连带测试用例和相关分析,用了小半天时间,也是一个教训吧。

    四、总结

    其实,正如前面的分析,解决问题的思路其实就是基础知识的检查过程。在这次解决问题的过程中,数次发现和基础知识理解对不上,所以否定了自己的想法(比如析构函数的顺序)。只要按照标准和规则来,解决问题一定是水到渠成的。不要总想着弯道超车,没有大量的基础知识做底子,超车也是运气的结果。
    这次解决这个问题,正是一次比较多面的对基础知识的一次综合运用,收获颇丰。

  • 相关阅读:
    GC日志开启及分析
    【华为机试真题 JAVA】组成最大数-100
    BFS——武士风度的牛
    【Java】equals() 方法详解
    Apache Drill 2万字面试题及参考答案
    Ali-Sentinel-Spring WebMVC 流控
    【阿旭机器学习实战】【23】特征降维实战---人脸识别降维建模,并选出最有模型进行未知图片预测
    JavaWeb-使用session机制和cookie机制改造JavaWeb基础项目
    js获取当前月份天数,获取指定月份的天数
    【***操作系统---第五章***】
  • 原文地址:https://blog.csdn.net/fpcc/article/details/134231203