相关知识补充

EPOLL的相关API是线程安全吗?

是的。

EPOLL的设计缺陷

epoll有一个巨大的设计失误,但凡理解文件描术符是什么的人都能看到这一点。但是如果你回望epoll的历史,你会发现设计者显然不理解文件描述符与文件描述的区别。
epoll的缺陷在于它把文件描述符当作内核对象(文件描述)使用。当使用close()方法清理epoll事件订阅时,问题就出现了。
epoll_ctl(EPOLL_CTL_ADD) 并没有注册文件描述符,而是注册了一个文件描述符元组和内核对象的指针。最让人困惑的是事件关注者的生命周期跟文件描述符无关,而与内核对象的生命周期相关.
由于这个设计缺陷,对文件描述符调用close()方法可能会,也可能不会取消对epoll事件的订阅。如果close方法删除了内核对象的最后一个指针而让对象释放,epoll事件的订阅即可清除。但如果内核对象有多个指针,多个文件描述符,不管在哪一个进程中,close方法都无法清除订阅,很可能会收到已关闭的文件描述符的事件。

EPOLL ONESHOT

简单来说就是令EPOLL触发的事件只触发一次,直到下一次调用epoll_ctl进行mod 重置EPOLLONESHOT这个事件才会继续监听事件。可以避免在对一个文件描述符进行读事件的时候客户端又发来请求,这时候如果再分配一个线程来处理请求就会产生错误,如果加锁的话就大大影响多线程并发效率。添加这个事件的好处就是我们可以将缓冲区的内容循环读取干净后再重置,也就是读取过程中EPOLL将不会监听这个文件描述符了。

虚假唤醒

简单来说就是线程阻塞在条件变量上,之后得到信号量了但是没有资源(可能有其他线程也在等这个资源刚好被抢走了),但是线程的确被唤醒了,和我们一开始想的有资源线程就启动所违背,这就是虚假唤醒。
避免虚假唤醒一般是:

while(资源为空==true)
    cond.wait();

RAII

翻译过来中文就是获取资源的时候就初始化。这种使用方式就是为了避免资源泄露,自动的释放资源。例如下面这段代码:

class Lock{
  Lock(mutex * mute): mut(mute){
     mut.Lock()
  }
  ~Lock()
  {
    mute.unLock();
  }
private:
   mutex *mut;


}
int func()
{
    static int count=0;
    Lock lock(&mutex)
    printf("%d",count++);
}

上述代码中就是在离开所在区块的时候就释放锁(通过调用Lock的析构函数)。

BUG篇

线程池发布任务的时候需要考虑到唤醒的速度和发布任务的速度不一致的问题。发布任务所在的线程执行完毕时,需要被唤醒的线程可能未必已经唤醒了,这时候如果再来一个任务,可能会将刚刚唤醒的任务重新唤醒一遍,并导致将之前尚未开始的任务覆盖。(已解决)

解决方案: 在检索到空闲任务的时候直接将空闲任务置为忙,不在任务线程中置为运行态。
更好的改进:将线程池独立一个线程里面进行管理任务就绪队列(重新封装的安全队列),发布任务API只是将队列中添加任务,真正将任务从就绪队列中移除,放入工作线程中是在线程池所在的线程中执行的,同时也在这个线程监控线程池实时状态,进行扩充或者缩减。


纯虚函数被调用,这个问题是在哪里出现的。(已解决)

原因:线程基类初始化的时候没有将成员全部初始化,其中有一个成员是决定线程是否开始运行,由于没有初始化状态是位置的,所以这个问题出现的很随机。


第一次的发包都是正确的,但是每个文件描述符第二次发包就很多没有响应到。(已解决)

解决方案: 问题8的连锁反应。


SIGEGV 段错误。每次的文件描述符莫名其妙变成负数,之后再调用就变成 出错了,查看堆栈的时候出现这样子(已解决)

# 0  0x0000000000763370 in ?? ()
# 1  0x000000000040c840 in TCPClient::recvPacket (this=0x698e00)
    at TCP/TCPClient.cpp:45
# 2  0x0000000000414a55 in ClientTask::run (this=0x762ff0)



# 0  0x0000000000759790 in ?? ()
# 1  0x000000000040b2a0 in TCPClient::recvPacket (this=0x6e6c50)

解决方案:

  • 由于分配任务是利用多态的,删除时候只删除了基类对象的内存空间,没有释放派生类的,需要在基类对象的析构函数上加上virtual
  • 线程池缩容的时候由于是简单判断线程的状态,可能出现一种情况,当线程这时候是空闲状态,当线程池要缩容的时候刚好分配任务过来了,就出错了。(已解决。将分配任务和缩容放在同一个线程里管理)

文件描述符意外关闭的时候 读取SIGPIPE 错误需要正确处理。(已解决)

解决方案:重新设计一个单例类,进行全局环境的初始化,包括相关信号的捕捉和线程池、端口等的设置。


问题:出现SIGABRT错误,连续释放指针对象

解决方案: 对应线程池中的分配任务、扩容、缩容一直都会出现问题,通过将这些放在同一个线程里就都解决了。


问题:epoll 在移除文件描述符的时候 出现 no such file or dir … (已解决)

原因: 多线程的问题,epoll中的移除添加函数中的关键变量是成员变量,多线程的时候会出现意外情况。例如多个线程同时在用同时修改成员变量或者同时使用同一个成员变量。


当线程池需要缩容的时候就会出现段错误。(已解决)

解决方案:使用自己封装的安全队列。


速度太慢了,而且线程池的扩容明明速度不够的时候没有及时响应,但是不知道为什么在一开始的时候等待任务队列会被判定为非空。(已解决)

原因: 多线程并发的问题,没有加锁。队列不安全。


为什么每次就只有第一个FD响应请求,其他的FD的请求都没有响应到(已解决)

原因: 在删除冗余代码的时候没有将break删除掉导致退第一次判断结束后就退出循环了!!(好蠢的错误)


总结

其实大部分的错误都是刚刚接触多线程编程时候带来的错误,包括对于线程安全的认识以及锁、条件变量、信号量等的认识都不是很充分。尤其是第一次遇到这么多问题而且很多问题都是需要和压力测试端一起解决的优点措手不及(以前都是自己单步调试慢慢找到的),现在也学到很多GDB相关调试的知识。