memcache 线程模型

Mempool 采用主线程+工作线程的模型,主进程主要是:1创建工作线程 2接受连接并把任务分配给工作线程。

子进程主要做具体的工作:解析请求,处理请求。

 

 

 

 

 

一  创建工作线程

先来看源代码

/* * Initializes the thread subsystem, creating various worker threads. * * nthreads Number of worker event handler threads to spawn * main_base Event base for main thread */ void thread_init(int nthreads, struct event_base *main_base) { int i; //初始化线程的变量,包括锁,条件变量,空闲连接队列等 pthread_mutex_init(&cache_lock, NULL); pthread_mutex_init(&stats_lock, NULL); pthread_mutex_init(&init_lock, NULL); pthread_cond_init(&init_cond, NULL); pthread_mutex_init(&cqi_freelist_lock, NULL); cqi_freelist = NULL; //给线程分配空间 threads = calloc(nthreads, sizeof(LIBEVENT_THREAD)); if (! threads) { perror("Can't allocate thread descriptors"); exit(1); } dispatcher_thread.base = main_base; dispatcher_thread.thread_id = pthread_self(); //初始化所有的线程:包括管道和其它设置 for (i = 0; i < nthreads; i++) { int fds[2]; if (pipe(fds)) { perror("Can't create notify pipe"); exit(1); } threads[i].notify_receive_fd = fds[0]; threads[i].notify_send_fd = fds[1]; setup_thread(&threads[i]); } /* Create threads after we've done all the libevent setup. */ //创建工作线程 for (i = 0; i < nthreads; i++) { create_worker(worker_libevent, &threads[i]); } /* Wait for all the threads to set themselves up before returning. */ //所有的线程都起来了才退出 pthread_mutex_lock(&init_lock); while (init_count < nthreads) { pthread_cond_wait(&init_cond, &init_lock); } pthread_mutex_unlock(&init_lock); }

整个过程:

1 初始化主线程的变量

 

2创建和初始化所有工作线程的资源

 

3 启动工作线程

 

4 等待所有工作线程创建完成。

 

我们可以看到为每个线程创建管道的过程,其它的初始都是在函数setup_thread中完成,我们来看看它具体做些什么事情

/* * Set up a thread's information. */ static void setup_thread(LIBEVENT_THREAD *me) { //创建libevent me->base = event_init(); if (! me->base) { fprintf(stderr, "Can't allocate event base/n"); exit(1); } //监听事件 /* Listen for notifications from other threads */ event_set(&me->notify_event, me->notify_receive_fd, EV_READ | EV_PERSIST, thread_libevent_process, me); event_base_set(me->base, &me->notify_event); if (event_add(&me->notify_event, 0) == -1) { fprintf(stderr, "Can't monitor libevent notify pipe/n"); exit(1); } //创建连接队列并初始化 me->new_conn_queue = malloc(sizeof(struct conn_queue)); if (me->new_conn_queue == NULL) { perror("Failed to allocate memory for connection queue"); exit(EXIT_FAILURE); } cq_init(me->new_conn_queue); //初始化互斥锁 if (pthread_mutex_init(&me->stats.mutex, NULL) != 0) { perror("Failed to initialize mutex"); exit(EXIT_FAILURE); } //创建后缀缓存 me->suffix_cache = cache_create("suffix", SUFFIX_SIZE, sizeof(char*), NULL, NULL); if (me->suffix_cache == NULL) { fprintf(stderr, "Failed to create suffix cache/n"); exit(EXIT_FAILURE); } }

整个过程

1 创建libevent,并进行初始化,并加入监听的事件

 

2  创建工作线程的连接队列

 

3 初始化互斥锁和后缀缓冲

 

 

这样所有的工作线程都运行起来了,各自干各自的活。

 

问题:

 

1 为什么主线程需要等到所有工作线程启动完成了才去干自己的事情?

 

是不是防止下面情况发生: 主线程去接受客户端的请求,并把任务分发给一个工作线程,而该线程又没有完成启动,就会出错??理解错了希望大家指正。

 

 

 

 

 

 

二  主线程接受请求并分发给工作线程

 

当主线程完成了工作线程的启动,就开始进入自己的正常工作了:循环的调用libevent,是否有连结到来,如果有,那么通过简单的round robin 选择一个线程进行具体的工作。

 

首先主线程调用server_socket函数,来启动监听的socket。具体在conn_new函数中通过调用event_set把监听事件处理函数设置为 event_handler ,这样当有外部的连接过来的时候,就会直接调用event_handler

 

而在event_handler中,直接调用drive_machine来处理具体事件。

 

最后我们来看看drive_machine 干什么事情,其实这个函数就是根据连接的状态进行不同的处理,比如我们连接过来的时候,状态就是 conn_listening:

 

case conn_listening: //接受连接 addrlen = sizeof(addr); if ((sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen)) == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { /* these are transient, so don't log anything */ stop = true; } else if (errno == EMFILE) { if (settings.verbose > 0) fprintf(stderr, "Too many open connections/n"); accept_new_conns(false); stop = true; } else { perror("accept()"); stop = true; } break; } if ((flags = fcntl(sfd, F_GETFL, 0)) < 0 || fcntl(sfd, F_SETFL, flags | O_NONBLOCK) < 0) { perror("setting O_NONBLOCK"); close(sfd); break; } dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST, DATA_BUFFER_SIZE, tcp_transport); stop = true; break;

就是接受连接,并设置socket为非阻塞,最后调用 dispatch_conn_new 处理

 

 

 

void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags, int read_buffer_size, enum network_transport transport) { CQ_ITEM *item = cqi_new(); int tid = (last_thread + 1) % settings.num_threads; LIBEVENT_THREAD *thread = threads + tid; last_thread = tid; item->sfd = sfd; item->init_state = init_state; item->event_flags = event_flags; item->read_buffer_size = read_buffer_size; item->transport = transport; cq_push(thread->new_conn_queue, item); MEMCACHED_CONN_DISPATCH(sfd, thread->thread_id); if (write(thread->notify_send_fd, "", 1) != 1) { perror("Writing to thread notify pipe"); } }

从自由链表中获取一个连接,并选择一个工作线程,把连接挂载到工作线程的连接队列中,最后通过管道通知工作线程,工作线程就能触发一个事件,开始工作。

 

 

 

 

 

 

三  工作线程

 

当主线程通过管道发送一个字节的数据的时候,工作线程通过libevent会触发事件,直接调用thread_libevent_process 函数

 

/* * Processes an incoming "handle a new connection" item. This is called when * input arrives on the libevent wakeup pipe. */ static void thread_libevent_process(int fd, short which, void *arg) { LIBEVENT_THREAD *me = arg; CQ_ITEM *item; char buf[1]; //libevent采用水平触发,读取一个字节,防止不停触发 if (read(fd, buf, 1) != 1) if (settings.verbose > 0) fprintf(stderr, "Can't read from libevent pipe/n"); //从队列中获取一个连接 item = cq_pop(me->new_conn_queue); if (NULL != item) { //创建一个连接 conn *c = conn_new(item->sfd, item->init_state, item->event_flags, item->read_buffer_size, item->transport, me->base); if (c == NULL) { if (IS_UDP(item->transport)) { fprintf(stderr, "Can't listen for events on UDP socket/n"); exit(1); } else { if (settings.verbose > 0) { fprintf(stderr, "Can't listen for events on fd %d/n", item->sfd); } close(item->sfd); } } else { c->thread = me; } cqi_free(item); } }

 

这个函数就是读取一个字节数据,防止重复触发事件,再去调用cq_pop ,从线程的等待队列中得到一个连接(socket分装),通过调用conn_new来创建一个连接,在conn_new中主要创建一个连接,并往libevent中添加监听的事件,这样可以监听来自客户端的数据.当有数据来的时候,和前面一样会去调用drive_machine函数,根据连接的状态进行不同的处理。

        所以工作线程而言,会触发两类事件,一个来自管道(主线程),一个来自socket(客户端的),前者在刚接受一个新的请求时触发,并且只有一次, 后者有客户端出发,可以多次。并且前者的触发才往libevent中添加后者监听事件,所以后者一定在前者之后。  

 

 

 

 

 

 

总结:

 

 1 通知直接使用libevent,简化了异步编程

              

 2 通过主线程和工作线程,使每个角色的任务简单了。主进程和工作线程通过简单的管道实现通信息,保证了系统的高效。

 

 3 工作线程的选择通过简单的 round robin, 对于cache这种业务简单的应用,具有很好的性能。

 

 4  运用很多“池”的方式,提交资源分配效率(会有少量的浪费)

 

 

 

 

参考

 

http://www.javaeye.com/topic/344172    Memcached源码分析(线程模型)

 

http://www.javaeye.com/topic/251488  memcached的通讯层分析

 

 

你可能感兴趣的:(linux)