memcached网络模型

网络模型图

这里写图片描述

memcached网络模型

  最近看了下memcached的网络模型,memcached的模型就是一个标准的半同步半反应堆模型,如下所示
这里写图片描述

主线程调度任务

  在memcached中,主线程主要负责监听socket,当有连接到来的时候,主线程就会被唤醒,然后进入回调函数随即在回调函数中又转到了drive_machine函数。drive_machine这个函数是一个基于状态机的一个函数,它主要是用来网络I/O的。它根据处于不同阶段的连接来转调不同函数。那么主线程进入这个函数的状态肯定是conn_listen状态,所以它会accept新连接,然后选取一个子线程,通过管道发生一些命令,然后把新接受到的socket封装成一个结构体push到相应子线程的队列中。
  第一段我们主要讲了主线程干了什么,那么为什么要有drive_machine这个函数存在呢,为什么要有状态存在,我们完全可以直接把新接受到的连接给子线程即可,为什么还分那么多状态?
  其实把请求状态化的原因主要是以下几点

  1. 分多个状态可以把连接模块化,从而是子线程能够真正的多路复用起来,如果我们直接连接交给子线程,不记录其处理的状态,那么势必只能一个一个的阻塞式处理连接,那么子线程就无法发挥多路复用的功能。所以我们要状态化,记录每一个连接的状态当多路复用的时候下次该连接的某个 read / write就绪的时候,我们能够让子线程知道上次这个连接处理到了那一步,我们可以让它根据上一次的结果来继续去完成连接请求,这也就能够使子线程真正的能够多路复用起来。比如nginx也是把一个连接模块化来提高每个work进程的效率。
  2. 模块化也助于代码的解耦,如果我们把所有的处理操作都写到一个模块里面,然后这个模块将臃肿不堪,如果我们模块化了后期的维护和重构都将能事半功倍。
子线程处理请求

  在详细阐述子线程的模型之前,我们先提及一下全局作用域的conn数组。首先我个人认为这个数组设计的很巧妙,它是用来当子线程 的 管道fd收到消息后其callback函数被带哦用,然后子线程将自己的任务队列中取出具体的任务后,把该连接的相应信息保存到conn数组中,方便后续处理连接的时候索引到具体socket信息,伪代码这样的

extern conn * Conn;
CQ_ITEM * item = CQ_ITEM::pop()
int clientsocket = item->socket_;
if(!Conn[clientsocket])
{
     conn[clientsocket] = malloc(con);
     conn[clientsocket]->sockfd_ = clientsocket;
}
剩下的操作是进行一些conn结构体的初始化工作,然后注册网络I/O的event事件,当client
数据到来在其callback函数中转调 drive_machine函数来网络I/O...

  其实这里有一点,访问一个全局变量(conn数组)为什么没有加锁呢?
  这里memcached就处理的很巧妙,因为它是根据sockfd来索引这个全局变量的,所以原子性由 Linux kernel来保证,无论子线程怎么去close fd 或者 打开fd,其线程组(也就是进程)的文件描述符表内的fd(实际未struct file 结构体指针)都是具有原子性的,不会因为多线程的原因同一个fd其实索引到了俩个不同的文件(socket也是文件Linux一切皆文件),所以内核帮助我们保证了fd上的原子性,我们在应用层才可以不加锁的访问全局变量。
  第二个疑问为什么放全局呢,为什么不每个线程单独一个conn指针数组呢?
  因为本身索引conn结构就是原子性了,所以没必要每个线程一个单独的conn指针数组,这样可以节省内存
  第三个疑问,为什么使用指针数组而不是结构体数组?
  因为节省内存,比如我们申请1024大小的数组,直接申请了这么多,可能并发太低,每个子线程使用到的fd根本用不到这么多,所以我们使用指针变量在堆上随用随申请这样优点是节省内存,但是也有缺点申请的conn结构体在内存上并不连续,这样降低了缓存局部性。但是,Linux线程属于内核线程,真正由kernei调度,所以运行的时候每个 cpu都会绑定对应的线程,一般L1 cache 是每个核单独、L2 是相邻核共享、L3是所有核共享,所以如果这个程序的多线程并行调度的时候,L1/L2其实意义不大,我们真正损失的其实只有L3cache的局部性,所以影响不是特别大。
  那么剩下的子线程其实没什么讲了,无法就是根据不同的状态,drive_machine函数会转调不同的函数,来使子线程完成网络I/O。

总结

  我应用了下memcached的网络模型,写了个Http。其中对锁和多线程的同步有了新的理解。对应锁方面,当我们想要一系列操作都是原子的,那么可以把这些操作封装成一个函数在函数里面使用锁,这样就避免了在多处写相同的加锁或者解锁的代码避免不小心写错造成的死锁。第二个就是多线程同步方面,memcached有一段代码是主线程pthread_create后,在等待所有线程都创建完毕后再进行其他的操作,它是这样做的

同样伪代码
int  Globe_init_count;
 pthread_cond_t cond;
 pthread_mutex_t mutex;
void setup_threads()
{
   ...
   pthread_cond_init(&cond);
   pthread_mutex_lock(&mutex)
   while(Globe_init_count< TRHEADS)
   {
       pthread_cond_wait(&cond , &mutex);
   }
   pthread_mutex_unlock(&mutex);
}
void* ThreadRun(void * arg)
{
     pthread_mutex_lock(&mutex);
     Globe_init_count++;
     pthread_cond_signal(&cond);
     pthread_mutex_unlock(&mutex);
     ......其他操作
}

  代码如上虽然很简单但是也是讲解了条件变量怎么用,所以这里有个疑问为什么pthread_cond_wait函数多一个 mutex参数呢?
  试想如果pthread_cond_wait(&cond)是这种接口的话,那么我们只能先解锁再调用wait函数,试想 unlock 与 wait 之间的时候条件就绪了发生了相应的信号,但是这个信号我们错过了,那就可能一直被阻塞到了这里。

pthread_mutex_unlock(&lock);
pthread_mutex_wait(&cond);

  还学习到了主线程怎么给子线程或子进程通知条件就绪啦,主要通过管道或者其他ipc(进程间通信)去通知条件就绪,nginx也是这样做的,所以下次有其他的需求我们也可以这样实现。还有内存池的一些小技巧,这个其实更推荐大家去看SGI_STL的内存池管理。

猜你喜欢

转载自blog.csdn.net/sdoyuxuan/article/details/82110313